@loidolt/theme-maps 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/components/BasemapSwitcher.svelte +53 -0
  4. package/dist/components/BasemapSwitcher.svelte.d.ts +20 -0
  5. package/dist/components/CircleLayer.svelte +22 -0
  6. package/dist/components/CircleLayer.svelte.d.ts +4 -0
  7. package/dist/components/CoordinateDisplay.svelte +68 -0
  8. package/dist/components/CoordinateDisplay.svelte.d.ts +13 -0
  9. package/dist/components/DeckOverlay.svelte +58 -0
  10. package/dist/components/DeckOverlay.svelte.d.ts +18 -0
  11. package/dist/components/FillExtrusionLayer.svelte +17 -0
  12. package/dist/components/FillExtrusionLayer.svelte.d.ts +4 -0
  13. package/dist/components/FillLayer.svelte +21 -0
  14. package/dist/components/FillLayer.svelte.d.ts +4 -0
  15. package/dist/components/HeatmapLayer.svelte +36 -0
  16. package/dist/components/HeatmapLayer.svelte.d.ts +4 -0
  17. package/dist/components/LayerManager.svelte +234 -0
  18. package/dist/components/LayerManager.svelte.d.ts +24 -0
  19. package/dist/components/LineLayer.svelte +18 -0
  20. package/dist/components/LineLayer.svelte.d.ts +4 -0
  21. package/dist/components/MapControl.svelte +47 -0
  22. package/dist/components/MapControl.svelte.d.ts +13 -0
  23. package/dist/components/MapFeatureList.svelte +67 -0
  24. package/dist/components/MapFeatureList.svelte.d.ts +20 -0
  25. package/dist/components/MapLegend.svelte +68 -0
  26. package/dist/components/MapLegend.svelte.d.ts +19 -0
  27. package/dist/components/MapLegendGroup.svelte +72 -0
  28. package/dist/components/MapLegendGroup.svelte.d.ts +19 -0
  29. package/dist/components/MapMarker.svelte +130 -0
  30. package/dist/components/MapMarker.svelte.d.ts +32 -0
  31. package/dist/components/MapPopup.svelte +123 -0
  32. package/dist/components/MapPopup.svelte.d.ts +21 -0
  33. package/dist/components/MapSource.svelte +109 -0
  34. package/dist/components/MapSource.svelte.d.ts +31 -0
  35. package/dist/components/MapView.svelte +454 -0
  36. package/dist/components/MapView.svelte.d.ts +76 -0
  37. package/dist/components/RasterLayer.svelte +14 -0
  38. package/dist/components/RasterLayer.svelte.d.ts +4 -0
  39. package/dist/components/SymbolLayer.svelte +22 -0
  40. package/dist/components/SymbolLayer.svelte.d.ts +4 -0
  41. package/dist/core/accessibility.d.ts +24 -0
  42. package/dist/core/accessibility.js +33 -0
  43. package/dist/core/bounds.d.ts +18 -0
  44. package/dist/core/bounds.js +91 -0
  45. package/dist/core/cluster.d.ts +35 -0
  46. package/dist/core/cluster.js +44 -0
  47. package/dist/core/data.d.ts +33 -0
  48. package/dist/core/data.js +157 -0
  49. package/dist/core/equality.d.ts +7 -0
  50. package/dist/core/equality.js +33 -0
  51. package/dist/core/format.d.ts +20 -0
  52. package/dist/core/format.js +47 -0
  53. package/dist/core/index.d.ts +16 -0
  54. package/dist/core/index.js +18 -0
  55. package/dist/core/lod.d.ts +27 -0
  56. package/dist/core/lod.js +49 -0
  57. package/dist/core/route.d.ts +58 -0
  58. package/dist/core/route.js +182 -0
  59. package/dist/core/simplify.d.ts +9 -0
  60. package/dist/core/simplify.js +102 -0
  61. package/dist/core/spatial.d.ts +21 -0
  62. package/dist/core/spatial.js +88 -0
  63. package/dist/core/style.d.ts +43 -0
  64. package/dist/core/style.js +205 -0
  65. package/dist/core/types.d.ts +110 -0
  66. package/dist/core/types.js +5 -0
  67. package/dist/core/workers/client.d.ts +26 -0
  68. package/dist/core/workers/client.js +88 -0
  69. package/dist/core/workers/handler.d.ts +6 -0
  70. package/dist/core/workers/handler.js +53 -0
  71. package/dist/core/workers/protocol.d.ts +32 -0
  72. package/dist/core/workers/protocol.js +1 -0
  73. package/dist/core/workers/spatial.worker.d.ts +1 -0
  74. package/dist/core/workers/spatial.worker.js +12 -0
  75. package/dist/deck.d.ts +18 -0
  76. package/dist/deck.js +27 -0
  77. package/dist/index.d.ts +27 -0
  78. package/dist/index.js +23 -0
  79. package/dist/internal/context.d.ts +39 -0
  80. package/dist/internal/context.js +15 -0
  81. package/dist/internal/safely.d.ts +5 -0
  82. package/dist/internal/safely.js +12 -0
  83. package/dist/internal/use-layer.svelte.d.ts +45 -0
  84. package/dist/internal/use-layer.svelte.js +179 -0
  85. package/dist/maplibre.d.ts +19 -0
  86. package/dist/maplibre.js +47 -0
  87. package/dist/styles.css +10 -0
  88. package/package.json +76 -0
@@ -0,0 +1,182 @@
1
+ /*
2
+ * Straight-line routing: great-circle distances, a good visiting order for a set of stops, and
3
+ * links that hand the order to Google or Apple Maps for turn-by-turn directions. No routing
4
+ * service, no key — the order is by distance as the crow flies, which is what a day's stops
5
+ * usually need before a driver looks at roads.
6
+ */
7
+ /** Mean earth radius, in metres (IUGG). */
8
+ export const EARTH_RADIUS = 6_371_008.8;
9
+ const radians = (degrees) => (degrees * Math.PI) / 180;
10
+ const finite = ([lng, lat]) => Number.isFinite(lng) && Number.isFinite(lat);
11
+ /** Great-circle distance in metres. `NaN` when either point is not a real coordinate. */
12
+ export function haversine(a, b) {
13
+ if (!finite(a) || !finite(b))
14
+ return Number.NaN;
15
+ const [dLat, dLng] = [radians(b[1] - a[1]), radians(b[0] - a[0])];
16
+ const h = Math.sin(dLat / 2) ** 2 +
17
+ Math.cos(radians(a[1])) * Math.cos(radians(b[1])) * Math.sin(dLng / 2) ** 2;
18
+ return 2 * EARTH_RADIUS * Math.asin(Math.min(1, Math.sqrt(h)));
19
+ }
20
+ /** The length of a path through `points`, in metres. */
21
+ export const pathLength = (points) => points.slice(1).reduce((sum, point, index) => sum + haversine(points[index], point), 0);
22
+ /** Every pairwise distance, in metres. Symmetric, with zeros on the diagonal. */
23
+ export function distanceMatrix(points) {
24
+ const matrix = points.map(() => new Array(points.length).fill(0));
25
+ for (let i = 0; i < points.length; i++) {
26
+ for (let j = i + 1; j < points.length; j++) {
27
+ matrix[i][j] = matrix[j][i] = haversine(points[i], points[j]);
28
+ }
29
+ }
30
+ return matrix;
31
+ }
32
+ function measure(origin, points, order, matrix) {
33
+ const legs = order.map((stop, index) => index === 0 ? haversine(origin, points[stop]) : matrix[order[index - 1]][stop]);
34
+ return {
35
+ order,
36
+ legs,
37
+ total: legs.reduce((sum, leg) => sum + (Number.isFinite(leg) ? leg : 0), 0),
38
+ };
39
+ }
40
+ function greedy(origin, points, matrix) {
41
+ const visited = new Array(points.length).fill(false);
42
+ const order = [];
43
+ while (order.length < points.length) {
44
+ let best = -1;
45
+ let bestDistance = Infinity;
46
+ for (let index = 0; index < points.length; index++) {
47
+ if (visited[index])
48
+ continue;
49
+ const distance = order.length
50
+ ? matrix[order.at(-1)][index]
51
+ : haversine(origin, points[index]);
52
+ // Strictly less: a tie keeps the earlier stop, so the order is stable.
53
+ if (Number.isFinite(distance) && distance < bestDistance)
54
+ [best, bestDistance] = [index, distance];
55
+ }
56
+ if (best === -1) {
57
+ // Whatever is left cannot be measured; keep it, in the order given.
58
+ for (let index = 0; index < points.length; index++)
59
+ if (!visited[index])
60
+ order.push(index);
61
+ break;
62
+ }
63
+ visited[best] = true;
64
+ order.push(best);
65
+ }
66
+ return order;
67
+ }
68
+ /**
69
+ * Improves an order by reversing any stretch that shortens the route, until none does (2-opt).
70
+ * The route starts at `origin` and ends at its last stop; it does not return.
71
+ */
72
+ export function twoOpt(origin, points, order, { maxPasses = 20, matrix = distanceMatrix(points), } = {}) {
73
+ const best = [...order];
74
+ if (best.length < 3)
75
+ return best;
76
+ const fromOrigin = points.map((point) => haversine(origin, point));
77
+ const last = best.length - 1;
78
+ for (let pass = 0; pass < maxPasses; pass++) {
79
+ let improved = false;
80
+ for (let i = 0; i < last; i++) {
81
+ for (let j = i + 1; j <= last; j++) {
82
+ const before = i === 0 ? fromOrigin[best[0]] : matrix[best[i - 1]][best[i]];
83
+ const after = i === 0 ? fromOrigin[best[j]] : matrix[best[i - 1]][best[j]];
84
+ // A stretch running to the end has no edge after it: the route just stops there.
85
+ const tailBefore = j === last ? 0 : matrix[best[j]][best[j + 1]];
86
+ const tailAfter = j === last ? 0 : matrix[best[i]][best[j + 1]];
87
+ if (after + tailAfter - before - tailBefore < -1e-6) {
88
+ best.splice(i, j - i + 1, ...best.slice(i, j + 1).reverse());
89
+ improved = true;
90
+ }
91
+ }
92
+ }
93
+ if (!improved)
94
+ break;
95
+ }
96
+ return best;
97
+ }
98
+ /**
99
+ * A short order to visit `stops` from `origin`: nearest neighbour first, then 2-opt. Not the
100
+ * shortest possible — that problem is intractable — but close for a day's worth of stops.
101
+ */
102
+ export function optimizeRoute(origin, stops, { improve = true, maxPasses } = {}) {
103
+ if (!stops.length)
104
+ return { order: [], legs: [], total: 0 };
105
+ const matrix = distanceMatrix(stops);
106
+ const first = greedy(origin, stops, matrix);
107
+ const order = improve ? twoOpt(origin, stops, first, { maxPasses, matrix }) : first;
108
+ return measure(origin, stops, order, matrix);
109
+ }
110
+ /** Legs and total for an order you already have, to compare against an optimised one. */
111
+ export const measureRoute = (origin, stops, order) => measure(origin, stops, order, distanceMatrix(stops));
112
+ const encodeStop = (stop) => {
113
+ if (typeof stop === 'string')
114
+ return stop.trim() ? encodeURIComponent(stop.trim()) : null;
115
+ return finite(stop) ? `${stop[1]},${stop[0]}` : null;
116
+ };
117
+ /** Google Maps allows this many stops between origin and destination in one link. */
118
+ export const GOOGLE_MAX_WAYPOINTS = 9;
119
+ /** A Google Maps directions link through `stops`, the last being the destination. */
120
+ export function googleDirectionsUrl(stops, { origin, travelMode } = {}) {
121
+ const encoded = stops.map(encodeStop).filter((stop) => stop !== null);
122
+ if (!encoded.length)
123
+ return '';
124
+ const start = origin === undefined ? null : encodeStop(origin);
125
+ const params = [
126
+ 'api=1',
127
+ ...(start ? [`origin=${start}`] : []),
128
+ `destination=${encoded.at(-1)}`,
129
+ ...(encoded.length > 1 ? [`waypoints=${encoded.slice(0, -1).join('%7C')}`] : []),
130
+ ...(travelMode ? [`travelmode=${travelMode}`] : []),
131
+ ];
132
+ return `https://www.google.com/maps/dir/?${params.join('&')}`;
133
+ }
134
+ /** An Apple Maps directions link through `stops`. */
135
+ export function appleDirectionsUrl(stops, { origin, travelMode } = {}) {
136
+ const encoded = stops.map(encodeStop).filter((stop) => stop !== null);
137
+ if (!encoded.length)
138
+ return '';
139
+ const start = origin === undefined ? null : encodeStop(origin);
140
+ // Apple has no cycling mode in links; it falls back to driving, as it does itself.
141
+ const mode = travelMode === 'walking' || travelMode === 'transit' ? travelMode : 'driving';
142
+ const params = [
143
+ ...(start ? [`source=${start}`] : []),
144
+ ...encoded.slice(0, -1).map((stop) => `waypoint=${stop}`),
145
+ `destination=${encoded.at(-1)}`,
146
+ `mode=${mode}`,
147
+ ];
148
+ return `https://maps.apple.com/directions?${params.join('&')}`;
149
+ }
150
+ /**
151
+ * Splits a long list of stops into legs that each fit one link, every leg starting where the
152
+ * last one ended.
153
+ */
154
+ export function splitIntoLegs(stops, maxPerLeg = GOOGLE_MAX_WAYPOINTS + 2) {
155
+ if (maxPerLeg < 2)
156
+ throw new RangeError('splitIntoLegs: a leg needs at least 2 stops');
157
+ if (stops.length <= maxPerLeg)
158
+ return stops.length ? [[...stops]] : [];
159
+ const legs = [];
160
+ for (let start = 0; start < stops.length - 1; start += maxPerLeg - 1) {
161
+ legs.push(stops.slice(start, start + maxPerLeg));
162
+ }
163
+ return legs;
164
+ }
165
+ /**
166
+ * Google Maps links for a route too long for one: one per leg, each starting where the last one
167
+ * ended. A leg holds an origin, up to nine waypoints and a destination.
168
+ */
169
+ export function googleDirectionsLegUrls(stops, options = {}) {
170
+ const usable = stops.filter((stop) => encodeStop(stop) !== null);
171
+ if (!usable.length)
172
+ return [];
173
+ const { origin, travelMode } = options;
174
+ // With no origin the first link starts wherever the user is, so it has room for one more stop.
175
+ const first = origin === undefined ? usable.slice(0, GOOGLE_MAX_WAYPOINTS + 1) : [];
176
+ const remaining = origin === undefined ? usable.slice(first.length - 1) : [origin, ...usable];
177
+ const legs = remaining.length > 1 ? splitIntoLegs(remaining) : [];
178
+ return [
179
+ ...(first.length ? [googleDirectionsUrl(first, { travelMode })] : []),
180
+ ...legs.map(([start, ...rest]) => googleDirectionsUrl(rest, { origin: start, travelMode })),
181
+ ];
182
+ }
@@ -0,0 +1,9 @@
1
+ import type { FeatureCollection, Geometry, LngLat, Position } from './types.js';
2
+ /** A line with every vertex within `tolerance` of it dropped. Keeps both ends. */
3
+ export declare function simplifyLine<T extends Position>(points: T[], tolerance: number): T[];
4
+ /** A geometry with its lines and rings simplified; points are left alone. */
5
+ export declare function simplifyGeometry(geometry: Geometry, tolerance: number): Geometry;
6
+ /** Every feature simplified; ids and properties are kept. */
7
+ export declare function simplifyFeatures<P>(collection: FeatureCollection<P>, tolerance: number): FeatureCollection<P>;
8
+ /** The smallest convex polygon holding every point, counter-clockwise and closed. */
9
+ export declare function convexHull(points: LngLat[]): LngLat[];
@@ -0,0 +1,102 @@
1
+ /*
2
+ * Fewer vertices for the same shape: Douglas-Peucker line simplification and convex hulls.
3
+ * Tolerances are in degrees — see `toleranceForZoom` in `lod.ts` to derive one from a zoom.
4
+ */
5
+ const squaredSegmentDistance = (point, a, b) => {
6
+ let [x, y] = a;
7
+ let [dx, dy] = [b[0] - x, b[1] - y];
8
+ if (dx !== 0 || dy !== 0) {
9
+ const t = ((point[0] - x) * dx + (point[1] - y) * dy) / (dx * dx + dy * dy);
10
+ if (t > 1)
11
+ [x, y] = b;
12
+ else if (t > 0)
13
+ [x, y] = [x + dx * t, y + dy * t];
14
+ }
15
+ [dx, dy] = [point[0] - x, point[1] - y];
16
+ return dx * dx + dy * dy;
17
+ };
18
+ /** A line with every vertex within `tolerance` of it dropped. Keeps both ends. */
19
+ export function simplifyLine(points, tolerance) {
20
+ if (points.length <= 2 || tolerance <= 0)
21
+ return [...points];
22
+ const limit = tolerance * tolerance;
23
+ const keep = new Uint8Array(points.length);
24
+ keep[0] = keep[points.length - 1] = 1;
25
+ const stack = [[0, points.length - 1]];
26
+ while (stack.length) {
27
+ const [first, last] = stack.pop();
28
+ let [furthest, max] = [-1, limit];
29
+ for (let index = first + 1; index < last; index++) {
30
+ const distance = squaredSegmentDistance(points[index], points[first], points[last]);
31
+ if (distance > max)
32
+ [furthest, max] = [index, distance];
33
+ }
34
+ if (furthest !== -1) {
35
+ keep[furthest] = 1;
36
+ stack.push([first, furthest], [furthest, last]);
37
+ }
38
+ }
39
+ return points.filter((_, index) => keep[index]);
40
+ }
41
+ /** A polygon ring, simplified but never below a valid ring's four positions. */
42
+ function simplifyRing(ring, tolerance) {
43
+ const simplified = simplifyLine(ring, tolerance);
44
+ return simplified.length >= 4 ? simplified : ring;
45
+ }
46
+ /** A geometry with its lines and rings simplified; points are left alone. */
47
+ export function simplifyGeometry(geometry, tolerance) {
48
+ switch (geometry.type) {
49
+ case 'LineString':
50
+ return { ...geometry, coordinates: simplifyLine(geometry.coordinates, tolerance) };
51
+ case 'MultiLineString':
52
+ return {
53
+ ...geometry,
54
+ coordinates: geometry.coordinates.map((line) => simplifyLine(line, tolerance)),
55
+ };
56
+ case 'Polygon':
57
+ return {
58
+ ...geometry,
59
+ coordinates: geometry.coordinates.map((ring) => simplifyRing(ring, tolerance)),
60
+ };
61
+ case 'MultiPolygon':
62
+ return {
63
+ ...geometry,
64
+ coordinates: geometry.coordinates.map((polygon) => polygon.map((ring) => simplifyRing(ring, tolerance))),
65
+ };
66
+ case 'GeometryCollection':
67
+ return {
68
+ ...geometry,
69
+ geometries: geometry.geometries.map((part) => simplifyGeometry(part, tolerance)),
70
+ };
71
+ default:
72
+ return geometry;
73
+ }
74
+ }
75
+ /** Every feature simplified; ids and properties are kept. */
76
+ export function simplifyFeatures(collection, tolerance) {
77
+ return {
78
+ ...collection,
79
+ features: collection.features.map((feature) => feature.geometry
80
+ ? { ...feature, geometry: simplifyGeometry(feature.geometry, tolerance) }
81
+ : feature),
82
+ };
83
+ }
84
+ /** The smallest convex polygon holding every point, counter-clockwise and closed. */
85
+ export function convexHull(points) {
86
+ const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
87
+ const unique = sorted.filter((point, index) => index === 0 || point[0] !== sorted[index - 1][0] || point[1] !== sorted[index - 1][1]);
88
+ if (unique.length < 3)
89
+ return unique;
90
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
91
+ const half = (list) => {
92
+ const hull = [];
93
+ for (const point of list) {
94
+ while (hull.length >= 2 && cross(hull[hull.length - 2], hull[hull.length - 1], point) <= 0)
95
+ hull.pop();
96
+ hull.push(point);
97
+ }
98
+ return hull.slice(0, -1);
99
+ };
100
+ const hull = [...half(unique), ...half([...unique].reverse())];
101
+ return [...hull, hull[0]];
102
+ }
@@ -0,0 +1,21 @@
1
+ import type { Feature, FeatureCollection, LngLat, LngLatBounds } from './types.js';
2
+ export interface SpatialIndex<P = Record<string, unknown>> {
3
+ readonly size: number;
4
+ /** Replaces everything in the index. Bulk loading is much faster than inserting one by one. */
5
+ load(features: Array<Feature<P>> | FeatureCollection<P>): void;
6
+ insert(feature: Feature<P>): void;
7
+ remove(id: string | number): boolean;
8
+ clear(): void;
9
+ /** Features whose bounding box overlaps `bounds`, up to `limit`. */
10
+ search(bounds: LngLatBounds, options?: {
11
+ limit?: number;
12
+ }): Array<Feature<P>>;
13
+ /** The `count` features whose boxes are nearest `point`, nearest first. */
14
+ nearest(point: LngLat, count?: number): Array<Feature<P>>;
15
+ }
16
+ export interface SpatialIndexOptions<P> {
17
+ /** A stable id for a feature. Defaults to `feature.id`, then its position in the input. */
18
+ getId?: (feature: Feature<P>, index: number) => string | number;
19
+ }
20
+ /** A spatial index over features. Features with no geometry are skipped. */
21
+ export declare function createSpatialIndex<P = Record<string, unknown>>(options?: SpatialIndexOptions<P>): SpatialIndex<P>;
@@ -0,0 +1,88 @@
1
+ import RBush from 'rbush';
2
+ import { boundsOf } from './bounds.js';
3
+ /** A spatial index over features. Features with no geometry are skipped. */
4
+ export function createSpatialIndex(options = {}) {
5
+ const tree = new RBush();
6
+ const byId = new Map();
7
+ let counter = 0;
8
+ const idOf = (feature) => options.getId?.(feature, counter) ?? feature.id ?? `feature-${counter}`;
9
+ const entry = (feature) => {
10
+ const box = boundsOf(feature);
11
+ const id = idOf(feature);
12
+ counter += 1;
13
+ if (!box)
14
+ return null;
15
+ const [[minX, minY], [maxX, maxY]] = box;
16
+ return { minX, minY, maxX, maxY, id, feature: feature };
17
+ };
18
+ const distanceTo = ([x, y], item) => {
19
+ const dx = Math.max(item.minX - x, 0, x - item.maxX);
20
+ const dy = Math.max(item.minY - y, 0, y - item.maxY);
21
+ return dx * dx + dy * dy;
22
+ };
23
+ return {
24
+ get size() {
25
+ return byId.size;
26
+ },
27
+ load(input) {
28
+ tree.clear();
29
+ byId.clear();
30
+ counter = 0;
31
+ const features = Array.isArray(input) ? input : input.features;
32
+ const entries = features.map(entry).filter((item) => item !== null);
33
+ for (const item of entries)
34
+ byId.set(item.id, item);
35
+ tree.load(entries);
36
+ },
37
+ insert(feature) {
38
+ const item = entry(feature);
39
+ if (!item)
40
+ return;
41
+ const existing = byId.get(item.id);
42
+ if (existing)
43
+ tree.remove(existing);
44
+ byId.set(item.id, item);
45
+ tree.insert(item);
46
+ },
47
+ remove(id) {
48
+ const existing = byId.get(id);
49
+ if (!existing)
50
+ return false;
51
+ tree.remove(existing);
52
+ byId.delete(id);
53
+ return true;
54
+ },
55
+ clear() {
56
+ tree.clear();
57
+ byId.clear();
58
+ counter = 0;
59
+ },
60
+ search([[minX, minY], [maxX, maxY]], { limit = Infinity } = {}) {
61
+ return tree
62
+ .search({ minX, minY, maxX, maxY })
63
+ .slice(0, limit)
64
+ .map((item) => item.feature);
65
+ },
66
+ nearest(point, count = 1) {
67
+ // Widen the search box until it holds enough candidates, then rank them exactly.
68
+ const all = byId.size;
69
+ if (!all)
70
+ return [];
71
+ let radius = 0.01;
72
+ let found = [];
73
+ while (found.length < Math.min(count, all) && radius < 720) {
74
+ found = tree.search({
75
+ minX: point[0] - radius,
76
+ minY: point[1] - radius,
77
+ maxX: point[0] + radius,
78
+ maxY: point[1] + radius,
79
+ });
80
+ radius *= 4;
81
+ }
82
+ return found
83
+ .sort((a, b) => distanceTo(point, a) - distanceTo(point, b))
84
+ .slice(0, count)
85
+ .map((item) => item.feature);
86
+ },
87
+ };
88
+ }
@@ -0,0 +1,43 @@
1
+ import { type MapColors } from '@loidolt/theme-tokens';
2
+ import type { StyleSpecification } from './types.js';
3
+ /** OpenFreeMap's planet tiles: free, keyless, OpenMapTiles schema. */
4
+ export declare const OPENFREEMAP_TILES = "https://tiles.openfreemap.org/planet";
5
+ /** OpenFreeMap's glyph server. It serves the Noto Sans family. */
6
+ export declare const OPENFREEMAP_GLYPHS = "https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf";
7
+ /** Attribution OpenFreeMap requires. Keep it visible: the attribution control shows it. */
8
+ export declare const OPENFREEMAP_ATTRIBUTION = "<a href=\"https://openfreemap.org\" target=\"_blank\" rel=\"noopener\">OpenFreeMap</a> <a href=\"https://www.openmaptiles.org/\" target=\"_blank\" rel=\"noopener\">\u00A9 OpenMapTiles</a> Data from <a href=\"https://www.openstreetmap.org/copyright\" target=\"_blank\" rel=\"noopener\">OpenStreetMap</a>";
9
+ /** Every layer this generator makes starts with this, so it can be found and repainted. */
10
+ export declare const BASEMAP_PREFIX = "ldt-base-";
11
+ type Paint = Record<string, unknown>;
12
+ /**
13
+ * The colour of every basemap layer, by layer id. The style is built from this, and a theme
14
+ * change repaints from it without reloading the style — so data layers on top survive.
15
+ */
16
+ export declare function basemapPaint(colors?: MapColors): Record<string, Paint>;
17
+ export interface BasemapStyleOptions {
18
+ colors?: MapColors;
19
+ /** A TileJSON URL, or a tile URL template with `{z}/{x}/{y}`. OpenMapTiles schema. */
20
+ tiles?: string;
21
+ /** Glyph URL template. The labels need it. */
22
+ glyphs?: string;
23
+ attribution?: string;
24
+ /** Place, road and water names. */
25
+ labels?: boolean;
26
+ /** Font stack for labels — names the glyph server knows. */
27
+ font?: string[];
28
+ boldFont?: string[];
29
+ }
30
+ /**
31
+ * A quiet basemap in the loidolt palette, drawn from any OpenMapTiles-schema vector tiles —
32
+ * OpenFreeMap's by default. Land, water, parks, buildings, roads, rail, boundaries and names;
33
+ * nothing else, so the data drawn on top carries the colour.
34
+ */
35
+ export declare function createBasemapStyle({ colors, tiles, glyphs, attribution, labels, font, boldFont, }?: BasemapStyleOptions): StyleSpecification;
36
+ /**
37
+ * A tile-free map: the land colour and nothing else. For data that stands on its own, for
38
+ * places with no network, and for tests. `glyphs` is kept so symbol layers can still label.
39
+ */
40
+ export declare function createBlankStyle({ colors, glyphs, }?: Pick<BasemapStyleOptions, 'colors' | 'glyphs'>): StyleSpecification;
41
+ /** Whether a style was made here, and so can be repainted in place on a theme change. */
42
+ export declare const isLoidoltStyle: (style: unknown) => style is StyleSpecification;
43
+ export {};
@@ -0,0 +1,205 @@
1
+ import { mapColors } from '@loidolt/theme-tokens';
2
+ /** OpenFreeMap's planet tiles: free, keyless, OpenMapTiles schema. */
3
+ export const OPENFREEMAP_TILES = 'https://tiles.openfreemap.org/planet';
4
+ /** OpenFreeMap's glyph server. It serves the Noto Sans family. */
5
+ export const OPENFREEMAP_GLYPHS = 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf';
6
+ /** Attribution OpenFreeMap requires. Keep it visible: the attribution control shows it. */
7
+ export const OPENFREEMAP_ATTRIBUTION = '<a href="https://openfreemap.org" target="_blank" rel="noopener">OpenFreeMap</a> <a href="https://www.openmaptiles.org/" target="_blank" rel="noopener">© OpenMapTiles</a> Data from <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a>';
8
+ /** Every layer this generator makes starts with this, so it can be found and repainted. */
9
+ export const BASEMAP_PREFIX = 'ldt-base-';
10
+ const SOURCE = 'ldt-base';
11
+ /** Line width that grows with zoom: `[zoom, width]` stops, interpolated exponentially. */
12
+ const widths = (...stops) => [
13
+ 'interpolate',
14
+ ['exponential', 1.4],
15
+ ['zoom'],
16
+ ...stops.flat(),
17
+ ];
18
+ const MAJOR = ['motorway', 'trunk', 'primary'];
19
+ const MEDIUM = ['secondary', 'tertiary'];
20
+ const MINOR = ['minor', 'service', 'track'];
21
+ /**
22
+ * The colour of every basemap layer, by layer id. The style is built from this, and a theme
23
+ * change repaints from it without reloading the style — so data layers on top survive.
24
+ */
25
+ export function basemapPaint(colors = mapColors()) {
26
+ const id = (name) => `${BASEMAP_PREFIX}${name}`;
27
+ const label = {
28
+ 'text-color': colors.label,
29
+ 'text-halo-color': colors.labelHalo,
30
+ 'text-halo-width': 1.4,
31
+ };
32
+ return {
33
+ [id('land')]: { 'background-color': colors.land },
34
+ [id('park')]: { 'fill-color': colors.park },
35
+ [id('wood')]: { 'fill-color': colors.park, 'fill-opacity': 0.7 },
36
+ [id('water')]: { 'fill-color': colors.water },
37
+ [id('waterway')]: { 'line-color': colors.water, 'line-width': widths([8, 0.5], [16, 3]) },
38
+ [id('building')]: { 'fill-color': colors.building, 'fill-opacity': 0.8 },
39
+ [id('road-minor')]: { 'line-color': colors.road, 'line-width': widths([12, 0.5], [18, 10]) },
40
+ [id('road-medium-casing')]: {
41
+ 'line-color': colors.building,
42
+ 'line-width': widths([8, 0.8], [18, 16]),
43
+ },
44
+ [id('road-medium')]: { 'line-color': colors.road, 'line-width': widths([8, 0.5], [18, 13]) },
45
+ [id('road-major-casing')]: {
46
+ 'line-color': colors.building,
47
+ 'line-width': widths([5, 1], [18, 22]),
48
+ },
49
+ [id('road-major')]: {
50
+ 'line-color': colors.roadMajor,
51
+ 'line-width': widths([5, 0.6], [18, 18]),
52
+ },
53
+ [id('rail')]: {
54
+ 'line-color': colors.boundary,
55
+ 'line-width': 1,
56
+ 'line-dasharray': [3, 3],
57
+ 'line-opacity': 0.6,
58
+ },
59
+ [id('boundary-country')]: {
60
+ 'line-color': colors.boundary,
61
+ 'line-width': widths([2, 0.8], [10, 2]),
62
+ },
63
+ [id('boundary-state')]: {
64
+ 'line-color': colors.boundary,
65
+ 'line-width': 1,
66
+ 'line-dasharray': [4, 3],
67
+ 'line-opacity': 0.7,
68
+ },
69
+ [id('label-water')]: label,
70
+ [id('label-road')]: label,
71
+ [id('label-place-minor')]: label,
72
+ [id('label-place-major')]: label,
73
+ [id('label-country')]: label,
74
+ };
75
+ }
76
+ /**
77
+ * A quiet basemap in the loidolt palette, drawn from any OpenMapTiles-schema vector tiles —
78
+ * OpenFreeMap's by default. Land, water, parks, buildings, roads, rail, boundaries and names;
79
+ * nothing else, so the data drawn on top carries the colour.
80
+ */
81
+ export function createBasemapStyle({ colors = mapColors(), tiles = OPENFREEMAP_TILES, glyphs = OPENFREEMAP_GLYPHS, attribution = OPENFREEMAP_ATTRIBUTION, labels = true, font = ['Noto Sans Regular'], boldFont = ['Noto Sans Bold'], } = {}) {
82
+ const paint = basemapPaint(colors);
83
+ const layer = (name, type, sourceLayer, extra = {}) => ({
84
+ id: `${BASEMAP_PREFIX}${name}`,
85
+ type,
86
+ ...(sourceLayer ? { source: SOURCE, 'source-layer': sourceLayer } : {}),
87
+ paint: paint[`${BASEMAP_PREFIX}${name}`],
88
+ ...extra,
89
+ });
90
+ const road = (classes) => ['match', ['get', 'class'], classes, true, false];
91
+ const lineLayout = { 'line-cap': 'round', 'line-join': 'round' };
92
+ const text = (size, fontStack = font) => ({
93
+ 'text-font': fontStack,
94
+ 'text-size': size,
95
+ 'text-max-width': 8,
96
+ });
97
+ const layers = [
98
+ layer('land', 'background', null),
99
+ layer('park', 'fill', 'park'),
100
+ layer('wood', 'fill', 'landcover', { filter: road(['wood', 'grass']) }),
101
+ layer('water', 'fill', 'water'),
102
+ layer('waterway', 'line', 'waterway', { minzoom: 8 }),
103
+ layer('building', 'fill', 'building', { minzoom: 13 }),
104
+ layer('road-minor', 'line', 'transportation', {
105
+ minzoom: 12,
106
+ filter: road(MINOR),
107
+ layout: lineLayout,
108
+ }),
109
+ layer('road-medium-casing', 'line', 'transportation', {
110
+ minzoom: 8,
111
+ filter: road(MEDIUM),
112
+ layout: lineLayout,
113
+ }),
114
+ layer('road-medium', 'line', 'transportation', {
115
+ minzoom: 8,
116
+ filter: road(MEDIUM),
117
+ layout: lineLayout,
118
+ }),
119
+ layer('road-major-casing', 'line', 'transportation', {
120
+ minzoom: 5,
121
+ filter: road(MAJOR),
122
+ layout: lineLayout,
123
+ }),
124
+ layer('road-major', 'line', 'transportation', {
125
+ minzoom: 5,
126
+ filter: road(MAJOR),
127
+ layout: lineLayout,
128
+ }),
129
+ layer('rail', 'line', 'transportation', { minzoom: 10, filter: road(['rail', 'transit']) }),
130
+ layer('boundary-country', 'line', 'boundary', {
131
+ filter: ['all', ['==', ['get', 'admin_level'], 2], ['!=', ['get', 'maritime'], 1]],
132
+ }),
133
+ layer('boundary-state', 'line', 'boundary', {
134
+ minzoom: 3,
135
+ filter: ['all', ['==', ['get', 'admin_level'], 4], ['!=', ['get', 'maritime'], 1]],
136
+ }),
137
+ ];
138
+ if (labels) {
139
+ layers.push(layer('label-water', 'symbol', 'water_name', {
140
+ layout: { 'text-field': ['get', 'name'], ...text(12, ['Noto Sans Italic']) },
141
+ }), layer('label-road', 'symbol', 'transportation_name', {
142
+ minzoom: 13,
143
+ layout: {
144
+ 'text-field': ['get', 'name'],
145
+ 'symbol-placement': 'line',
146
+ ...text(11),
147
+ },
148
+ }), layer('label-place-minor', 'symbol', 'place', {
149
+ minzoom: 11,
150
+ filter: road(['village', 'suburb', 'neighbourhood', 'quarter', 'hamlet']),
151
+ layout: { 'text-field': ['get', 'name'], ...text(12) },
152
+ }), layer('label-place-major', 'symbol', 'place', {
153
+ minzoom: 4,
154
+ filter: road(['city', 'town']),
155
+ layout: {
156
+ 'text-field': ['get', 'name'],
157
+ ...text(['interpolate', ['linear'], ['zoom'], 4, 11, 12, 18], boldFont),
158
+ },
159
+ }), layer('label-country', 'symbol', 'place', {
160
+ maxzoom: 7,
161
+ filter: road(['country']),
162
+ layout: {
163
+ 'text-field': ['get', 'name'],
164
+ 'text-transform': 'uppercase',
165
+ 'text-letter-spacing': 0.1,
166
+ ...text(12, boldFont),
167
+ },
168
+ }));
169
+ }
170
+ const source = /\{z\}/.test(tiles)
171
+ ? { type: 'vector', tiles: [tiles], maxzoom: 14, attribution }
172
+ : { type: 'vector', url: tiles, attribution };
173
+ return {
174
+ version: 8,
175
+ name: 'Loidolt',
176
+ metadata: { 'loidolt:basemap': 'default' },
177
+ glyphs,
178
+ sources: { [SOURCE]: source },
179
+ layers,
180
+ };
181
+ }
182
+ /**
183
+ * A tile-free map: the land colour and nothing else. For data that stands on its own, for
184
+ * places with no network, and for tests. `glyphs` is kept so symbol layers can still label.
185
+ */
186
+ export function createBlankStyle({ colors = mapColors(), glyphs = OPENFREEMAP_GLYPHS, } = {}) {
187
+ return {
188
+ version: 8,
189
+ name: 'Loidolt plain',
190
+ metadata: { 'loidolt:basemap': 'blank' },
191
+ glyphs,
192
+ sources: {},
193
+ layers: [
194
+ {
195
+ id: `${BASEMAP_PREFIX}land`,
196
+ type: 'background',
197
+ paint: basemapPaint(colors)[`${BASEMAP_PREFIX}land`],
198
+ },
199
+ ],
200
+ };
201
+ }
202
+ /** Whether a style was made here, and so can be repainted in place on a theme change. */
203
+ export const isLoidoltStyle = (style) => typeof style === 'object' &&
204
+ style !== null &&
205
+ typeof style.metadata?.['loidolt:basemap'] === 'string';