@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.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/components/BasemapSwitcher.svelte +53 -0
- package/dist/components/BasemapSwitcher.svelte.d.ts +20 -0
- package/dist/components/CircleLayer.svelte +22 -0
- package/dist/components/CircleLayer.svelte.d.ts +4 -0
- package/dist/components/CoordinateDisplay.svelte +68 -0
- package/dist/components/CoordinateDisplay.svelte.d.ts +13 -0
- package/dist/components/DeckOverlay.svelte +58 -0
- package/dist/components/DeckOverlay.svelte.d.ts +18 -0
- package/dist/components/FillExtrusionLayer.svelte +17 -0
- package/dist/components/FillExtrusionLayer.svelte.d.ts +4 -0
- package/dist/components/FillLayer.svelte +21 -0
- package/dist/components/FillLayer.svelte.d.ts +4 -0
- package/dist/components/HeatmapLayer.svelte +36 -0
- package/dist/components/HeatmapLayer.svelte.d.ts +4 -0
- package/dist/components/LayerManager.svelte +234 -0
- package/dist/components/LayerManager.svelte.d.ts +24 -0
- package/dist/components/LineLayer.svelte +18 -0
- package/dist/components/LineLayer.svelte.d.ts +4 -0
- package/dist/components/MapControl.svelte +47 -0
- package/dist/components/MapControl.svelte.d.ts +13 -0
- package/dist/components/MapFeatureList.svelte +67 -0
- package/dist/components/MapFeatureList.svelte.d.ts +20 -0
- package/dist/components/MapLegend.svelte +68 -0
- package/dist/components/MapLegend.svelte.d.ts +19 -0
- package/dist/components/MapLegendGroup.svelte +72 -0
- package/dist/components/MapLegendGroup.svelte.d.ts +19 -0
- package/dist/components/MapMarker.svelte +130 -0
- package/dist/components/MapMarker.svelte.d.ts +32 -0
- package/dist/components/MapPopup.svelte +123 -0
- package/dist/components/MapPopup.svelte.d.ts +21 -0
- package/dist/components/MapSource.svelte +109 -0
- package/dist/components/MapSource.svelte.d.ts +31 -0
- package/dist/components/MapView.svelte +454 -0
- package/dist/components/MapView.svelte.d.ts +76 -0
- package/dist/components/RasterLayer.svelte +14 -0
- package/dist/components/RasterLayer.svelte.d.ts +4 -0
- package/dist/components/SymbolLayer.svelte +22 -0
- package/dist/components/SymbolLayer.svelte.d.ts +4 -0
- package/dist/core/accessibility.d.ts +24 -0
- package/dist/core/accessibility.js +33 -0
- package/dist/core/bounds.d.ts +18 -0
- package/dist/core/bounds.js +91 -0
- package/dist/core/cluster.d.ts +35 -0
- package/dist/core/cluster.js +44 -0
- package/dist/core/data.d.ts +33 -0
- package/dist/core/data.js +157 -0
- package/dist/core/equality.d.ts +7 -0
- package/dist/core/equality.js +33 -0
- package/dist/core/format.d.ts +20 -0
- package/dist/core/format.js +47 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.js +18 -0
- package/dist/core/lod.d.ts +27 -0
- package/dist/core/lod.js +49 -0
- package/dist/core/route.d.ts +58 -0
- package/dist/core/route.js +182 -0
- package/dist/core/simplify.d.ts +9 -0
- package/dist/core/simplify.js +102 -0
- package/dist/core/spatial.d.ts +21 -0
- package/dist/core/spatial.js +88 -0
- package/dist/core/style.d.ts +43 -0
- package/dist/core/style.js +205 -0
- package/dist/core/types.d.ts +110 -0
- package/dist/core/types.js +5 -0
- package/dist/core/workers/client.d.ts +26 -0
- package/dist/core/workers/client.js +88 -0
- package/dist/core/workers/handler.d.ts +6 -0
- package/dist/core/workers/handler.js +53 -0
- package/dist/core/workers/protocol.d.ts +32 -0
- package/dist/core/workers/protocol.js +1 -0
- package/dist/core/workers/spatial.worker.d.ts +1 -0
- package/dist/core/workers/spatial.worker.js +12 -0
- package/dist/deck.d.ts +18 -0
- package/dist/deck.js +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/internal/context.d.ts +39 -0
- package/dist/internal/context.js +15 -0
- package/dist/internal/safely.d.ts +5 -0
- package/dist/internal/safely.js +12 -0
- package/dist/internal/use-layer.svelte.d.ts +45 -0
- package/dist/internal/use-layer.svelte.js +179 -0
- package/dist/maplibre.d.ts +19 -0
- package/dist/maplibre.js +47 -0
- package/dist/styles.css +10 -0
- package/package.json +76 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Whether a pair is a real longitude/latitude: finite, within ±180 and ±90. */
|
|
2
|
+
export function isLngLat(value) {
|
|
3
|
+
return (Array.isArray(value) &&
|
|
4
|
+
value.length >= 2 &&
|
|
5
|
+
Number.isFinite(value[0]) &&
|
|
6
|
+
Number.isFinite(value[1]) &&
|
|
7
|
+
Math.abs(value[0]) <= 180 &&
|
|
8
|
+
Math.abs(value[1]) <= 90);
|
|
9
|
+
}
|
|
10
|
+
function* positions(geometry) {
|
|
11
|
+
if (!geometry)
|
|
12
|
+
return;
|
|
13
|
+
switch (geometry.type) {
|
|
14
|
+
case 'Point':
|
|
15
|
+
yield geometry.coordinates;
|
|
16
|
+
break;
|
|
17
|
+
case 'MultiPoint':
|
|
18
|
+
case 'LineString':
|
|
19
|
+
yield* geometry.coordinates;
|
|
20
|
+
break;
|
|
21
|
+
case 'MultiLineString':
|
|
22
|
+
case 'Polygon':
|
|
23
|
+
for (const ring of geometry.coordinates)
|
|
24
|
+
yield* ring;
|
|
25
|
+
break;
|
|
26
|
+
case 'MultiPolygon':
|
|
27
|
+
for (const polygon of geometry.coordinates)
|
|
28
|
+
for (const ring of polygon)
|
|
29
|
+
yield* ring;
|
|
30
|
+
break;
|
|
31
|
+
case 'GeometryCollection':
|
|
32
|
+
for (const part of geometry.geometries)
|
|
33
|
+
yield* positions(part);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Every coordinate in any GeoJSON object. */
|
|
38
|
+
export function* coordinatesOf(data) {
|
|
39
|
+
if (data.type === 'FeatureCollection') {
|
|
40
|
+
for (const feature of data.features)
|
|
41
|
+
yield* positions(feature.geometry);
|
|
42
|
+
}
|
|
43
|
+
else if (data.type === 'Feature') {
|
|
44
|
+
yield* positions(data.geometry);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
yield* positions(data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The box around GeoJSON or a list of points, or `null` when there is nothing to box. Does
|
|
52
|
+
* not handle data that crosses the antimeridian.
|
|
53
|
+
*/
|
|
54
|
+
export function boundsOf(data) {
|
|
55
|
+
let [west, south, east, north] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
56
|
+
const points = Array.isArray(data) ? data : coordinatesOf(data);
|
|
57
|
+
for (const [lng, lat] of points) {
|
|
58
|
+
if (!Number.isFinite(lng) || !Number.isFinite(lat))
|
|
59
|
+
continue;
|
|
60
|
+
west = Math.min(west, lng);
|
|
61
|
+
east = Math.max(east, lng);
|
|
62
|
+
south = Math.min(south, lat);
|
|
63
|
+
north = Math.max(north, lat);
|
|
64
|
+
}
|
|
65
|
+
return Number.isFinite(west)
|
|
66
|
+
? [
|
|
67
|
+
[west, south],
|
|
68
|
+
[east, north],
|
|
69
|
+
]
|
|
70
|
+
: null;
|
|
71
|
+
}
|
|
72
|
+
/** The middle of a box. */
|
|
73
|
+
export function boundsCenter([[west, south], [east, north]]) {
|
|
74
|
+
return [(west + east) / 2, (south + north) / 2];
|
|
75
|
+
}
|
|
76
|
+
/** Grows a box by a fraction of its size on every side (`0.1` adds 10%). */
|
|
77
|
+
export function padBounds([[west, south], [east, north]], ratio) {
|
|
78
|
+
const [dx, dy] = [(east - west) * ratio, (north - south) * ratio];
|
|
79
|
+
return [
|
|
80
|
+
[Math.max(-180, west - dx), Math.max(-90, south - dy)],
|
|
81
|
+
[Math.min(180, east + dx), Math.min(90, north + dy)],
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
/** Whether a point lies in a box, edges included. */
|
|
85
|
+
export function boundsContain([[west, south], [east, north]], [lng, lat]) {
|
|
86
|
+
return lng >= west && lng <= east && lat >= south && lat <= north;
|
|
87
|
+
}
|
|
88
|
+
/** Whether two boxes overlap. */
|
|
89
|
+
export function boundsIntersect(a, b) {
|
|
90
|
+
return a[0][0] <= b[1][0] && a[1][0] >= b[0][0] && a[0][1] <= b[1][1] && a[1][1] >= b[0][1];
|
|
91
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type MapColors } from '@loidolt/theme-tokens';
|
|
2
|
+
import type { LngLat } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Cluster colour by size, stepping through the sequential ramp at the given point counts: the
|
|
5
|
+
* more points, the further along the ramp.
|
|
6
|
+
*/
|
|
7
|
+
export declare function clusterColor(colors?: Pick<MapColors, 'sequential'>, steps?: number[]): unknown[];
|
|
8
|
+
/** Cluster radius by size, in pixels, at the same point counts. */
|
|
9
|
+
export declare function clusterRadius(steps?: number[], radii?: number[]): unknown[];
|
|
10
|
+
/** Whether a rendered feature is a cluster rather than one point. */
|
|
11
|
+
export declare const isCluster: (feature: {
|
|
12
|
+
properties?: Record<string, unknown> | null;
|
|
13
|
+
}) => boolean;
|
|
14
|
+
interface ClusterMap {
|
|
15
|
+
getSource(id: string): unknown;
|
|
16
|
+
easeTo(options: {
|
|
17
|
+
center: LngLat;
|
|
18
|
+
zoom: number;
|
|
19
|
+
duration?: number;
|
|
20
|
+
}): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Zooms to where a clicked cluster splits apart. `animate: false` for reduced motion.
|
|
24
|
+
* Resolves `false` when the feature is not a cluster or its source cannot expand it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function expandCluster(map: ClusterMap, sourceId: string, feature: {
|
|
27
|
+
properties?: Record<string, unknown> | null;
|
|
28
|
+
geometry?: {
|
|
29
|
+
type: string;
|
|
30
|
+
coordinates?: unknown;
|
|
31
|
+
} | null;
|
|
32
|
+
}, { animate }?: {
|
|
33
|
+
animate?: boolean;
|
|
34
|
+
}): Promise<boolean>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mapColors } from '@loidolt/theme-tokens';
|
|
2
|
+
/*
|
|
3
|
+
* Clustering is a GeoJSON source option (`cluster` on `MapSource`). These build the paint for
|
|
4
|
+
* the cluster circles and handle the one interaction every cluster needs: click to zoom in.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Cluster colour by size, stepping through the sequential ramp at the given point counts: the
|
|
8
|
+
* more points, the further along the ramp.
|
|
9
|
+
*/
|
|
10
|
+
export function clusterColor(colors = mapColors(), steps = [10, 50, 200]) {
|
|
11
|
+
const ramp = colors.sequential.slice(-(steps.length + 1));
|
|
12
|
+
return [
|
|
13
|
+
'step',
|
|
14
|
+
['get', 'point_count'],
|
|
15
|
+
ramp[0],
|
|
16
|
+
...steps.flatMap((step, index) => [step, ramp[index + 1] ?? ramp.at(-1)]),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
/** Cluster radius by size, in pixels, at the same point counts. */
|
|
20
|
+
export function clusterRadius(steps = [10, 50, 200], radii = [14, 18, 24, 30]) {
|
|
21
|
+
return [
|
|
22
|
+
'step',
|
|
23
|
+
['get', 'point_count'],
|
|
24
|
+
radii[0],
|
|
25
|
+
...steps.flatMap((step, index) => [step, radii[index + 1] ?? radii.at(-1)]),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Whether a rendered feature is a cluster rather than one point. */
|
|
29
|
+
export const isCluster = (feature) => feature.properties?.cluster === true && typeof feature.properties.cluster_id === 'number';
|
|
30
|
+
/**
|
|
31
|
+
* Zooms to where a clicked cluster splits apart. `animate: false` for reduced motion.
|
|
32
|
+
* Resolves `false` when the feature is not a cluster or its source cannot expand it.
|
|
33
|
+
*/
|
|
34
|
+
export async function expandCluster(map, sourceId, feature, { animate = true } = {}) {
|
|
35
|
+
const source = map.getSource(sourceId);
|
|
36
|
+
if (!isCluster(feature) || typeof source?.getClusterExpansionZoom !== 'function')
|
|
37
|
+
return false;
|
|
38
|
+
if (feature.geometry?.type !== 'Point')
|
|
39
|
+
return false;
|
|
40
|
+
const zoom = await source.getClusterExpansionZoom(feature.properties.cluster_id);
|
|
41
|
+
const [lng, lat] = feature.geometry.coordinates;
|
|
42
|
+
map.easeTo({ center: [lng, lat], zoom, duration: animate ? undefined : 0 });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection, GeoJSON, LngLat } from './types.js';
|
|
2
|
+
export interface GeoJSONCheck {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
/** What is wrong, one message per problem, with a path such as `features[3].geometry`. */
|
|
5
|
+
errors: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Checks that a value is GeoJSON a map can draw: known types, and positions that are real
|
|
9
|
+
* `[longitude, latitude]` pairs. The most common mistake it catches is `[lat, lng]` order,
|
|
10
|
+
* which puts a latitude above 90 in the longitude slot.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validateGeoJSON(value: unknown): GeoJSONCheck;
|
|
13
|
+
export interface LoadOptions {
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
/** Give up after this many milliseconds. */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
/** For tests and custom transports. Defaults to the global `fetch`. */
|
|
18
|
+
fetch?: typeof fetch;
|
|
19
|
+
}
|
|
20
|
+
/** Fetches and validates GeoJSON. Rejects with a readable message when either fails. */
|
|
21
|
+
export declare function loadGeoJSON(url: string, options?: LoadOptions): Promise<GeoJSON>;
|
|
22
|
+
/** Points as a FeatureCollection, each keeping its own properties. */
|
|
23
|
+
export declare function pointsToFeatureCollection<P extends Record<string, unknown>>(points: Array<{
|
|
24
|
+
lngLat: LngLat;
|
|
25
|
+
id?: string | number;
|
|
26
|
+
properties?: P;
|
|
27
|
+
}>): FeatureCollection<P>;
|
|
28
|
+
/**
|
|
29
|
+
* A representative point for a feature: the point itself, a line's middle vertex, or the
|
|
30
|
+
* average of a shape's outer ring. Good enough to fly to or to pin a label on; not a true
|
|
31
|
+
* centroid for concave shapes.
|
|
32
|
+
*/
|
|
33
|
+
export declare function featureAnchor(feature: Feature): LngLat | null;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { isLngLat } from './bounds.js';
|
|
2
|
+
const GEOMETRY_TYPES = new Set([
|
|
3
|
+
'Point',
|
|
4
|
+
'MultiPoint',
|
|
5
|
+
'LineString',
|
|
6
|
+
'MultiLineString',
|
|
7
|
+
'Polygon',
|
|
8
|
+
'MultiPolygon',
|
|
9
|
+
'GeometryCollection',
|
|
10
|
+
]);
|
|
11
|
+
function checkGeometry(value, path, errors) {
|
|
12
|
+
if (value === null)
|
|
13
|
+
return;
|
|
14
|
+
const geometry = value;
|
|
15
|
+
if (typeof value !== 'object' || !GEOMETRY_TYPES.has(geometry.type)) {
|
|
16
|
+
errors.push(`${path}: not a GeoJSON geometry`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (geometry.type === 'GeometryCollection') {
|
|
20
|
+
if (!Array.isArray(geometry.geometries))
|
|
21
|
+
errors.push(`${path}.geometries: must be an array`);
|
|
22
|
+
else
|
|
23
|
+
geometry.geometries.forEach((part, index) => checkGeometry(part, `${path}.geometries[${index}]`, errors));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const depth = {
|
|
27
|
+
Point: 0,
|
|
28
|
+
MultiPoint: 1,
|
|
29
|
+
LineString: 1,
|
|
30
|
+
MultiLineString: 2,
|
|
31
|
+
Polygon: 2,
|
|
32
|
+
MultiPolygon: 3,
|
|
33
|
+
}[geometry.type];
|
|
34
|
+
const walk = (node, level, where) => {
|
|
35
|
+
if (level === 0) {
|
|
36
|
+
if (!isLngLat(node))
|
|
37
|
+
errors.push(`${where}: not a [longitude, latitude] position`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(node)) {
|
|
41
|
+
errors.push(`${where}: must be an array`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
node.forEach((child, index) => walk(child, level - 1, `${where}[${index}]`));
|
|
45
|
+
};
|
|
46
|
+
walk(geometry.coordinates, depth, `${path}.coordinates`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Checks that a value is GeoJSON a map can draw: known types, and positions that are real
|
|
50
|
+
* `[longitude, latitude]` pairs. The most common mistake it catches is `[lat, lng]` order,
|
|
51
|
+
* which puts a latitude above 90 in the longitude slot.
|
|
52
|
+
*/
|
|
53
|
+
export function validateGeoJSON(value) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
const data = value;
|
|
56
|
+
if (typeof value !== 'object' || value === null) {
|
|
57
|
+
errors.push('not an object');
|
|
58
|
+
}
|
|
59
|
+
else if (data.type === 'FeatureCollection') {
|
|
60
|
+
if (!Array.isArray(data.features))
|
|
61
|
+
errors.push('features: must be an array');
|
|
62
|
+
else
|
|
63
|
+
data.features.forEach((feature, index) => {
|
|
64
|
+
if (feature?.type !== 'Feature')
|
|
65
|
+
errors.push(`features[${index}]: not a Feature`);
|
|
66
|
+
else
|
|
67
|
+
checkGeometry(feature.geometry, `features[${index}].geometry`, errors);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else if (data.type === 'Feature') {
|
|
71
|
+
checkGeometry(data.geometry, 'geometry', errors);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
checkGeometry(value, 'geometry', errors);
|
|
75
|
+
}
|
|
76
|
+
return { valid: errors.length === 0, errors };
|
|
77
|
+
}
|
|
78
|
+
/** Fetches and validates GeoJSON. Rejects with a readable message when either fails. */
|
|
79
|
+
export async function loadGeoJSON(url, options = {}) {
|
|
80
|
+
const { timeout = 30_000, fetch: request = globalThis.fetch } = options;
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
const abort = () => controller.abort();
|
|
83
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
84
|
+
const timer = setTimeout(abort, timeout);
|
|
85
|
+
try {
|
|
86
|
+
const response = await request(url, { signal: controller.signal });
|
|
87
|
+
if (!response.ok)
|
|
88
|
+
throw new Error(`loadGeoJSON: ${url} answered ${response.status}`);
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
const check = validateGeoJSON(data);
|
|
91
|
+
if (!check.valid)
|
|
92
|
+
throw new Error(`loadGeoJSON: ${url} is not valid GeoJSON (${check.errors[0]})`);
|
|
93
|
+
return data;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (controller.signal.aborted && !options.signal?.aborted) {
|
|
97
|
+
throw new Error(`loadGeoJSON: ${url} took longer than ${timeout} ms`);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
options.signal?.removeEventListener('abort', abort);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Points as a FeatureCollection, each keeping its own properties. */
|
|
107
|
+
export function pointsToFeatureCollection(points) {
|
|
108
|
+
return {
|
|
109
|
+
type: 'FeatureCollection',
|
|
110
|
+
features: points.map(({ lngLat, id, properties }) => ({
|
|
111
|
+
type: 'Feature',
|
|
112
|
+
...(id === undefined ? {} : { id }),
|
|
113
|
+
geometry: { type: 'Point', coordinates: lngLat },
|
|
114
|
+
properties: properties ?? {},
|
|
115
|
+
})),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* A representative point for a feature: the point itself, a line's middle vertex, or the
|
|
120
|
+
* average of a shape's outer ring. Good enough to fly to or to pin a label on; not a true
|
|
121
|
+
* centroid for concave shapes.
|
|
122
|
+
*/
|
|
123
|
+
export function featureAnchor(feature) {
|
|
124
|
+
const geometry = feature.geometry;
|
|
125
|
+
if (!geometry)
|
|
126
|
+
return null;
|
|
127
|
+
const average = (ring) => {
|
|
128
|
+
const points = ring.length > 1 && ring[0][0] === ring.at(-1)[0] && ring[0][1] === ring.at(-1)[1]
|
|
129
|
+
? ring.slice(0, -1)
|
|
130
|
+
: ring;
|
|
131
|
+
const [lng, lat] = points.reduce(([x, y], point) => [x + point[0], y + point[1]], [0, 0]);
|
|
132
|
+
return [lng / points.length, lat / points.length];
|
|
133
|
+
};
|
|
134
|
+
switch (geometry.type) {
|
|
135
|
+
case 'Point':
|
|
136
|
+
return [geometry.coordinates[0], geometry.coordinates[1]];
|
|
137
|
+
case 'MultiPoint':
|
|
138
|
+
return average(geometry.coordinates);
|
|
139
|
+
case 'LineString': {
|
|
140
|
+
const middle = geometry.coordinates[Math.floor(geometry.coordinates.length / 2)];
|
|
141
|
+
return middle ? [middle[0], middle[1]] : null;
|
|
142
|
+
}
|
|
143
|
+
case 'MultiLineString':
|
|
144
|
+
return featureAnchor({
|
|
145
|
+
...feature,
|
|
146
|
+
geometry: { type: 'LineString', coordinates: geometry.coordinates[0] ?? [] },
|
|
147
|
+
});
|
|
148
|
+
case 'Polygon':
|
|
149
|
+
return geometry.coordinates[0]?.length ? average(geometry.coordinates[0]) : null;
|
|
150
|
+
case 'MultiPolygon':
|
|
151
|
+
return geometry.coordinates[0]?.[0]?.length ? average(geometry.coordinates[0][0]) : null;
|
|
152
|
+
default:
|
|
153
|
+
return geometry.geometries.length
|
|
154
|
+
? featureAnchor({ ...feature, geometry: geometry.geometries[0] })
|
|
155
|
+
: null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Structural equality for the JSON-shaped values in a style: expressions, colours, numbers. */
|
|
2
|
+
export declare function deepEqual(a: unknown, b: unknown): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* The properties that differ between two property objects, as `[name, next value]`. A
|
|
5
|
+
* property that was removed comes back as `undefined`, which resets it to the style default.
|
|
6
|
+
*/
|
|
7
|
+
export declare function changedProperties(previous: Record<string, unknown> | undefined, next: Record<string, unknown> | undefined): Array<[string, unknown]>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Diffing for layer properties, so a re-render sends MapLibre only what changed. Every
|
|
3
|
+
* `setPaintProperty` call re-validates and repaints; sending the whole paint object on every
|
|
4
|
+
* change makes a busy map stutter.
|
|
5
|
+
*/
|
|
6
|
+
/** Structural equality for the JSON-shaped values in a style: expressions, colours, numbers. */
|
|
7
|
+
export function deepEqual(a, b) {
|
|
8
|
+
if (Object.is(a, b))
|
|
9
|
+
return true;
|
|
10
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null)
|
|
11
|
+
return false;
|
|
12
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
13
|
+
return false;
|
|
14
|
+
if (Array.isArray(a)) {
|
|
15
|
+
const other = b;
|
|
16
|
+
return a.length === other.length && a.every((value, index) => deepEqual(value, other[index]));
|
|
17
|
+
}
|
|
18
|
+
const [left, right] = [a, b];
|
|
19
|
+
const keys = Object.keys(left);
|
|
20
|
+
return (keys.length === Object.keys(right).length &&
|
|
21
|
+
keys.every((key) => Object.hasOwn(right, key) && deepEqual(left[key], right[key])));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The properties that differ between two property objects, as `[name, next value]`. A
|
|
25
|
+
* property that was removed comes back as `undefined`, which resets it to the style default.
|
|
26
|
+
*/
|
|
27
|
+
export function changedProperties(previous, next) {
|
|
28
|
+
const [before, after] = [previous ?? {}, next ?? {}];
|
|
29
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
30
|
+
return [...keys]
|
|
31
|
+
.filter((key) => !deepEqual(before[key], after[key]))
|
|
32
|
+
.map((key) => [key, after[key]]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LngLat } from './types.js';
|
|
2
|
+
export interface CoordinateFormat {
|
|
3
|
+
/** `decimal` → `45.52345° N, 122.67621° W`; `dms` → `45° 31′ 24.4″ N, 122° 40′ 34.4″ W`. */
|
|
4
|
+
format?: 'decimal' | 'dms';
|
|
5
|
+
/** Decimal places for `decimal`, or for the seconds in `dms`. */
|
|
6
|
+
precision?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A coordinate as people read it: latitude first, with hemispheres rather than signs.
|
|
10
|
+
* `[-122.676, 45.523]` → `45.52300° N, 122.67600° W`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatCoordinate([lng, lat]: LngLat, { format, precision }?: CoordinateFormat): string;
|
|
13
|
+
export interface MeasureFormat {
|
|
14
|
+
unit?: 'metric' | 'imperial';
|
|
15
|
+
locale?: string | string[];
|
|
16
|
+
}
|
|
17
|
+
/** `850` → `850 m`, `12400` → `12.4 km`; imperial gives feet and miles. */
|
|
18
|
+
export declare function formatDistance(meters: number, { unit, locale }?: MeasureFormat): string;
|
|
19
|
+
/** `5000` → `5,000 m²`, `2.5e6` → `2.5 km²`; imperial gives acres and square miles. */
|
|
20
|
+
export declare function formatArea(squareMeters: number, { unit, locale }?: MeasureFormat): string;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
function dms(value, precision) {
|
|
2
|
+
const absolute = Math.abs(value);
|
|
3
|
+
let degrees = Math.floor(absolute);
|
|
4
|
+
let minutes = Math.floor((absolute - degrees) * 60);
|
|
5
|
+
let seconds = Number(((absolute - degrees - minutes / 60) * 3600).toFixed(precision));
|
|
6
|
+
// Rounding can carry: 59.96″ at one place is 60.0″, which is the next minute.
|
|
7
|
+
if (seconds >= 60)
|
|
8
|
+
[seconds, minutes] = [0, minutes + 1];
|
|
9
|
+
if (minutes >= 60)
|
|
10
|
+
[minutes, degrees] = [0, degrees + 1];
|
|
11
|
+
return `${degrees}° ${minutes}′ ${seconds.toFixed(precision)}″`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A coordinate as people read it: latitude first, with hemispheres rather than signs.
|
|
15
|
+
* `[-122.676, 45.523]` → `45.52300° N, 122.67600° W`.
|
|
16
|
+
*/
|
|
17
|
+
export function formatCoordinate([lng, lat], { format = 'decimal', precision = format === 'dms' ? 1 : 5 } = {}) {
|
|
18
|
+
const part = (value, positive, negative) => `${format === 'dms' ? dms(value, precision) : `${Math.abs(value).toFixed(precision)}°`} ${value >= 0 ? positive : negative}`;
|
|
19
|
+
return `${part(lat, 'N', 'S')}, ${part(lng, 'E', 'W')}`;
|
|
20
|
+
}
|
|
21
|
+
const number = (value, locale, digits) => new Intl.NumberFormat(locale, { maximumFractionDigits: digits }).format(value);
|
|
22
|
+
/** `850` → `850 m`, `12400` → `12.4 km`; imperial gives feet and miles. */
|
|
23
|
+
export function formatDistance(meters, { unit = 'metric', locale } = {}) {
|
|
24
|
+
if (unit === 'imperial') {
|
|
25
|
+
const feet = meters * 3.28084;
|
|
26
|
+
if (feet < 1000)
|
|
27
|
+
return `${number(feet, locale, 0)} ft`;
|
|
28
|
+
const miles = feet / 5280;
|
|
29
|
+
return `${number(miles, locale, miles < 10 ? 1 : 0)} mi`;
|
|
30
|
+
}
|
|
31
|
+
if (meters < 1000)
|
|
32
|
+
return `${number(meters, locale, 0)} m`;
|
|
33
|
+
const km = meters / 1000;
|
|
34
|
+
return `${number(km, locale, km < 10 ? 1 : 0)} km`;
|
|
35
|
+
}
|
|
36
|
+
/** `5000` → `5,000 m²`, `2.5e6` → `2.5 km²`; imperial gives acres and square miles. */
|
|
37
|
+
export function formatArea(squareMeters, { unit = 'metric', locale } = {}) {
|
|
38
|
+
if (unit === 'imperial') {
|
|
39
|
+
const acres = squareMeters / 4046.8564224;
|
|
40
|
+
if (acres < 640)
|
|
41
|
+
return `${number(acres, locale, acres < 10 ? 2 : 1)} ac`;
|
|
42
|
+
return `${number(acres / 640, locale, 1)} mi²`;
|
|
43
|
+
}
|
|
44
|
+
if (squareMeters < 1e6)
|
|
45
|
+
return `${number(squareMeters, locale, 0)} m²`;
|
|
46
|
+
return `${number(squareMeters / 1e6, locale, 1)} km²`;
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './bounds.js';
|
|
3
|
+
export * from './format.js';
|
|
4
|
+
export * from './style.js';
|
|
5
|
+
export * from './data.js';
|
|
6
|
+
export * from './equality.js';
|
|
7
|
+
export * from './cluster.js';
|
|
8
|
+
export * from './accessibility.js';
|
|
9
|
+
export * from './route.js';
|
|
10
|
+
export * from './spatial.js';
|
|
11
|
+
export * from './simplify.js';
|
|
12
|
+
export * from './lod.js';
|
|
13
|
+
export { createSpatialWorker } from './workers/client.js';
|
|
14
|
+
export type { SpatialWorker, SpatialWorkerOptions } from './workers/client.js';
|
|
15
|
+
export { createSpatialHandler } from './workers/handler.js';
|
|
16
|
+
export type { SpatialRequest, SpatialResponse } from './workers/protocol.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Plain TypeScript with no Svelte and no MapLibre import: styles, geometry, formatting and
|
|
3
|
+
* GeoJSON helpers that run anywhere — the server, a worker, a test.
|
|
4
|
+
*/
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export * from './bounds.js';
|
|
7
|
+
export * from './format.js';
|
|
8
|
+
export * from './style.js';
|
|
9
|
+
export * from './data.js';
|
|
10
|
+
export * from './equality.js';
|
|
11
|
+
export * from './cluster.js';
|
|
12
|
+
export * from './accessibility.js';
|
|
13
|
+
export * from './route.js';
|
|
14
|
+
export * from './spatial.js';
|
|
15
|
+
export * from './simplify.js';
|
|
16
|
+
export * from './lod.js';
|
|
17
|
+
export { createSpatialWorker } from './workers/client.js';
|
|
18
|
+
export { createSpatialHandler } from './workers/handler.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The simplification tolerance, in degrees, that keeps error under `pixels` at `zoom` — about
|
|
4
|
+
* the width of one screen pixel of longitude at the equator.
|
|
5
|
+
*/
|
|
6
|
+
export declare function toleranceForZoom(zoom: number, { pixels, tileSize }?: {
|
|
7
|
+
pixels?: number | undefined;
|
|
8
|
+
tileSize?: number | undefined;
|
|
9
|
+
}): number;
|
|
10
|
+
export interface LevelOfDetail<P> {
|
|
11
|
+
/** The collection simplified for `zoom`, cached per whole zoom level. */
|
|
12
|
+
at(zoom: number): FeatureCollection<P>;
|
|
13
|
+
clear(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Precomputes nothing; simplifies on demand and caches per whole zoom level, so panning at one
|
|
17
|
+
* zoom costs nothing and zooming costs one pass. At `maxZoom` and above the data is untouched.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createLevelOfDetail<P>(collection: FeatureCollection<P>, { pixels, maxZoom }?: {
|
|
20
|
+
pixels?: number;
|
|
21
|
+
maxZoom?: number;
|
|
22
|
+
}): LevelOfDetail<P>;
|
|
23
|
+
/**
|
|
24
|
+
* A group of features as one outline — the convex hull of every coordinate — for drawing a
|
|
25
|
+
* whole set as a single shape when zoomed far out. `null` for fewer than three distinct points.
|
|
26
|
+
*/
|
|
27
|
+
export declare function outlineOf<P extends Record<string, unknown>>(features: Array<Feature>, properties?: P): Feature<P> | null;
|
package/dist/core/lod.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { convexHull, simplifyFeatures } from './simplify.js';
|
|
2
|
+
import { coordinatesOf } from './bounds.js';
|
|
3
|
+
/*
|
|
4
|
+
* Level of detail: draw less when zoomed out. Shapes are simplified to what a pixel can show at
|
|
5
|
+
* the current zoom, and a group of features can collapse into its outline.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The simplification tolerance, in degrees, that keeps error under `pixels` at `zoom` — about
|
|
9
|
+
* the width of one screen pixel of longitude at the equator.
|
|
10
|
+
*/
|
|
11
|
+
export function toleranceForZoom(zoom, { pixels = 1, tileSize = 512 } = {}) {
|
|
12
|
+
return (360 / (tileSize * 2 ** zoom)) * pixels;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Precomputes nothing; simplifies on demand and caches per whole zoom level, so panning at one
|
|
16
|
+
* zoom costs nothing and zooming costs one pass. At `maxZoom` and above the data is untouched.
|
|
17
|
+
*/
|
|
18
|
+
export function createLevelOfDetail(collection, { pixels = 1, maxZoom = 16 } = {}) {
|
|
19
|
+
const cache = new Map();
|
|
20
|
+
return {
|
|
21
|
+
at(zoom) {
|
|
22
|
+
const level = Math.max(0, Math.floor(zoom));
|
|
23
|
+
if (level >= maxZoom)
|
|
24
|
+
return collection;
|
|
25
|
+
let simplified = cache.get(level);
|
|
26
|
+
if (!simplified) {
|
|
27
|
+
simplified = simplifyFeatures(collection, toleranceForZoom(level, { pixels }));
|
|
28
|
+
cache.set(level, simplified);
|
|
29
|
+
}
|
|
30
|
+
return simplified;
|
|
31
|
+
},
|
|
32
|
+
clear: () => cache.clear(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* A group of features as one outline — the convex hull of every coordinate — for drawing a
|
|
37
|
+
* whole set as a single shape when zoomed far out. `null` for fewer than three distinct points.
|
|
38
|
+
*/
|
|
39
|
+
export function outlineOf(features, properties = {}) {
|
|
40
|
+
const points = [];
|
|
41
|
+
for (const feature of features) {
|
|
42
|
+
for (const position of coordinatesOf(feature))
|
|
43
|
+
points.push([position[0], position[1]]);
|
|
44
|
+
}
|
|
45
|
+
const hull = convexHull(points);
|
|
46
|
+
if (hull.length < 4)
|
|
47
|
+
return null;
|
|
48
|
+
return { type: 'Feature', geometry: { type: 'Polygon', coordinates: [hull] }, properties };
|
|
49
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { LngLat } from './types.js';
|
|
2
|
+
/** Mean earth radius, in metres (IUGG). */
|
|
3
|
+
export declare const EARTH_RADIUS = 6371008.8;
|
|
4
|
+
/** Great-circle distance in metres. `NaN` when either point is not a real coordinate. */
|
|
5
|
+
export declare function haversine(a: LngLat, b: LngLat): number;
|
|
6
|
+
/** The length of a path through `points`, in metres. */
|
|
7
|
+
export declare const pathLength: (points: LngLat[]) => number;
|
|
8
|
+
/** Every pairwise distance, in metres. Symmetric, with zeros on the diagonal. */
|
|
9
|
+
export declare function distanceMatrix(points: LngLat[]): number[][];
|
|
10
|
+
export interface RouteOrder {
|
|
11
|
+
/** Indexes into the stops, in visiting order. */
|
|
12
|
+
order: number[];
|
|
13
|
+
/** Each leg's length in metres: origin to the first stop, then stop to stop. */
|
|
14
|
+
legs: number[];
|
|
15
|
+
/** The whole route, in metres. Legs that cannot be measured count as zero. */
|
|
16
|
+
total: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Improves an order by reversing any stretch that shortens the route, until none does (2-opt).
|
|
20
|
+
* The route starts at `origin` and ends at its last stop; it does not return.
|
|
21
|
+
*/
|
|
22
|
+
export declare function twoOpt(origin: LngLat, points: LngLat[], order: number[], { maxPasses, matrix, }?: {
|
|
23
|
+
maxPasses?: number;
|
|
24
|
+
matrix?: number[][];
|
|
25
|
+
}): number[];
|
|
26
|
+
/**
|
|
27
|
+
* A short order to visit `stops` from `origin`: nearest neighbour first, then 2-opt. Not the
|
|
28
|
+
* shortest possible — that problem is intractable — but close for a day's worth of stops.
|
|
29
|
+
*/
|
|
30
|
+
export declare function optimizeRoute(origin: LngLat, stops: LngLat[], { improve, maxPasses }?: {
|
|
31
|
+
improve?: boolean;
|
|
32
|
+
maxPasses?: number;
|
|
33
|
+
}): RouteOrder;
|
|
34
|
+
/** Legs and total for an order you already have, to compare against an optimised one. */
|
|
35
|
+
export declare const measureRoute: (origin: LngLat, stops: LngLat[], order: number[]) => RouteOrder;
|
|
36
|
+
/** A stop for a directions link: a coordinate, or an address to search for. */
|
|
37
|
+
export type RouteStop = LngLat | string;
|
|
38
|
+
/** Google Maps allows this many stops between origin and destination in one link. */
|
|
39
|
+
export declare const GOOGLE_MAX_WAYPOINTS = 9;
|
|
40
|
+
export interface DirectionsOptions {
|
|
41
|
+
/** Where the trip starts. Leave it out to start from the user's location. */
|
|
42
|
+
origin?: RouteStop;
|
|
43
|
+
travelMode?: 'driving' | 'walking' | 'bicycling' | 'transit';
|
|
44
|
+
}
|
|
45
|
+
/** A Google Maps directions link through `stops`, the last being the destination. */
|
|
46
|
+
export declare function googleDirectionsUrl(stops: RouteStop[], { origin, travelMode }?: DirectionsOptions): string;
|
|
47
|
+
/** An Apple Maps directions link through `stops`. */
|
|
48
|
+
export declare function appleDirectionsUrl(stops: RouteStop[], { origin, travelMode }?: DirectionsOptions): string;
|
|
49
|
+
/**
|
|
50
|
+
* Splits a long list of stops into legs that each fit one link, every leg starting where the
|
|
51
|
+
* last one ended.
|
|
52
|
+
*/
|
|
53
|
+
export declare function splitIntoLegs<T>(stops: T[], maxPerLeg?: number): T[][];
|
|
54
|
+
/**
|
|
55
|
+
* Google Maps links for a route too long for one: one per leg, each starting where the last one
|
|
56
|
+
* ended. A leg holds an origin, up to nine waypoints and a destination.
|
|
57
|
+
*/
|
|
58
|
+
export declare function googleDirectionsLegUrls(stops: RouteStop[], options?: DirectionsOptions): string[];
|