@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.10

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +713 -2
  3. package/dist/core/basemap.svelte.d.ts +38 -0
  4. package/dist/core/basemap.svelte.js +163 -0
  5. package/dist/core/context.d.ts +5 -0
  6. package/dist/core/context.js +13 -0
  7. package/dist/core/create-map.svelte.d.ts +274 -0
  8. package/dist/core/create-map.svelte.js +591 -0
  9. package/dist/core/gl.d.ts +97 -0
  10. package/dist/core/gl.js +1 -0
  11. package/dist/core/interactions.svelte.d.ts +50 -0
  12. package/dist/core/interactions.svelte.js +330 -0
  13. package/dist/core/plugin-host.svelte.d.ts +71 -0
  14. package/dist/core/plugin-host.svelte.js +445 -0
  15. package/dist/core/plugin.d.ts +190 -0
  16. package/dist/core/plugin.js +1 -0
  17. package/dist/core/scoped-context.d.ts +7 -0
  18. package/dist/core/scoped-context.js +34 -0
  19. package/dist/core/shape-view.d.ts +27 -0
  20. package/dist/core/shape-view.js +323 -0
  21. package/dist/data/format.d.ts +10 -0
  22. package/dist/data/format.js +37 -0
  23. package/dist/data/index.d.ts +9 -0
  24. package/dist/data/index.js +9 -0
  25. package/dist/data/messages/en.json +61 -0
  26. package/dist/data/messages.d.ts +10 -0
  27. package/dist/data/messages.js +17 -0
  28. package/dist/data/metadata.d.ts +22 -0
  29. package/dist/data/metadata.js +110 -0
  30. package/dist/data/refs.d.ts +18 -0
  31. package/dist/data/refs.js +61 -0
  32. package/dist/data/scale-types.d.ts +7 -0
  33. package/dist/data/scale-types.js +1 -0
  34. package/dist/data/scale.d.ts +13 -0
  35. package/dist/data/scale.js +27 -0
  36. package/dist/data/selection.d.ts +13 -0
  37. package/dist/data/selection.js +52 -0
  38. package/dist/data/static-source.d.ts +4 -0
  39. package/dist/data/static-source.js +3 -0
  40. package/dist/data/types.d.ts +74 -0
  41. package/dist/data/types.js +1 -0
  42. package/dist/index.d.ts +59 -0
  43. package/dist/index.js +42 -0
  44. package/dist/plugins/icons.d.ts +21 -0
  45. package/dist/plugins/icons.js +73 -0
  46. package/dist/plugins/points-clusters.d.ts +60 -0
  47. package/dist/plugins/points-clusters.js +93 -0
  48. package/dist/plugins/points.svelte.d.ts +105 -0
  49. package/dist/plugins/points.svelte.js +502 -0
  50. package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
  51. package/dist/plugins/scale-resolution.svelte.js +111 -0
  52. package/dist/plugins/shapes.svelte.d.ts +34 -0
  53. package/dist/plugins/shapes.svelte.js +325 -0
  54. package/dist/plugins/shared.d.ts +6 -0
  55. package/dist/plugins/shared.js +18 -0
  56. package/dist/plugins/style.d.ts +57 -0
  57. package/dist/plugins/style.js +193 -0
  58. package/dist/presets/CollocationTooltip.svelte +132 -0
  59. package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
  60. package/dist/presets/FireTooltip.svelte +70 -0
  61. package/dist/presets/FireTooltip.svelte.d.ts +13 -0
  62. package/dist/presets/MonitorClusterTooltip.svelte +168 -0
  63. package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
  64. package/dist/presets/MonitorReadingBox.svelte +58 -0
  65. package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
  66. package/dist/presets/MonitorTooltip.svelte +88 -0
  67. package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
  68. package/dist/presets/calheatscore.d.ts +24 -0
  69. package/dist/presets/calheatscore.js +22 -0
  70. package/dist/presets/cluster-area.d.ts +13 -0
  71. package/dist/presets/cluster-area.js +34 -0
  72. package/dist/presets/collocation-sites.d.ts +18 -0
  73. package/dist/presets/collocation-sites.js +67 -0
  74. package/dist/presets/fire-groups.d.ts +26 -0
  75. package/dist/presets/fire-groups.js +51 -0
  76. package/dist/presets/forecast-zones.d.ts +25 -0
  77. package/dist/presets/forecast-zones.js +23 -0
  78. package/dist/presets/hms-fire.d.ts +30 -0
  79. package/dist/presets/hms-fire.js +86 -0
  80. package/dist/presets/hms-smoke.d.ts +20 -0
  81. package/dist/presets/hms-smoke.js +22 -0
  82. package/dist/presets/monitors.d.ts +48 -0
  83. package/dist/presets/monitors.js +243 -0
  84. package/dist/presets/reactive-props.svelte.d.ts +4 -0
  85. package/dist/presets/reactive-props.svelte.js +7 -0
  86. package/dist/presets/regions.d.ts +24 -0
  87. package/dist/presets/regions.js +22 -0
  88. package/dist/stores/abort.d.ts +16 -0
  89. package/dist/stores/abort.js +52 -0
  90. package/dist/stores/collocation-store.svelte.d.ts +25 -0
  91. package/dist/stores/collocation-store.svelte.js +72 -0
  92. package/dist/stores/hms-stores.svelte.d.ts +38 -0
  93. package/dist/stores/hms-stores.svelte.js +97 -0
  94. package/dist/stores/index.d.ts +4 -0
  95. package/dist/stores/index.js +4 -0
  96. package/dist/stores/load-once.svelte.d.ts +25 -0
  97. package/dist/stores/load-once.svelte.js +93 -0
  98. package/dist/stores/monitors-store.svelte.d.ts +85 -0
  99. package/dist/stores/monitors-store.svelte.js +340 -0
  100. package/dist/theme.css +34 -0
  101. package/dist/ui/FeatureList.svelte +501 -0
  102. package/dist/ui/FeatureList.svelte.d.ts +14 -0
  103. package/dist/ui/Legend.svelte +214 -0
  104. package/dist/ui/Legend.svelte.d.ts +22 -0
  105. package/dist/ui/LegendBar.svelte +161 -0
  106. package/dist/ui/LegendBar.svelte.d.ts +11 -0
  107. package/dist/ui/MapControls.svelte +135 -0
  108. package/dist/ui/MapControls.svelte.d.ts +11 -0
  109. package/dist/ui/MapTooltip.svelte +397 -0
  110. package/dist/ui/MapTooltip.svelte.d.ts +37 -0
  111. package/dist/ui/PickNotice.svelte +56 -0
  112. package/dist/ui/PickNotice.svelte.d.ts +7 -0
  113. package/dist/ui/SelectionBar.svelte +123 -0
  114. package/dist/ui/SelectionBar.svelte.d.ts +10 -0
  115. package/dist/ui/TooltipItem.svelte +66 -0
  116. package/dist/ui/TooltipItem.svelte.d.ts +11 -0
  117. package/dist/ui/ViewSummary.svelte +97 -0
  118. package/dist/ui/ViewSummary.svelte.d.ts +7 -0
  119. package/dist/ui/card.d.ts +29 -0
  120. package/dist/ui/card.js +1 -0
  121. package/dist/ui/controls.css +45 -0
  122. package/dist/ui/data-credits.svelte.d.ts +9 -0
  123. package/dist/ui/data-credits.svelte.js +52 -0
  124. package/dist/ui/feature-list.d.ts +46 -0
  125. package/dist/ui/feature-list.js +115 -0
  126. package/dist/ui/legend-data.d.ts +41 -0
  127. package/dist/ui/legend-data.js +40 -0
  128. package/dist/ui/level-chip.d.ts +11 -0
  129. package/dist/ui/level-chip.js +15 -0
  130. package/dist/ui/value-format.d.ts +8 -0
  131. package/dist/ui/value-format.js +20 -0
  132. package/dist/ui/view-summary.d.ts +11 -0
  133. package/dist/ui/view-summary.js +33 -0
  134. package/dist/view/MapView.svelte +443 -0
  135. package/dist/view/MapView.svelte.d.ts +51 -0
  136. package/dist/view/create-gl-map.d.ts +34 -0
  137. package/dist/view/create-gl-map.js +186 -0
  138. package/dist/view/list-placement.d.ts +21 -0
  139. package/dist/view/list-placement.js +13 -0
  140. package/package.json +104 -5
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import { formatValue } from "../ui/value-format.js";
3
+ import type { FeatureListItem } from "../core/plugin.js";
4
+ import type { FormatProvider } from "../data/format.js";
5
+ import type { MessageProvider } from "../data/messages.js";
6
+ import { NO_LEVEL, numericValue } from "../data/scale.js";
7
+ import type { Scale } from "../data/scale-types.js";
8
+ import MonitorReadingBox from "./MonitorReadingBox.svelte";
9
+
10
+ // Ported from monitor-map's MonitorTooltip: the reading in a level-colored box beside when it
11
+ // was taken, the monitor's name, and how long ago. The whole number is cut off, not rounded, as
12
+ // monitor-map's is; the level label is added for accessibility. The map tooltip leaves its own
13
+ // name header out.
14
+
15
+ interface Props {
16
+ item: FeatureListItem;
17
+ /** ISO 8601, or null when unknown. */
18
+ timestamp: string | null;
19
+ scale: Scale | null;
20
+ messages: MessageProvider;
21
+ format: FormatProvider;
22
+ /**
23
+ * How often the monitor's type reports (e.g. "1 hour"); null leaves the "(N avg)" note
24
+ * out.
25
+ */
26
+ interval: string | null;
27
+ }
28
+
29
+ let { item, timestamp, scale, messages, format, interval }: Props = $props();
30
+
31
+ // A level with an empty color shows its label in the neutral box.
32
+ const level = $derived(item.level !== NO_LEVEL ? item.level : null);
33
+ const reading = $derived.by(() => {
34
+ const n = numericValue(item.value);
35
+ return n === null ? null : formatValue(Math.trunc(n), 0, format);
36
+ });
37
+ const time = $derived.by(() => {
38
+ if (!timestamp || Number.isNaN(Date.parse(timestamp))) return null;
39
+ const formatted = format.dateTime(timestamp);
40
+ const zone = format.zoneLabel();
41
+ return {
42
+ at: zone ? messages("mapsdk_time_with_zone", { time: formatted, zone }) : formatted,
43
+ ago: messages("mapsdk_monitor_updated", { when: format.relativeTime(timestamp) })
44
+ };
45
+ });
46
+ </script>
47
+
48
+ <div class="mapsdk-monitor-tip mapsdk-tooltip-wide">
49
+ <MonitorReadingBox
50
+ color={level?.color || null}
51
+ header={scale?.label ?? ""}
52
+ value={reading ?? messages("mapsdk_no_reading")}
53
+ large={reading !== null}
54
+ subheading={interval ? messages("mapsdk_monitor_interval_avg", { interval }) : undefined}
55
+ />
56
+ <div class="mapsdk-monitor-detail">
57
+ {#if time}
58
+ <span>{time.at}</span>
59
+ {/if}
60
+ <span class="mapsdk-monitor-name text-lg">{item.name}</span>
61
+ <span>{level ? level.label : messages("mapsdk_no_level")}</span>
62
+ {#if time}
63
+ <span class="mapsdk-monitor-ago text-base">{time.ago}</span>
64
+ {/if}
65
+ </div>
66
+ </div>
67
+
68
+ <style>
69
+ .mapsdk-monitor-tip {
70
+ display: flex;
71
+ gap: 0.5rem;
72
+ }
73
+
74
+ .mapsdk-monitor-name {
75
+ font-weight: 700;
76
+ text-decoration: underline;
77
+ }
78
+
79
+ .mapsdk-monitor-ago {
80
+ margin-top: auto;
81
+ }
82
+
83
+ .mapsdk-monitor-detail {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 0.25rem;
87
+ }
88
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { FeatureListItem } from "../core/plugin.js";
2
+ import type { FormatProvider } from "../data/format.js";
3
+ import type { MessageProvider } from "../data/messages.js";
4
+ import type { Scale } from "../data/scale-types.js";
5
+ interface Props {
6
+ item: FeatureListItem;
7
+ /** ISO 8601, or null when unknown. */
8
+ timestamp: string | null;
9
+ scale: Scale | null;
10
+ messages: MessageProvider;
11
+ format: FormatProvider;
12
+ /**
13
+ * How often the monitor's type reports (e.g. "1 hour"); null leaves the "(N avg)" note
14
+ * out.
15
+ */
16
+ interval: string | null;
17
+ }
18
+ declare const MonitorTooltip: import("svelte").Component<Props, {}, "">;
19
+ type MonitorTooltip = ReturnType<typeof MonitorTooltip>;
20
+ export default MonitorTooltip;
@@ -0,0 +1,24 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { DataSource } from "../data/types.js";
3
+ export interface CalHeatScorePluginOptions {
4
+ /** Default "calheatscore". */
5
+ id?: string;
6
+ source: DataSource;
7
+ /** Already-translated layer name for the list and summary. */
8
+ layerLabel: string;
9
+ /** Default true. */
10
+ selectable?: boolean;
11
+ }
12
+ /**
13
+ * CalHeatScore ZIP code areas filled by their heat score.
14
+ *
15
+ * The source's `scale` is expected to be the ScaleRef
16
+ * `{ dataset: "calheatscore", entryType: "heat-score" }`.
17
+ *
18
+ * This preset does not fetch. The host's data source supplies the features, and each feature's
19
+ * `refKey` must already be `region:<type>:<external_id>`: a ZIP, `region:zipcode:<zip>`, e.g.
20
+ * `region:zipcode:93721` (`zipcode` is the server's region type name). Use the region's
21
+ * `external_id` (its code), never the server's `id` (a sqid), which is not a ref. A picked
22
+ * feature selects `{ kind: "regions", ids: ["<type>:<code>"] }`.
23
+ */
24
+ export declare function calHeatScorePlugin(options: CalHeatScorePluginOptions): MapPlugin;
@@ -0,0 +1,22 @@
1
+ import { shapesPlugin } from "../plugins/shapes.svelte.js";
2
+ /**
3
+ * CalHeatScore ZIP code areas filled by their heat score.
4
+ *
5
+ * The source's `scale` is expected to be the ScaleRef
6
+ * `{ dataset: "calheatscore", entryType: "heat-score" }`.
7
+ *
8
+ * This preset does not fetch. The host's data source supplies the features, and each feature's
9
+ * `refKey` must already be `region:<type>:<external_id>`: a ZIP, `region:zipcode:<zip>`, e.g.
10
+ * `region:zipcode:93721` (`zipcode` is the server's region type name). Use the region's
11
+ * `external_id` (its code), never the server's `id` (a sqid), which is not a ref. A picked
12
+ * feature selects `{ kind: "regions", ids: ["<type>:<code>"] }`.
13
+ */
14
+ export function calHeatScorePlugin(options) {
15
+ return shapesPlugin({
16
+ id: options.id ?? "calheatscore",
17
+ source: options.source,
18
+ layerLabel: options.layerLabel,
19
+ mode: "choropleth",
20
+ selectable: options.selectable ?? true
21
+ });
22
+ }
@@ -0,0 +1,13 @@
1
+ type Point = [number, number];
2
+ /**
3
+ * The convex hull of lng/lat points (monotone chain): counter-clockwise, without a repeated
4
+ * closing point, inner and collinear points dropped. Fewer than 3 points means no area.
5
+ */
6
+ export declare function convexHull(points: Point[]): Point[];
7
+ /**
8
+ * The area, in square miles, of the smallest shape around a cluster's monitors (their convex
9
+ * hull), as sjvair.com's map computed area: `@turf/area`, then `convertArea`. 0 for fewer than 3
10
+ * points not on one line.
11
+ */
12
+ export declare function clusterAreaSqMiles(points: Point[]): number;
13
+ export {};
@@ -0,0 +1,34 @@
1
+ import area from "@turf/area";
2
+ import { convertArea, polygon } from "@turf/helpers";
3
+ const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
4
+ /**
5
+ * The convex hull of lng/lat points (monotone chain): counter-clockwise, without a repeated
6
+ * closing point, inner and collinear points dropped. Fewer than 3 points means no area.
7
+ */
8
+ export function convexHull(points) {
9
+ const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
10
+ if (sorted.length < 3)
11
+ return sorted;
12
+ const half = (from) => {
13
+ const chain = [];
14
+ for (const p of from) {
15
+ while (chain.length >= 2 && cross(chain[chain.length - 2], chain[chain.length - 1], p) <= 0)
16
+ chain.pop();
17
+ chain.push(p);
18
+ }
19
+ chain.pop();
20
+ return chain;
21
+ };
22
+ return [...half(sorted), ...half([...sorted].reverse())];
23
+ }
24
+ /**
25
+ * The area, in square miles, of the smallest shape around a cluster's monitors (their convex
26
+ * hull), as sjvair.com's map computed area: `@turf/area`, then `convertArea`. 0 for fewer than 3
27
+ * points not on one line.
28
+ */
29
+ export function clusterAreaSqMiles(points) {
30
+ const hull = convexHull(points);
31
+ if (hull.length < 3)
32
+ return 0;
33
+ return convertArea(area(polygon([[...hull, hull[0]]])), "meters", "miles");
34
+ }
@@ -0,0 +1,18 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { CollocationFeatureProperties, DataSource } from "../data/types.js";
3
+ export declare const COLLOCATION_CROSSHAIR_COLOR = "#4A5FC6";
4
+ export interface CollocationSitesPluginOptions {
5
+ /** Default "collocation-sites". */
6
+ id?: string;
7
+ /** Usually `createCollocationSitesStore(…).dataSource`. */
8
+ source: DataSource<CollocationFeatureProperties>;
9
+ /** Already-translated layer name for the list and summary. */
10
+ layerLabel: string;
11
+ }
12
+ /**
13
+ * Collocation sites as on today's live map: the reference monitor's marker in a crosshair, with
14
+ * a tooltip of the reference and collocated monitors' readings side by side. Register it before
15
+ * `monitorsPlugin` so it draws under the monitors; its tooltip still wins the hover. Not
16
+ * selectable.
17
+ */
18
+ export declare function collocationSitesPlugin(options: CollocationSitesPluginOptions): MapPlugin;
@@ -0,0 +1,67 @@
1
+ import { CROSSHAIR_ICON_SIZE, withCrosshair } from "../plugins/icons.js";
2
+ import { pointsPlugin } from "../plugins/points.svelte.js";
3
+ import CollocationTooltip from "./CollocationTooltip.svelte";
4
+ import { componentSnippet, intervalLookup, MONITOR_INACTIVE_COLOR, MONITOR_INSIDE_OUTLINE, monitorShape, SHAPES } from "./monitors.js";
5
+ // Ported from monitor-map 4.3 (src/lib/collocation-sites/): each site drawn at its reference
6
+ // monitor, in that monitor's shape and level color, wrapped in a crosshair.
7
+ // The user's decision (2026-11-04), an exception to the theme-token rule: the crosshair is this
8
+ // blue on every map and theme, as on sjvair.com.
9
+ export const COLLOCATION_CROSSHAIR_COLOR = "#4A5FC6";
10
+ const BORDER_WIDTH = 2;
11
+ /** `shape:location`, e.g. "triangle:inside", as the monitors preset's icons. */
12
+ function category(props) {
13
+ return `${monitorShape(props.type)}:${props.location === "inside" ? "inside" : "outside"}`;
14
+ }
15
+ /**
16
+ * Collocation sites as on today's live map: the reference monitor's marker in a crosshair, with
17
+ * a tooltip of the reference and collocated monitors' readings side by side. Register it before
18
+ * `monitorsPlugin` so it draws under the monitors; its tooltip still wins the hover. Not
19
+ * selectable.
20
+ */
21
+ export function collocationSitesPlugin(options) {
22
+ const intervalFor = intervalLookup();
23
+ const plugin = pointsPlugin({
24
+ id: options.id ?? "collocation-sites",
25
+ source: options.source,
26
+ layerLabel: options.layerLabel,
27
+ selectable: false,
28
+ icon: {
29
+ category,
30
+ draw(cat, color) {
31
+ const [shape, location] = cat.split(":");
32
+ const marker = SHAPES[shape](color, BORDER_WIDTH, location === "inside" ? MONITOR_INSIDE_OUTLINE : undefined);
33
+ return withCrosshair(marker, COLLOCATION_CROSSHAIR_COLOR);
34
+ },
35
+ size: CROSSHAIR_ICON_SIZE,
36
+ muted: (props) => !props.isActive,
37
+ mutedColor: MONITOR_INACTIVE_COLOR
38
+ },
39
+ tooltipShowsName: true,
40
+ tooltip: (_item, props, env) => componentSnippet(CollocationTooltip, {
41
+ site: props,
42
+ scale: env.scale,
43
+ messages: env.messages,
44
+ format: env.format,
45
+ referenceInterval: null,
46
+ colocatedInterval: null
47
+ }, Promise.all([
48
+ intervalFor(props.type, env.metadata),
49
+ props.colocatedType
50
+ ? intervalFor(props.colocatedType, env.metadata)
51
+ : Promise.resolve(null)
52
+ ]).then(([referenceInterval, colocatedInterval]) => ({
53
+ referenceInterval,
54
+ colocatedInterval
55
+ })))
56
+ });
57
+ // monitor-map registers the collocation tooltip at the highest priority, over the monitors.
58
+ plugin.hoverPriority = Number.MAX_SAFE_INTEGER;
59
+ // The sites use the monitors' scale, which the monitors' own section shows; only a loading
60
+ // or failure status still reports here.
61
+ const legend = plugin.legend;
62
+ plugin.legend = () => {
63
+ const data = legend?.() ?? null;
64
+ return data && "scale" in data ? null : data;
65
+ };
66
+ return plugin;
67
+ }
@@ -0,0 +1,26 @@
1
+ export interface FireGroup {
2
+ id: string;
3
+ /** Mean [lng, lat] of the group's fires. */
4
+ coordinates: [number, number];
5
+ /** Mean fire radiative power of the group's fires. */
6
+ avgFrp: number;
7
+ count: number;
8
+ }
9
+ export interface FireDetection {
10
+ id: string;
11
+ coordinates: [number, number];
12
+ /** Fire radiative power; a fire without a finite one is skipped. */
13
+ frp: number | null;
14
+ /** ISO 8601. Of several fires at the same coordinates, the latest is kept (else the first). */
15
+ timestamp?: string;
16
+ }
17
+ /**
18
+ * Drops fires without a finite FRP, then keeps one fire per exact coordinates: the latest by
19
+ * timestamp, else the first listed. Dropping first means a newer fire with no FRP hides nothing.
20
+ */
21
+ export declare function cleanFires<F extends FireDetection>(fires: F[]): F[];
22
+ /**
23
+ * Groups nearby fires (DBSCAN, 2 km, every fire its own seed) into one point per group, after
24
+ * skipping fires without a finite FRP and keeping one fire per coordinates, as monitor-map does.
25
+ */
26
+ export declare function groupFires(fires: FireDetection[]): FireGroup[];
@@ -0,0 +1,51 @@
1
+ import clustersDbscan from "@turf/clusters-dbscan";
2
+ import { featureCollection, point } from "@turf/helpers";
3
+ // Ported from monitor-map 4.3.0 (src/lib/hms/hms.svelte.ts, computeFireGroups).
4
+ /** Fires within this distance (km) of each other share a group. */
5
+ const EPSILON_KM = 2;
6
+ const MIN_POINTS = 1;
7
+ const mean = (values) => values.reduce((s, v) => s + v, 0) / values.length;
8
+ /**
9
+ * Drops fires without a finite FRP, then keeps one fire per exact coordinates: the latest by
10
+ * timestamp, else the first listed. Dropping first means a newer fire with no FRP hides nothing.
11
+ */
12
+ export function cleanFires(fires) {
13
+ const latest = new Map();
14
+ for (const fire of fires) {
15
+ if (!Number.isFinite(fire.frp))
16
+ continue;
17
+ const key = `${fire.coordinates[0]},${fire.coordinates[1]}`;
18
+ const existing = latest.get(key);
19
+ if (!existing || (fire.timestamp ?? "") > (existing.timestamp ?? ""))
20
+ latest.set(key, fire);
21
+ }
22
+ return [...latest.values()];
23
+ }
24
+ /**
25
+ * Groups nearby fires (DBSCAN, 2 km, every fire its own seed) into one point per group, after
26
+ * skipping fires without a finite FRP and keeping one fire per coordinates, as monitor-map does.
27
+ */
28
+ export function groupFires(fires) {
29
+ const usable = cleanFires(fires);
30
+ if (!usable.length)
31
+ return [];
32
+ const clustered = clustersDbscan(featureCollection(usable.map((f) => point(f.coordinates, { fireId: f.id, frp: f.frp }))), EPSILON_KM, { minPoints: MIN_POINTS, units: "kilometers" });
33
+ const groups = new Map();
34
+ for (const feature of clustered.features) {
35
+ const props = feature.properties;
36
+ // With minPoints 1 every fire is a core point, so "noise" never occurs.
37
+ const key = props.dbscan === "noise" ? `noise-${props.fireId}` : `cluster-${props.cluster}`;
38
+ const [lng, lat] = feature.geometry.coordinates;
39
+ const entry = groups.get(key) ?? { frps: [], lngs: [], lats: [] };
40
+ entry.frps.push(props.frp);
41
+ entry.lngs.push(lng);
42
+ entry.lats.push(lat);
43
+ groups.set(key, entry);
44
+ }
45
+ return [...groups].map(([key, { frps, lngs, lats }]) => ({
46
+ id: `hms-fire-group-${key}`,
47
+ coordinates: [mean(lngs), mean(lats)],
48
+ avgFrp: mean(frps),
49
+ count: frps.length
50
+ }));
51
+ }
@@ -0,0 +1,25 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { DataSource } from "../data/types.js";
3
+ export interface ForecastZonesPluginOptions {
4
+ /** Default "forecast-zones". */
5
+ id?: string;
6
+ source: DataSource;
7
+ /** Already-translated layer name for the list and summary. */
8
+ layerLabel: string;
9
+ /** Default true. */
10
+ selectable?: boolean;
11
+ }
12
+ /**
13
+ * Air quality forecast zones filled by the forecast's AQI level.
14
+ *
15
+ * The source's `scale` is expected to be the ScaleRef
16
+ * `{ dataset: "forecasts", entryType: "aqi-forecast" }`.
17
+ *
18
+ * This preset does not fetch. The host's data source supplies the features, and each feature's
19
+ * `refKey` must already be `region:<type>:<external_id>`: a county region
20
+ * (`region:county:<code>`) for most zones and a `custom` region for the rest (e.g.
21
+ * `region:custom:sjvapcd-kern-airbasin`). Use the region's `external_id` (its code), never the
22
+ * server's `id` (a sqid), which is not a ref. A picked feature selects
23
+ * `{ kind: "regions", ids: ["<type>:<code>"] }`.
24
+ */
25
+ export declare function forecastZonesPlugin(options: ForecastZonesPluginOptions): MapPlugin;
@@ -0,0 +1,23 @@
1
+ import { shapesPlugin } from "../plugins/shapes.svelte.js";
2
+ /**
3
+ * Air quality forecast zones filled by the forecast's AQI level.
4
+ *
5
+ * The source's `scale` is expected to be the ScaleRef
6
+ * `{ dataset: "forecasts", entryType: "aqi-forecast" }`.
7
+ *
8
+ * This preset does not fetch. The host's data source supplies the features, and each feature's
9
+ * `refKey` must already be `region:<type>:<external_id>`: a county region
10
+ * (`region:county:<code>`) for most zones and a `custom` region for the rest (e.g.
11
+ * `region:custom:sjvapcd-kern-airbasin`). Use the region's `external_id` (its code), never the
12
+ * server's `id` (a sqid), which is not a ref. A picked feature selects
13
+ * `{ kind: "regions", ids: ["<type>:<code>"] }`.
14
+ */
15
+ export function forecastZonesPlugin(options) {
16
+ return shapesPlugin({
17
+ id: options.id ?? "forecast-zones",
18
+ source: options.source,
19
+ layerLabel: options.layerLabel,
20
+ mode: "choropleth",
21
+ selectable: options.selectable ?? true
22
+ });
23
+ }
@@ -0,0 +1,30 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { DataSource, FireFeatureProperties } from "../data/types.js";
3
+ export interface HmsFirePluginOptions {
4
+ /** Default "hms-fire". */
5
+ id?: string;
6
+ /**
7
+ * The fires, or with `hotspots` the groups shown below zoom 10 (listed). A feature with a
8
+ * `count` is a group (its tooltip says "Average" from 2 detections); one without is a single
9
+ * detection.
10
+ */
11
+ source: DataSource<FireFeatureProperties>;
12
+ /** Single detections shown from zoom 10 instead of `source` (not listed). Default: none. */
13
+ hotspots?: DataSource<FireFeatureProperties>;
14
+ /** Already-translated layer name for the list and summary. */
15
+ layerLabel: string;
16
+ }
17
+ /**
18
+ * NOAA HMS fire detections as flame icons colored by fire radiative power (MW) and growing with
19
+ * its level. Not selectable; `source`'s fires are listed.
20
+ *
21
+ * With `hotspots`, it is still one layer (one legend section, one list) that switches by itself
22
+ * while zooming: `source` (groups from `groupFires`, value = `avgFrp`) below zoom 10, `hotspots`
23
+ * (each detection) from zoom 10. Both are hit-tested and show the same tooltip: the power in a
24
+ * reading box colored by its level, beside the name and level; only the groups are listed. Values
25
+ * are cut off at the display precision, not rounded, in the tooltip, list, and accessible names.
26
+ *
27
+ * The sources' `scale` is expected to be the ScaleRef `{ dataset: "hms-fire", entryType:
28
+ * "fire-frp" }`, and each feature's `value` its FRP. This preset does not fetch.
29
+ */
30
+ export declare function hmsFirePlugin(options: HmsFirePluginOptions): MapPlugin;
@@ -0,0 +1,86 @@
1
+ import { scopedContext } from "../core/scoped-context.js";
2
+ import { fireIcon } from "../plugins/icons.js";
3
+ import { pointsPlugin } from "../plugins/points.svelte.js";
4
+ import FireTooltip from "./FireTooltip.svelte";
5
+ import { componentSnippet } from "./monitors.js";
6
+ /** The zoom where `hotspots` replace the groups (inclusive), as sjvair.com's map does. */
7
+ const HOTSPOT_ZOOM = 10;
8
+ /** Icon size in pixels by level index, smallest to largest (presentation). */
9
+ const SIZES = [20, 24, 28, 32, 36];
10
+ const CANVAS = SIZES[SIZES.length - 1];
11
+ /** The fire glyph at the size for a level index, centered on a fixed canvas. */
12
+ function drawFire(color, levelIndex = -1) {
13
+ const size = SIZES[Math.min(Math.max(levelIndex, 0), SIZES.length - 1)];
14
+ const offset = (CANVAS - size) / 2;
15
+ return `<svg width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}" xmlns="http://www.w3.org/2000/svg"><g transform="translate(${offset} ${offset})">${fireIcon(color, size)}</g></svg>`;
16
+ }
17
+ function firePoints(id, source, layerLabel, zoomRange) {
18
+ return pointsPlugin({
19
+ id,
20
+ source,
21
+ layerLabel,
22
+ selectable: false,
23
+ zoomRange,
24
+ legendDisplay: "blocks",
25
+ truncateValues: true,
26
+ tooltipShowsName: true,
27
+ tooltip: (item, props, env) => componentSnippet(FireTooltip, {
28
+ item,
29
+ count: typeof props.count === "number" ? props.count : null,
30
+ messages: env.messages,
31
+ format: env.format
32
+ }),
33
+ icon: {
34
+ category: () => "fire",
35
+ draw: (_category, color, _theme, levelIndex) => drawFire(color, levelIndex),
36
+ size: CANVAS
37
+ }
38
+ });
39
+ }
40
+ /**
41
+ * NOAA HMS fire detections as flame icons colored by fire radiative power (MW) and growing with
42
+ * its level. Not selectable; `source`'s fires are listed.
43
+ *
44
+ * With `hotspots`, it is still one layer (one legend section, one list) that switches by itself
45
+ * while zooming: `source` (groups from `groupFires`, value = `avgFrp`) below zoom 10, `hotspots`
46
+ * (each detection) from zoom 10. Both are hit-tested and show the same tooltip: the power in a
47
+ * reading box colored by its level, beside the name and level; only the groups are listed. Values
48
+ * are cut off at the display precision, not rounded, in the tooltip, list, and accessible names.
49
+ *
50
+ * The sources' `scale` is expected to be the ScaleRef `{ dataset: "hms-fire", entryType:
51
+ * "fire-frp" }`, and each feature's `value` its FRP. This preset does not fetch.
52
+ */
53
+ export function hmsFirePlugin(options) {
54
+ const id = options.id ?? "hms-fire";
55
+ const { source, hotspots, layerLabel } = options;
56
+ if (!hotspots)
57
+ return firePoints(id, source, layerLabel);
58
+ // Both inner plugins share the outer id, so their hits are this plugin's.
59
+ const groups = firePoints(id, source, layerLabel, { max: HOTSPOT_ZOOM });
60
+ const singles = firePoints(id, hotspots, layerLabel, { min: HOTSPOT_ZOOM });
61
+ return {
62
+ id,
63
+ label: layerLabel,
64
+ get dataset() {
65
+ return groups.dataset;
66
+ },
67
+ selectable: false,
68
+ setup(ctx) {
69
+ const teardownGroups = groups.setup(scopedContext(ctx, "groups:"));
70
+ const teardownSingles = singles.setup(scopedContext(ctx, "hotspots:"));
71
+ return () => {
72
+ teardownSingles();
73
+ teardownGroups();
74
+ };
75
+ },
76
+ legend: groups.legend,
77
+ // Only the groups are listed; a hovered hotspot shows the same box as a group.
78
+ features: groups.features,
79
+ tooltip: groups.tooltip,
80
+ tooltipShowsName: true,
81
+ hoverTooltip(hit) {
82
+ const item = singles.features?.().find((f) => f.refKey === hit.refKey);
83
+ return item && singles.tooltip?.(item);
84
+ }
85
+ };
86
+ }
@@ -0,0 +1,20 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { DataSource } from "../data/types.js";
3
+ export interface HmsSmokePluginOptions {
4
+ /** Default "hms-smoke". */
5
+ id?: string;
6
+ source: DataSource;
7
+ /** Already-translated layer name for the legend and summary. */
8
+ layerLabel: string;
9
+ }
10
+ /**
11
+ * NOAA HMS smoke plumes: polygons filled and outlined (`outline: "level"`) by density, using each
12
+ * level's opacity. Not selectable; listed (each plume a row with its density level), so plumes
13
+ * can be reached without a mouse. Hovering a plume shows no tooltip and never hides another
14
+ * layer's (`hoverable: false`); focusing one from the list still pins its tooltip.
15
+ *
16
+ * The source's `scale` is expected to be the ScaleRef
17
+ * `{ dataset: "hms-smoke", entryType: "smoke-density" }`, and each feature's `value` its density
18
+ * key ("light", "medium", "heavy"). This preset does not fetch.
19
+ */
20
+ export declare function hmsSmokePlugin(options: HmsSmokePluginOptions): MapPlugin;
@@ -0,0 +1,22 @@
1
+ import { shapesPlugin } from "../plugins/shapes.svelte.js";
2
+ /**
3
+ * NOAA HMS smoke plumes: polygons filled and outlined (`outline: "level"`) by density, using each
4
+ * level's opacity. Not selectable; listed (each plume a row with its density level), so plumes
5
+ * can be reached without a mouse. Hovering a plume shows no tooltip and never hides another
6
+ * layer's (`hoverable: false`); focusing one from the list still pins its tooltip.
7
+ *
8
+ * The source's `scale` is expected to be the ScaleRef
9
+ * `{ dataset: "hms-smoke", entryType: "smoke-density" }`, and each feature's `value` its density
10
+ * key ("light", "medium", "heavy"). This preset does not fetch.
11
+ */
12
+ export function hmsSmokePlugin(options) {
13
+ return shapesPlugin({
14
+ id: options.id ?? "hms-smoke",
15
+ source: options.source,
16
+ layerLabel: options.layerLabel,
17
+ mode: "choropleth",
18
+ selectable: false,
19
+ outline: "level",
20
+ hoverable: false
21
+ });
22
+ }
@@ -0,0 +1,48 @@
1
+ import { type Component, type Snippet } from "svelte";
2
+ import type { MapPlugin } from "../core/plugin.js";
3
+ import type { MetadataProvider } from "../data/metadata.js";
4
+ import type { DataSource, MonitorFeatureProperties } from "../data/types.js";
5
+ import { circle, square, triangle } from "../plugins/icons.js";
6
+ export declare const MONITOR_INACTIVE_COLOR = "#969696";
7
+ export declare const MONITOR_INSIDE_OUTLINE = "#000000";
8
+ export interface MonitorsPluginOptions {
9
+ /** Default "monitors". */
10
+ id?: string;
11
+ source: DataSource<MonitorFeatureProperties>;
12
+ /** Already-translated layer name for the list and summary. */
13
+ layerLabel: string;
14
+ /** Default true, like the shape presets. */
15
+ selectable?: boolean;
16
+ /** "ring" (default): ring and check mark. "enlarge": the selected icon draws at 1.3x. */
17
+ selectedStyle?: "ring" | "enlarge";
18
+ /** Cluster each monitor type below zoom 9. Default true. */
19
+ clustered?: boolean;
20
+ }
21
+ export type MonitorShape = "circle" | "square" | "triangle";
22
+ /** The marker shape for a monitor type: reference monitors are triangles, sensors circles. */
23
+ export declare function monitorShape(type: string): MonitorShape;
24
+ /** The marker drawing for each shape. */
25
+ export declare const SHAPES: {
26
+ circle: typeof circle;
27
+ square: typeof square;
28
+ triangle: typeof triangle;
29
+ };
30
+ /**
31
+ * A snippet that mounts `component` with `props` (a tooltip outside the map's component tree).
32
+ * It mounts at once; `late`, when given, updates the props once it resolves (e.g. a note that
33
+ * needs a lookup), so a slow lookup never leaves an empty card. Nothing is mounted or updated
34
+ * after the tooltip closes, and a failure to mount or update is logged, not thrown.
35
+ */
36
+ export declare function componentSnippet<Props extends Record<string, any>>(component: Component<Props>, props: Props, late?: Promise<Partial<Props>>): Snippet;
37
+ /**
38
+ * Looks up a monitor type's reporting interval (e.g. "1 hour") for tooltips' "(N avg)" note,
39
+ * once per type; null when the provider has none. Failures (a rejection or a synchronous throw)
40
+ * give null and are forgotten, so they're retried.
41
+ */
42
+ export declare function intervalLookup(): (type: string, metadata: MetadataProvider) => Promise<string | null>;
43
+ /**
44
+ * Air-quality monitors as on today's live map: a shape per monitor type filled with its level's
45
+ * color, inactive monitors in the neutral color, inside monitors outlined, and per-type clusters
46
+ * colored by their average that zoom in when clicked.
47
+ */
48
+ export declare function monitorsPlugin(options: MonitorsPluginOptions): MapPlugin;