@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.1
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 +487 -2
- package/dist/core/basemap.svelte.d.ts +38 -0
- package/dist/core/basemap.svelte.js +163 -0
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.js +13 -0
- package/dist/core/create-map.svelte.d.ts +207 -0
- package/dist/core/create-map.svelte.js +376 -0
- package/dist/core/gl.d.ts +92 -0
- package/dist/core/gl.js +1 -0
- package/dist/core/interactions.svelte.d.ts +42 -0
- package/dist/core/interactions.svelte.js +213 -0
- package/dist/core/plugin-host.svelte.d.ts +46 -0
- package/dist/core/plugin-host.svelte.js +366 -0
- package/dist/core/plugin.d.ts +148 -0
- package/dist/core/plugin.js +1 -0
- package/dist/core/scoped-context.d.ts +7 -0
- package/dist/core/scoped-context.js +34 -0
- package/dist/data/format.d.ts +10 -0
- package/dist/data/format.js +24 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +9 -0
- package/dist/data/messages/en.json +51 -0
- package/dist/data/messages.d.ts +10 -0
- package/dist/data/messages.js +17 -0
- package/dist/data/metadata.d.ts +22 -0
- package/dist/data/metadata.js +110 -0
- package/dist/data/refs.d.ts +18 -0
- package/dist/data/refs.js +61 -0
- package/dist/data/scale-types.d.ts +7 -0
- package/dist/data/scale-types.js +1 -0
- package/dist/data/scale.d.ts +13 -0
- package/dist/data/scale.js +27 -0
- package/dist/data/selection.d.ts +7 -0
- package/dist/data/selection.js +31 -0
- package/dist/data/static-source.d.ts +4 -0
- package/dist/data/static-source.js +3 -0
- package/dist/data/types.d.ts +69 -0
- package/dist/data/types.js +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +41 -0
- package/dist/plugins/icons.d.ts +21 -0
- package/dist/plugins/icons.js +73 -0
- package/dist/plugins/points-clusters.d.ts +60 -0
- package/dist/plugins/points-clusters.js +93 -0
- package/dist/plugins/points.svelte.d.ts +93 -0
- package/dist/plugins/points.svelte.js +473 -0
- package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
- package/dist/plugins/scale-resolution.svelte.js +111 -0
- package/dist/plugins/shapes.svelte.d.ts +29 -0
- package/dist/plugins/shapes.svelte.js +307 -0
- package/dist/plugins/shared.d.ts +6 -0
- package/dist/plugins/shared.js +18 -0
- package/dist/plugins/style.d.ts +57 -0
- package/dist/plugins/style.js +193 -0
- package/dist/presets/CollocationTooltip.svelte +132 -0
- package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorClusterTooltip.svelte +149 -0
- package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorReadingBox.svelte +58 -0
- package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
- package/dist/presets/MonitorTooltip.svelte +88 -0
- package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
- package/dist/presets/calheatscore.d.ts +24 -0
- package/dist/presets/calheatscore.js +22 -0
- package/dist/presets/collocation-sites.d.ts +18 -0
- package/dist/presets/collocation-sites.js +67 -0
- package/dist/presets/fire-groups.d.ts +26 -0
- package/dist/presets/fire-groups.js +51 -0
- package/dist/presets/forecast-zones.d.ts +25 -0
- package/dist/presets/forecast-zones.js +23 -0
- package/dist/presets/hms-fire.d.ts +26 -0
- package/dist/presets/hms-fire.js +87 -0
- package/dist/presets/hms-smoke.d.ts +19 -0
- package/dist/presets/hms-smoke.js +20 -0
- package/dist/presets/monitors.d.ts +48 -0
- package/dist/presets/monitors.js +243 -0
- package/dist/presets/reactive-props.svelte.d.ts +4 -0
- package/dist/presets/reactive-props.svelte.js +7 -0
- package/dist/presets/regions.d.ts +24 -0
- package/dist/presets/regions.js +22 -0
- package/dist/stores/abort.d.ts +16 -0
- package/dist/stores/abort.js +52 -0
- package/dist/stores/collocation-store.svelte.d.ts +25 -0
- package/dist/stores/collocation-store.svelte.js +72 -0
- package/dist/stores/hms-stores.svelte.d.ts +35 -0
- package/dist/stores/hms-stores.svelte.js +97 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/load-once.svelte.d.ts +25 -0
- package/dist/stores/load-once.svelte.js +93 -0
- package/dist/stores/monitors-store.svelte.d.ts +85 -0
- package/dist/stores/monitors-store.svelte.js +340 -0
- package/dist/theme.css +34 -0
- package/dist/ui/FeatureList.svelte +501 -0
- package/dist/ui/FeatureList.svelte.d.ts +14 -0
- package/dist/ui/Legend.svelte +172 -0
- package/dist/ui/Legend.svelte.d.ts +13 -0
- package/dist/ui/LegendBar.svelte +161 -0
- package/dist/ui/LegendBar.svelte.d.ts +11 -0
- package/dist/ui/MapControls.svelte +135 -0
- package/dist/ui/MapControls.svelte.d.ts +11 -0
- package/dist/ui/MapTooltip.svelte +158 -0
- package/dist/ui/MapTooltip.svelte.d.ts +28 -0
- package/dist/ui/SelectionBar.svelte +123 -0
- package/dist/ui/SelectionBar.svelte.d.ts +10 -0
- package/dist/ui/TooltipItem.svelte +66 -0
- package/dist/ui/TooltipItem.svelte.d.ts +11 -0
- package/dist/ui/ViewSummary.svelte +97 -0
- package/dist/ui/ViewSummary.svelte.d.ts +7 -0
- package/dist/ui/controls.css +45 -0
- package/dist/ui/data-credits.svelte.d.ts +9 -0
- package/dist/ui/data-credits.svelte.js +52 -0
- package/dist/ui/feature-list.d.ts +31 -0
- package/dist/ui/feature-list.js +80 -0
- package/dist/ui/legend-data.d.ts +41 -0
- package/dist/ui/legend-data.js +40 -0
- package/dist/ui/level-chip.d.ts +11 -0
- package/dist/ui/level-chip.js +15 -0
- package/dist/ui/value-format.d.ts +7 -0
- package/dist/ui/value-format.js +13 -0
- package/dist/ui/view-summary.d.ts +11 -0
- package/dist/ui/view-summary.js +33 -0
- package/dist/view/MapView.svelte +373 -0
- package/dist/view/MapView.svelte.d.ts +43 -0
- package/dist/view/create-gl-map.d.ts +34 -0
- package/dist/view/create-gl-map.js +182 -0
- package/dist/view/list-placement.d.ts +21 -0
- package/dist/view/list-placement.js +13 -0
- package/package.json +103 -5
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { FeatureCollection, Geometry } from "geojson";
|
|
2
|
+
import type { Scale, ScaleRef } from "./scale-types.js";
|
|
3
|
+
export type FeatureRef = {
|
|
4
|
+
kind: "monitor";
|
|
5
|
+
id: string;
|
|
6
|
+
} | {
|
|
7
|
+
kind: "region";
|
|
8
|
+
type: string;
|
|
9
|
+
code: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: "feature";
|
|
12
|
+
id: string;
|
|
13
|
+
};
|
|
14
|
+
export type SpatialSelection = {
|
|
15
|
+
kind: "monitors" | "regions";
|
|
16
|
+
ids: string[];
|
|
17
|
+
};
|
|
18
|
+
export type SelectionMode = "none" | "single" | "multiple";
|
|
19
|
+
export interface MapFeatureProperties {
|
|
20
|
+
refKey: string;
|
|
21
|
+
name: string;
|
|
22
|
+
value: number | string | null;
|
|
23
|
+
}
|
|
24
|
+
/** A monitor on the map: what the monitors preset draws and its tooltips show. */
|
|
25
|
+
export interface MonitorFeatureProperties extends MapFeatureProperties {
|
|
26
|
+
/** The server's monitor type (e.g. "purpleair", "airnow"); picks the marker shape. */
|
|
27
|
+
type: string;
|
|
28
|
+
isActive: boolean;
|
|
29
|
+
isSjvair: boolean;
|
|
30
|
+
location: "inside" | "outside";
|
|
31
|
+
/** When the reading was taken (ISO 8601), for the tooltip. */
|
|
32
|
+
timestamp?: string | null;
|
|
33
|
+
}
|
|
34
|
+
export type DataStatus = {
|
|
35
|
+
state: "loading";
|
|
36
|
+
} | {
|
|
37
|
+
state: "ready";
|
|
38
|
+
} | {
|
|
39
|
+
state: "error";
|
|
40
|
+
error: unknown;
|
|
41
|
+
retry(): void;
|
|
42
|
+
} | {
|
|
43
|
+
state: "unavailable";
|
|
44
|
+
reason: string;
|
|
45
|
+
};
|
|
46
|
+
export interface DataSource<P extends MapFeatureProperties = MapFeatureProperties> {
|
|
47
|
+
readonly status: DataStatus;
|
|
48
|
+
readonly features: FeatureCollection<Geometry, P> | null;
|
|
49
|
+
readonly scale: ScaleRef | Scale | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A collocation site: drawn at its reference monitor with that monitor's type, location,
|
|
53
|
+
* activity, and reading, plus the collocated monitor's reading. Readings are null when a
|
|
54
|
+
* monitor isn't in the monitors store (inactive, filtered out, or not loaded yet).
|
|
55
|
+
*/
|
|
56
|
+
export interface CollocationFeatureProperties extends MonitorFeatureProperties {
|
|
57
|
+
/**
|
|
58
|
+
* The reference monitor's own name (the tooltip shows it); null when it isn't in the
|
|
59
|
+
* monitors store.
|
|
60
|
+
*/
|
|
61
|
+
referenceName: string | null;
|
|
62
|
+
colocatedId: string;
|
|
63
|
+
/** Null when the collocated monitor isn't in the monitors store. */
|
|
64
|
+
colocatedName: string | null;
|
|
65
|
+
/** The collocated monitor's type, for its tooltip's "(N avg)" note; null when unknown. */
|
|
66
|
+
colocatedType: string | null;
|
|
67
|
+
colocatedValue: number | null;
|
|
68
|
+
colocatedTimestamp: string | null;
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { type CreateMapOptions, type MapContext } from "./core/create-map.svelte.js";
|
|
2
|
+
export { defaultBasemaps } from "./view/create-gl-map.js";
|
|
3
|
+
export { default as MapView } from "./view/MapView.svelte";
|
|
4
|
+
export type { FeatureListOptions, ListPosition } from "./view/list-placement.js";
|
|
5
|
+
export { default as Legend } from "./ui/Legend.svelte";
|
|
6
|
+
export { default as FeatureList } from "./ui/FeatureList.svelte";
|
|
7
|
+
export { default as MapTooltip } from "./ui/MapTooltip.svelte";
|
|
8
|
+
export { default as SelectionBar } from "./ui/SelectionBar.svelte";
|
|
9
|
+
export { default as ViewSummary } from "./ui/ViewSummary.svelte";
|
|
10
|
+
export { default as MapControls } from "./ui/MapControls.svelte";
|
|
11
|
+
export { legendSection } from "./ui/legend-data.js";
|
|
12
|
+
export type { LegendBar as LegendBarData, LegendSection } from "./ui/legend-data.js";
|
|
13
|
+
export { rowLabel } from "./ui/feature-list.js";
|
|
14
|
+
export type { SortBy } from "./ui/feature-list.js";
|
|
15
|
+
export { pointsPlugin } from "./plugins/points.svelte.js";
|
|
16
|
+
export type { ClusterInfo, ClusterLayerArgs, PointsOptions, PointsTooltipEnv } from "./plugins/points.svelte.js";
|
|
17
|
+
export type { ClusterLayers } from "./plugins/points-clusters.js";
|
|
18
|
+
export { colorExpression, levelKeyExpression, opacityExpression } from "./plugins/style.js";
|
|
19
|
+
export type { ThemeColors, ValueInput } from "./plugins/style.js";
|
|
20
|
+
export { shapesPlugin } from "./plugins/shapes.svelte.js";
|
|
21
|
+
export type { ShapesOptions } from "./plugins/shapes.svelte.js";
|
|
22
|
+
export { monitorShape, monitorsPlugin } from "./presets/monitors.js";
|
|
23
|
+
export type { MonitorShape, MonitorsPluginOptions } from "./presets/monitors.js";
|
|
24
|
+
export { regionsPlugin } from "./presets/regions.js";
|
|
25
|
+
export type { RegionsPluginOptions } from "./presets/regions.js";
|
|
26
|
+
export { calHeatScorePlugin } from "./presets/calheatscore.js";
|
|
27
|
+
export type { CalHeatScorePluginOptions } from "./presets/calheatscore.js";
|
|
28
|
+
export { forecastZonesPlugin } from "./presets/forecast-zones.js";
|
|
29
|
+
export type { ForecastZonesPluginOptions } from "./presets/forecast-zones.js";
|
|
30
|
+
export { summaryText } from "./ui/view-summary.js";
|
|
31
|
+
export { getMapContext } from "./core/context.js";
|
|
32
|
+
export type { BlockingClick, FeatureHit, FeatureListItem, LayerSpecWithoutId, LegendData, MapPlugin, PluginContext, Providers } from "./core/plugin.js";
|
|
33
|
+
export type { GlMapLike } from "./core/gl.js";
|
|
34
|
+
export type { CustomLayerInterface } from "@maptiler/sdk";
|
|
35
|
+
export type { Hovered } from "./core/interactions.svelte.js";
|
|
36
|
+
export type { BasemapConfig } from "./core/basemap.svelte.js";
|
|
37
|
+
export type { Bounds, CameraState, CameraTarget, CreateMapOptions, FeatureClickHandler, MapConfig, MapContext, PluginOptions } from "./core/create-map.svelte.js";
|
|
38
|
+
/**
|
|
39
|
+
* The attached MapTiler `Map`, or `null` while detached: an escape hatch for markers and popups
|
|
40
|
+
* only. The host must never add sources or layers through it; plugins own those.
|
|
41
|
+
*/
|
|
42
|
+
export declare function maptilerMap(map: MapContext): import("@maptiler/sdk").Map | null;
|
|
43
|
+
/**
|
|
44
|
+
* Creates a map's state and services; show it with `<MapView map={…} />`. Without
|
|
45
|
+
* `config.basemaps`, the basemaps are MapTiler Streets v4 and its variants, as on today's map.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createMap(options: CreateMapOptions): MapContext;
|
|
48
|
+
export { hmsSmokePlugin } from "./presets/hms-smoke.js";
|
|
49
|
+
export type { HmsSmokePluginOptions } from "./presets/hms-smoke.js";
|
|
50
|
+
export { hmsFirePlugin } from "./presets/hms-fire.js";
|
|
51
|
+
export type { HmsFirePluginOptions } from "./presets/hms-fire.js";
|
|
52
|
+
export { cleanFires, groupFires } from "./presets/fire-groups.js";
|
|
53
|
+
export type { FireDetection, FireGroup } from "./presets/fire-groups.js";
|
|
54
|
+
export { collocationSitesPlugin, COLLOCATION_CROSSHAIR_COLOR } from "./presets/collocation-sites.js";
|
|
55
|
+
export type { CollocationSitesPluginOptions } from "./presets/collocation-sites.js";
|
|
56
|
+
export { circle, CROSSHAIR_ICON_SIZE, square, triangle, withCrosshair } from "./plugins/icons.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createMap as createMapContext } from "./core/create-map.svelte.js";
|
|
2
|
+
import { defaultBasemaps } from "./view/create-gl-map.js";
|
|
3
|
+
export { defaultBasemaps } from "./view/create-gl-map.js";
|
|
4
|
+
export { default as MapView } from "./view/MapView.svelte";
|
|
5
|
+
export { default as Legend } from "./ui/Legend.svelte";
|
|
6
|
+
export { default as FeatureList } from "./ui/FeatureList.svelte";
|
|
7
|
+
export { default as MapTooltip } from "./ui/MapTooltip.svelte";
|
|
8
|
+
export { default as SelectionBar } from "./ui/SelectionBar.svelte";
|
|
9
|
+
export { default as ViewSummary } from "./ui/ViewSummary.svelte";
|
|
10
|
+
export { default as MapControls } from "./ui/MapControls.svelte";
|
|
11
|
+
export { legendSection } from "./ui/legend-data.js";
|
|
12
|
+
export { rowLabel } from "./ui/feature-list.js";
|
|
13
|
+
export { pointsPlugin } from "./plugins/points.svelte.js";
|
|
14
|
+
export { colorExpression, levelKeyExpression, opacityExpression } from "./plugins/style.js";
|
|
15
|
+
export { shapesPlugin } from "./plugins/shapes.svelte.js";
|
|
16
|
+
export { monitorShape, monitorsPlugin } from "./presets/monitors.js";
|
|
17
|
+
export { regionsPlugin } from "./presets/regions.js";
|
|
18
|
+
export { calHeatScorePlugin } from "./presets/calheatscore.js";
|
|
19
|
+
export { forecastZonesPlugin } from "./presets/forecast-zones.js";
|
|
20
|
+
export { summaryText } from "./ui/view-summary.js";
|
|
21
|
+
export { getMapContext } from "./core/context.js";
|
|
22
|
+
/**
|
|
23
|
+
* The attached MapTiler `Map`, or `null` while detached: an escape hatch for markers and popups
|
|
24
|
+
* only. The host must never add sources or layers through it; plugins own those.
|
|
25
|
+
*/
|
|
26
|
+
export function maptilerMap(map) {
|
|
27
|
+
return map.gl;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates a map's state and services; show it with `<MapView map={…} />`. Without
|
|
31
|
+
* `config.basemaps`, the basemaps are MapTiler Streets v4 and its variants, as on today's map.
|
|
32
|
+
*/
|
|
33
|
+
export function createMap(options) {
|
|
34
|
+
const basemaps = options.config.basemaps ?? defaultBasemaps();
|
|
35
|
+
return createMapContext({ ...options, config: { ...options.config, basemaps } });
|
|
36
|
+
}
|
|
37
|
+
export { hmsSmokePlugin } from "./presets/hms-smoke.js";
|
|
38
|
+
export { hmsFirePlugin } from "./presets/hms-fire.js";
|
|
39
|
+
export { cleanFires, groupFires } from "./presets/fire-groups.js";
|
|
40
|
+
export { collocationSitesPlugin, COLLOCATION_CROSSHAIR_COLOR } from "./presets/collocation-sites.js";
|
|
41
|
+
export { circle, CROSSHAIR_ICON_SIZE, square, triangle, withCrosshair } from "./plugins/icons.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function square(color: string, borderWidth: number, borderColor?: string): string;
|
|
2
|
+
export declare function circle(color: string, borderWidth: number, borderColor?: string): string;
|
|
3
|
+
export declare function triangle(color: string, borderWidth: number, borderColor?: string): string;
|
|
4
|
+
/** The size of a `withCrosshair` image: a 24px marker plus the crosshair around it (56px). */
|
|
5
|
+
export declare const CROSSHAIR_ICON_SIZE: number;
|
|
6
|
+
/**
|
|
7
|
+
* A 24px marker SVG (`circle`, `square`, `triangle`) centered in a crosshair of `color`, as
|
|
8
|
+
* monitor-map draws collocation sites. The result is `CROSSHAIR_ICON_SIZE` pixels square.
|
|
9
|
+
*/
|
|
10
|
+
export declare function withCrosshair(baseSvg: string, color: string): string;
|
|
11
|
+
export declare function fireIcon(color: string, size: number): string;
|
|
12
|
+
/** Room around the marker for the ring and the check badge. */
|
|
13
|
+
export declare const SELECTED_OVERLAY_PADDING = 6;
|
|
14
|
+
/**
|
|
15
|
+
* The selection mark drawn over a selected marker `size` pixels across: a ring around it in
|
|
16
|
+
* `highlight` plus a check mark badge, so selection never relies on color alone. The image is
|
|
17
|
+
* `size + 2 * SELECTED_OVERLAY_PADDING` pixels square, centered on the marker. Colors are
|
|
18
|
+
* concrete values read from the `--highlight` and `--highlight-foreground` tokens at setup.
|
|
19
|
+
*/
|
|
20
|
+
export declare function selectedOverlay(highlight: string, checkColor: string, size: number): string;
|
|
21
|
+
export declare function asDataURI(svg: string): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { darken } from "color2k";
|
|
2
|
+
// Marker shapes ported from monitor-map (src/lib/map/icons.ts). Shapes and sizes are
|
|
3
|
+
// presentation; every color comes in from a scale level or a theme token.
|
|
4
|
+
/**
|
|
5
|
+
* A slightly darker stroke for a fill color. color2k throws on an empty or unparseable color
|
|
6
|
+
* (monitor-map crashed on `darken("")`), so those keep the color itself.
|
|
7
|
+
*/
|
|
8
|
+
function strokeFor(color) {
|
|
9
|
+
try {
|
|
10
|
+
return darken(color, 0.1);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return color;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function square(color, borderWidth, borderColor) {
|
|
17
|
+
return `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
18
|
+
<rect x="${borderWidth / 2}" y="${borderWidth / 2}" width="${24 - borderWidth}" height="${24 - borderWidth}"
|
|
19
|
+
fill="${color}" stroke="${strokeFor(borderColor ?? color)}" stroke-width="${borderWidth}"/>
|
|
20
|
+
</svg>`;
|
|
21
|
+
}
|
|
22
|
+
export function circle(color, borderWidth, borderColor) {
|
|
23
|
+
return `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
24
|
+
<circle cx="12" cy="12" r="${12 - borderWidth}" fill="${color}" stroke="${strokeFor(borderColor ?? color)}" stroke-width="${borderWidth}"/>
|
|
25
|
+
</svg>`;
|
|
26
|
+
}
|
|
27
|
+
export function triangle(color, borderWidth, borderColor) {
|
|
28
|
+
const points = `${12},${borderWidth} ${24 - borderWidth},${24 - borderWidth} ${borderWidth},${24 - borderWidth}`;
|
|
29
|
+
return `<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
30
|
+
<polygon points="${points}"
|
|
31
|
+
fill="${color}" stroke="${strokeFor(borderColor ?? color)}" stroke-width="${borderWidth}"/>
|
|
32
|
+
</svg>`;
|
|
33
|
+
}
|
|
34
|
+
// monitor-map's collocation crosshair (src/lib/map/icons.ts): a ring with ticks around a marker.
|
|
35
|
+
const CROSSHAIR_PATH = "M495.9 248H392c-4.4 0-8 3.6-8 8s3.6 8 8 8H495.9C491.7 390.2 390.2 491.7 264 495.9V392c0-4.4-3.6-8-8-8s-8 3.6-8 8V495.9C121.8 491.7 20.3 390.2 16.1 264H120c4.4 0 8-3.6 8-8s-3.6-8-8-8H16.1C20.3 121.8 121.8 20.3 248 16.1V120c0 4.4 3.6 8 8 8s8-3.6 8-8V16.1C390.2 20.3 491.7 121.8 495.9 248zM0 256a256 256 0 1 0 512 0A256 256 0 1 0 0 256z";
|
|
36
|
+
const CROSSHAIR_PADDING = 16;
|
|
37
|
+
/** The size of a `withCrosshair` image: a 24px marker plus the crosshair around it (56px). */
|
|
38
|
+
export const CROSSHAIR_ICON_SIZE = 24 + CROSSHAIR_PADDING * 2;
|
|
39
|
+
/**
|
|
40
|
+
* A 24px marker SVG (`circle`, `square`, `triangle`) centered in a crosshair of `color`, as
|
|
41
|
+
* monitor-map draws collocation sites. The result is `CROSSHAIR_ICON_SIZE` pixels square.
|
|
42
|
+
*/
|
|
43
|
+
export function withCrosshair(baseSvg, color) {
|
|
44
|
+
const size = CROSSHAIR_ICON_SIZE;
|
|
45
|
+
const inner = baseSvg.replace("<svg ", `<svg x="${CROSSHAIR_PADDING}" y="${CROSSHAIR_PADDING}" `);
|
|
46
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" xmlns="http://www.w3.org/2000/svg">${inner}<svg x="0" y="0" width="${size}" height="${size}" viewBox="-8 -8 528 528"><path d="${CROSSHAIR_PATH}" fill="${color}" stroke="${color}" stroke-width="5"/></svg></svg>`;
|
|
47
|
+
}
|
|
48
|
+
export function fireIcon(color, size) {
|
|
49
|
+
return `<svg width="${size}" height="${size}" viewBox="0 -960 960 960" xmlns="http://www.w3.org/2000/svg" fill="${color}"><path d="M200-400q0 52 21 98.5t60 81.5q-1-5-1-9v-9q0-32 12-60t35-51l113-111 113 111q23 23 35 51t12 60v9q0 4-1 9 39-35 60-81.5t21-98.5q0-50-18.5-94.5T608-574q-20 13-42 19.5t-45 6.5q-62 0-107.5-41T361-690q-39 33-69 68.5t-50.5 72Q221-513 210.5-475T200-400Zm240 52-57 56q-11 11-17 25t-6 29q0 32 23.5 55t56.5 23q33 0 56.5-23t23.5-55q0-16-6-29.5T497-292l-57-56Zm0-492v132q0 34 23.5 57t57.5 23q18 0 33.5-7.5T582-658l18-22q74 42 117 117t43 163q0 134-93 227T440-80q-134 0-227-93t-93-227q0-129 86.5-245T440-840Zm400 320q-17 0-28.5-11.5T800-560q0-17 11.5-28.5T840-600q17 0 28.5 11.5T880-560q0 17-11.5 28.5T840-520Zm-40-120v-200h80v200h-80Z"/></svg>`;
|
|
50
|
+
}
|
|
51
|
+
/** Room around the marker for the ring and the check badge. */
|
|
52
|
+
export const SELECTED_OVERLAY_PADDING = 6;
|
|
53
|
+
/**
|
|
54
|
+
* The selection mark drawn over a selected marker `size` pixels across: a ring around it in
|
|
55
|
+
* `highlight` plus a check mark badge, so selection never relies on color alone. The image is
|
|
56
|
+
* `size + 2 * SELECTED_OVERLAY_PADDING` pixels square, centered on the marker. Colors are
|
|
57
|
+
* concrete values read from the `--highlight` and `--highlight-foreground` tokens at setup.
|
|
58
|
+
*/
|
|
59
|
+
export function selectedOverlay(highlight, checkColor, size) {
|
|
60
|
+
const total = size + 2 * SELECTED_OVERLAY_PADDING;
|
|
61
|
+
const c = total / 2;
|
|
62
|
+
const ring = size / 2 + 2.5;
|
|
63
|
+
const b = SELECTED_OVERLAY_PADDING;
|
|
64
|
+
const bx = total - b;
|
|
65
|
+
return `<svg width="${total}" height="${total}" viewBox="0 0 ${total} ${total}" xmlns="http://www.w3.org/2000/svg">
|
|
66
|
+
<circle cx="${c}" cy="${c}" r="${ring}" fill="none" stroke="${highlight}" stroke-width="3"/>
|
|
67
|
+
<circle cx="${bx}" cy="${b}" r="${b - 0.5}" fill="${highlight}" stroke="${checkColor}" stroke-width="1"/>
|
|
68
|
+
<path d="M${bx - 2.5} ${b} l1.75 1.75 l3.25 -3.5" fill="none" stroke="${checkColor}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
69
|
+
</svg>`;
|
|
70
|
+
}
|
|
71
|
+
export function asDataURI(svg) {
|
|
72
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
73
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ExpressionSpecification, FilterSpecification } from "@maptiler/sdk";
|
|
2
|
+
import type { GlMapLike } from "../core/gl.js";
|
|
3
|
+
import type { BlockingClick, FeatureHit, LayerSpecWithoutId } from "../core/plugin.js";
|
|
4
|
+
import type { Scale } from "../data/scale-types.js";
|
|
5
|
+
import type { MapFeatureProperties } from "../data/types.js";
|
|
6
|
+
import type { ThemeColors } from "./style.js";
|
|
7
|
+
/** A hovered cluster bubble. */
|
|
8
|
+
export interface ClusterInfo<P extends MapFeatureProperties> {
|
|
9
|
+
group: string;
|
|
10
|
+
pointCount: number;
|
|
11
|
+
/** The cluster feature's properties, including `cluster.properties` aggregates. */
|
|
12
|
+
properties: Readonly<Record<string, unknown>>;
|
|
13
|
+
/** Every point in the cluster; rejects if the cluster is gone, empty once torn down. */
|
|
14
|
+
leaves(): Promise<Array<{
|
|
15
|
+
props: P;
|
|
16
|
+
lngLat: [number, number];
|
|
17
|
+
}>>;
|
|
18
|
+
}
|
|
19
|
+
/** What custom cluster layers are built from. */
|
|
20
|
+
export interface ClusterLayerArgs {
|
|
21
|
+
group: string;
|
|
22
|
+
sourceId: string;
|
|
23
|
+
scale: Scale;
|
|
24
|
+
theme: ThemeColors;
|
|
25
|
+
/**
|
|
26
|
+
* The `icon-image` for the plugin's icon of `category` at the level `levelKey` evaluates to
|
|
27
|
+
* (use `levelKeyExpression`); registers that category's images.
|
|
28
|
+
*/
|
|
29
|
+
iconImage(category: string, levelKey: ExpressionSpecification): ExpressionSpecification;
|
|
30
|
+
/** The `icon-size` that shows an icon at its `size` (images are drawn larger for HiDPI). */
|
|
31
|
+
iconSizeScale: number;
|
|
32
|
+
}
|
|
33
|
+
export type ClusterLayers = Array<{
|
|
34
|
+
local: string;
|
|
35
|
+
spec: LayerSpecWithoutId;
|
|
36
|
+
}>;
|
|
37
|
+
export declare const CLUSTERED: FilterSpecification;
|
|
38
|
+
/** Cluster bubbles: a neutral circle with the count. */
|
|
39
|
+
export declare function defaultClusterLayers({ sourceId, theme }: ClusterLayerArgs): ClusterLayers;
|
|
40
|
+
/** What a cluster hover hit names: the data generation it was hovered in, the id, the group. */
|
|
41
|
+
export interface ClusterRef {
|
|
42
|
+
generation: number;
|
|
43
|
+
clusterId: number;
|
|
44
|
+
group: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The hover hit for a bubble: a `{ kind: "feature", id: "cluster:<generation>:<id>:<group>" }`
|
|
48
|
+
* ref (cluster ids are only meaningful for the data they were made from) carrying the bubble's
|
|
49
|
+
* properties. No side effects: lookups call it too.
|
|
50
|
+
*/
|
|
51
|
+
export declare function clusterHit(pluginId: string, generation: number, group: string, props: Record<string, unknown>): FeatureHit | null;
|
|
52
|
+
/** The cluster a hover hit names, or null for any other hit. */
|
|
53
|
+
export declare function parseClusterHit(hit: FeatureHit): ClusterRef | null;
|
|
54
|
+
/** A cluster's points, from its source; `[]` once `isDead`. */
|
|
55
|
+
export declare function clusterLeaves<P extends MapFeatureProperties>(gl: GlMapLike, sourceId: string, clusterId: number, pointCount: number, isDead: () => boolean): Promise<Array<{
|
|
56
|
+
props: P;
|
|
57
|
+
lngLat: [number, number];
|
|
58
|
+
}>>;
|
|
59
|
+
/** Zooms in far enough to expand a clicked bubble; no animation under reduced motion. */
|
|
60
|
+
export declare function zoomIntoCluster(gl: GlMapLike, sourceId: string, feature: BlockingClick, isDead: () => boolean, pluginId: string): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { encodeRef } from "../data/refs.js";
|
|
2
|
+
export const CLUSTERED = ["has", "point_count"];
|
|
3
|
+
function prefersReducedMotion() {
|
|
4
|
+
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
5
|
+
}
|
|
6
|
+
/** Cluster bubbles: a neutral circle with the count. */
|
|
7
|
+
export function defaultClusterLayers({ sourceId, theme }) {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
local: "clusters",
|
|
11
|
+
spec: {
|
|
12
|
+
type: "circle",
|
|
13
|
+
source: sourceId,
|
|
14
|
+
filter: CLUSTERED,
|
|
15
|
+
paint: {
|
|
16
|
+
"circle-radius": ["step", ["get", "point_count"], 12, 10, 16, 25, 20],
|
|
17
|
+
"circle-color": theme.neutral,
|
|
18
|
+
"circle-stroke-color": theme.foreground,
|
|
19
|
+
"circle-stroke-width": 1
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
local: "cluster-count",
|
|
25
|
+
spec: {
|
|
26
|
+
type: "symbol",
|
|
27
|
+
source: sourceId,
|
|
28
|
+
filter: CLUSTERED,
|
|
29
|
+
layout: {
|
|
30
|
+
"text-field": ["get", "point_count_abbreviated"],
|
|
31
|
+
"text-size": 12,
|
|
32
|
+
"text-allow-overlap": true
|
|
33
|
+
},
|
|
34
|
+
paint: { "text-color": theme.foreground }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
const CLUSTER_ID = /^cluster:(\d+):(\d+):(.*)$/s;
|
|
40
|
+
/**
|
|
41
|
+
* The hover hit for a bubble: a `{ kind: "feature", id: "cluster:<generation>:<id>:<group>" }`
|
|
42
|
+
* ref (cluster ids are only meaningful for the data they were made from) carrying the bubble's
|
|
43
|
+
* properties. No side effects: lookups call it too.
|
|
44
|
+
*/
|
|
45
|
+
export function clusterHit(pluginId, generation, group, props) {
|
|
46
|
+
const clusterId = props.cluster_id;
|
|
47
|
+
if (typeof clusterId !== "number")
|
|
48
|
+
return null;
|
|
49
|
+
const ref = { kind: "feature", id: `cluster:${generation}:${clusterId}:${group}` };
|
|
50
|
+
return { pluginId, ref, refKey: encodeRef(ref), name: "", properties: { ...props } };
|
|
51
|
+
}
|
|
52
|
+
/** The cluster a hover hit names, or null for any other hit. */
|
|
53
|
+
export function parseClusterHit(hit) {
|
|
54
|
+
if (hit.ref.kind !== "feature")
|
|
55
|
+
return null;
|
|
56
|
+
const match = CLUSTER_ID.exec(hit.ref.id);
|
|
57
|
+
if (!match)
|
|
58
|
+
return null;
|
|
59
|
+
return { generation: Number(match[1]), clusterId: Number(match[2]), group: match[3] };
|
|
60
|
+
}
|
|
61
|
+
/** A cluster's points, from its source; `[]` once `isDead`. */
|
|
62
|
+
export async function clusterLeaves(gl, sourceId, clusterId, pointCount, isDead) {
|
|
63
|
+
const clusters = isDead() ? undefined : gl.getSource(sourceId);
|
|
64
|
+
if (!clusters)
|
|
65
|
+
return [];
|
|
66
|
+
const leaves = await clusters.getClusterLeaves(clusterId, pointCount, 0);
|
|
67
|
+
if (isDead())
|
|
68
|
+
return [];
|
|
69
|
+
return leaves.flatMap((leaf) => leaf.geometry.type === "Point"
|
|
70
|
+
? [
|
|
71
|
+
{
|
|
72
|
+
props: leaf.properties,
|
|
73
|
+
lngLat: leaf.geometry.coordinates.slice(0, 2)
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
: []);
|
|
77
|
+
}
|
|
78
|
+
/** Zooms in far enough to expand a clicked bubble; no animation under reduced motion. */
|
|
79
|
+
export function zoomIntoCluster(gl, sourceId, feature, isDead, pluginId) {
|
|
80
|
+
const clusterId = feature.properties.cluster_id;
|
|
81
|
+
if (typeof clusterId !== "number" || feature.geometry.type !== "Point")
|
|
82
|
+
return;
|
|
83
|
+
const [lng, lat] = feature.geometry.coordinates;
|
|
84
|
+
const clusters = gl.getSource(sourceId);
|
|
85
|
+
clusters?.getClusterExpansionZoom(clusterId).then((zoom) => {
|
|
86
|
+
if (isDead())
|
|
87
|
+
return;
|
|
88
|
+
gl.easeTo({ center: [lng, lat], zoom, ...(prefersReducedMotion() && { duration: 0 }) });
|
|
89
|
+
}, (error) => {
|
|
90
|
+
if (!isDead())
|
|
91
|
+
console.warn(`map-sdk: plugin "${pluginId}" could not expand a cluster`, error);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { GeoJSONSourceSpecification } from "@maptiler/sdk";
|
|
2
|
+
import { type Snippet } from "svelte";
|
|
3
|
+
import type { FeatureListItem, MapPlugin, Providers } from "../core/plugin.js";
|
|
4
|
+
import type { Scale } from "../data/scale-types.js";
|
|
5
|
+
import type { DataSource, MapFeatureProperties } from "../data/types.js";
|
|
6
|
+
import { type ClusterInfo, type ClusterLayerArgs, type ClusterLayers } from "./points-clusters.js";
|
|
7
|
+
import { type ThemeColors } from "./style.js";
|
|
8
|
+
export type { ClusterInfo, ClusterLayerArgs } from "./points-clusters.js";
|
|
9
|
+
/** What tooltip content gets: the map's providers and the plugin's resolved scale. */
|
|
10
|
+
export interface PointsTooltipEnv extends Providers {
|
|
11
|
+
/** Null while it resolves or when unavailable. */
|
|
12
|
+
scale: Scale | null;
|
|
13
|
+
/** Decimal places for values, as the list items carry it. */
|
|
14
|
+
precision: number;
|
|
15
|
+
}
|
|
16
|
+
export interface PointsOptions<P extends MapFeatureProperties> {
|
|
17
|
+
id: string;
|
|
18
|
+
source: DataSource<P>;
|
|
19
|
+
/** Already-translated layer name for the list and summary. */
|
|
20
|
+
layerLabel: string;
|
|
21
|
+
selectable?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* How a selected feature shows. "ring" (default): a highlight ring and check mark over it.
|
|
24
|
+
* "enlarge": the icon draws at 1.3x with no ring (icons only; circles are unchanged).
|
|
25
|
+
*/
|
|
26
|
+
selectedStyle?: "ring" | "enlarge";
|
|
27
|
+
icon?: {
|
|
28
|
+
category(props: P): string;
|
|
29
|
+
/**
|
|
30
|
+
* An SVG `size` pixels square, filled with `color`; `theme` for outlines. `levelIndex` is the
|
|
31
|
+
* level's position in the scale, or -1 for the neutral image.
|
|
32
|
+
*/
|
|
33
|
+
draw(category: string, color: string, theme: ThemeColors, levelIndex?: number): string;
|
|
34
|
+
size: number;
|
|
35
|
+
/** Draws in `mutedColor` whatever the value (e.g. an inactive monitor). */
|
|
36
|
+
muted?(props: P): boolean;
|
|
37
|
+
/** The color of muted images. Default: the theme's `--muted-foreground`. */
|
|
38
|
+
mutedColor?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Used when there is no `icon`. */
|
|
41
|
+
circle?: {
|
|
42
|
+
radius: number;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* One clustered source per group. Cluster bubbles hide what is beneath them from clicks and
|
|
46
|
+
* hovers; clicking one zooms in to expand it.
|
|
47
|
+
*/
|
|
48
|
+
cluster?: {
|
|
49
|
+
radius: number;
|
|
50
|
+
maxZoom: number;
|
|
51
|
+
groupBy?(props: P): string;
|
|
52
|
+
/** MapLibre `clusterProperties`: aggregates the layers and tooltip can read. */
|
|
53
|
+
properties?: GeoJSONSourceSpecification["clusterProperties"];
|
|
54
|
+
/** Replaces the neutral count bubbles. Local ids get the group appended. */
|
|
55
|
+
layers?(args: ClusterLayerArgs): ClusterLayers;
|
|
56
|
+
/** Tooltip content for a hovered cluster. Without it, hovering a bubble shows nothing. */
|
|
57
|
+
tooltip?(cluster: ClusterInfo<P>, env: PointsTooltipEnv): Snippet | undefined;
|
|
58
|
+
};
|
|
59
|
+
/** Higher draws on top (and, clustered, a group with a higher key draws above the others). */
|
|
60
|
+
sortKey?(props: P): number;
|
|
61
|
+
/**
|
|
62
|
+
* Limits every layer the plugin draws (points, clusters, selection) to a zoom range: visible
|
|
63
|
+
* from `min` (inclusive) up to `max` (exclusive). Set on the layers, so it switches live while
|
|
64
|
+
* zooming. Default: all zooms.
|
|
65
|
+
*/
|
|
66
|
+
zoomRange?: {
|
|
67
|
+
min?: number;
|
|
68
|
+
max?: number;
|
|
69
|
+
};
|
|
70
|
+
/** How the legend shows a range scale: a gradient (default) or labeled blocks. */
|
|
71
|
+
legendDisplay?: "gradient" | "blocks";
|
|
72
|
+
/**
|
|
73
|
+
* What titles the legend section: `layerLabel` (default) or, for a source with a `ScaleRef`,
|
|
74
|
+
* the entry type's label from the metadata provider (e.g. "PM2.5"), with `layerLabel` until
|
|
75
|
+
* it loads.
|
|
76
|
+
*/
|
|
77
|
+
legendTitle?: "layer" | "entry-type";
|
|
78
|
+
/** Whether `features()` lists this plugin's features. Default true. */
|
|
79
|
+
listed?: boolean;
|
|
80
|
+
/** Tooltip content for a feature, below its name; `props` are its source properties. */
|
|
81
|
+
tooltip?(item: FeatureListItem, props: P, env: PointsTooltipEnv): Snippet | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* The `tooltip` content shows the feature's name itself, so no name header is drawn above
|
|
84
|
+
* it.
|
|
85
|
+
*/
|
|
86
|
+
tooltipShowsName?: boolean;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Points with value-based color: anything with coordinates. Draws circles, or icons by
|
|
90
|
+
* category (one image per category and level color), optionally clustered per group, with a
|
|
91
|
+
* highlight ring and check mark over selected features.
|
|
92
|
+
*/
|
|
93
|
+
export declare function pointsPlugin<P extends MapFeatureProperties>(options: PointsOptions<P>): MapPlugin;
|