@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,243 @@
1
+ import { createRawSnippet, mount, unmount } from "svelte";
2
+ import { circle, square, triangle } from "../plugins/icons.js";
3
+ import { pointsPlugin } from "../plugins/points.svelte.js";
4
+ import { hasNumericValue, levelKeyExpression, levelLookup, MISSING_VALUE } from "../plugins/style.js";
5
+ import { levelChip } from "../ui/level-chip.js";
6
+ import { reactiveProps } from "./reactive-props.svelte.js";
7
+ import MonitorClusterTooltip from "./MonitorClusterTooltip.svelte";
8
+ import MonitorTooltip from "./MonitorTooltip.svelte";
9
+ // Ported from monitor-map 4.3.0 (src/lib/monitors/): shapes, draw order, icon variants, and
10
+ // cluster rendering. Shapes, sizes, and order are presentation; colors come from the scale and
11
+ // the theme, except the two fixed monitor colors below.
12
+ // The user's decision (2026-11-04), an exception to the theme-token rule: inactive monitors are
13
+ // this gray and inside monitors have a black outline, on every map and theme, as on sjvair.com.
14
+ export const MONITOR_INACTIVE_COLOR = "#969696";
15
+ export const MONITOR_INSIDE_OUTLINE = "#000000";
16
+ /** The marker shape for a monitor type: reference monitors are triangles, sensors circles. */
17
+ export function monitorShape(type) {
18
+ switch (type) {
19
+ case "airnow":
20
+ case "aqview":
21
+ case "bam1022":
22
+ case "aqlite":
23
+ return "triangle";
24
+ default:
25
+ return "circle";
26
+ }
27
+ }
28
+ /** monitor-map's draw order (`getOrder`): higher draws on top. Unknown types draw lowest. */
29
+ function monitorOrder(props) {
30
+ switch (props.type) {
31
+ case "airgradient":
32
+ return 5;
33
+ case "airnow":
34
+ return 2;
35
+ case "aqview":
36
+ return 3;
37
+ case "bam1022":
38
+ return 6;
39
+ case "purpleair":
40
+ return props.isSjvair ? 4 : 1;
41
+ case "vozbox":
42
+ return 7;
43
+ case "aqlite":
44
+ return 8;
45
+ default:
46
+ return 0;
47
+ }
48
+ }
49
+ /** The marker drawing for each shape. */
50
+ export const SHAPES = { circle, square, triangle };
51
+ const ICON_SIZE = 24;
52
+ const BORDER_WIDTH = 2;
53
+ const CLUSTER_RADIUS = 40;
54
+ const CLUSTER_MAX_ZOOM = 9;
55
+ const CLUSTER_OPACITY = 0.8;
56
+ /** Cluster icons grow with the count: 1.3×, then 1.6× from 10, then 2× from 25. */
57
+ const CLUSTER_SIZES = [1.3, 10, 1.6, 25, 2.0];
58
+ /** `shape:location`, e.g. "triangle:inside". */
59
+ function category(props) {
60
+ return `${monitorShape(props.type)}:${props.location === "inside" ? "inside" : "outside"}`;
61
+ }
62
+ /** Readings by `numericValue`'s rule (numbers and numeric strings), as levels and tooltips count. */
63
+ const IS_READING = hasNumericValue("value");
64
+ const COUNT = ["get", "countValues"];
65
+ const MEAN = ["/", ["get", "sumValues"], ["max", COUNT, 1]];
66
+ /** The cluster's average reading, or MISSING_VALUE when none of its monitors has one. */
67
+ const AVERAGE = ["case", [">", COUNT, 0], MEAN, MISSING_VALUE];
68
+ /** Readable text on `color`, or `fallback` for an unparseable color. */
69
+ function readable(color, fallback) {
70
+ return levelChip(color)?.text ?? fallback;
71
+ }
72
+ /** A type's cluster: its shape colored by the average's level, with the rounded average on it. */
73
+ function clusterLayers({ group, sourceId, scale, theme, iconImage, iconSizeScale }) {
74
+ const filter = ["has", "point_count"];
75
+ const sizes = CLUSTER_SIZES.map((n, i) => (i % 2 === 0 ? n * iconSizeScale : n));
76
+ return [
77
+ {
78
+ local: "cluster-icon",
79
+ spec: {
80
+ type: "symbol",
81
+ source: sourceId,
82
+ filter,
83
+ layout: {
84
+ "icon-image": iconImage(`${monitorShape(group)}:outside`, levelKeyExpression(scale, AVERAGE)),
85
+ "icon-size": [
86
+ "step",
87
+ ["get", "point_count"],
88
+ ...sizes
89
+ ],
90
+ "icon-ignore-placement": true,
91
+ "icon-allow-overlap": true
92
+ },
93
+ paint: { "icon-opacity": CLUSTER_OPACITY }
94
+ }
95
+ },
96
+ {
97
+ local: "cluster-average",
98
+ spec: {
99
+ type: "symbol",
100
+ source: sourceId,
101
+ filter,
102
+ layout: {
103
+ "text-field": [
104
+ "case",
105
+ [">", COUNT, 0],
106
+ ["to-string", ["round", MEAN]],
107
+ ""
108
+ ],
109
+ "text-size": 12,
110
+ "text-ignore-placement": true,
111
+ "text-allow-overlap": true
112
+ },
113
+ paint: {
114
+ "text-color": levelLookup(scale, AVERAGE, readable(theme.neutral, theme.foreground), (l) => readable(l.color || theme.neutral, theme.foreground))
115
+ }
116
+ }
117
+ }
118
+ ];
119
+ }
120
+ /**
121
+ * A snippet that mounts `component` with `props` (a tooltip outside the map's component tree).
122
+ * It mounts at once; `late`, when given, updates the props once it resolves (e.g. a note that
123
+ * needs a lookup), so a slow lookup never leaves an empty card. Nothing is mounted or updated
124
+ * after the tooltip closes, and a failure to mount or update is logged, not thrown.
125
+ */
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ export function componentSnippet(component, props, late) {
128
+ return createRawSnippet(() => ({
129
+ render: () => "<div></div>",
130
+ setup(target) {
131
+ let instance;
132
+ let closed = false;
133
+ const live = reactiveProps(props);
134
+ try {
135
+ instance = mount(component, { target, props: live });
136
+ }
137
+ catch (error) {
138
+ console.error("map-sdk: a tooltip failed to mount", error);
139
+ }
140
+ void late?.then((extra) => {
141
+ if (closed || !instance)
142
+ return;
143
+ try {
144
+ Object.assign(live, extra);
145
+ }
146
+ catch (error) {
147
+ console.error("map-sdk: a tooltip failed to update", error);
148
+ }
149
+ });
150
+ return () => {
151
+ closed = true;
152
+ if (instance)
153
+ void unmount(instance);
154
+ };
155
+ }
156
+ }));
157
+ }
158
+ /**
159
+ * Looks up a monitor type's reporting interval (e.g. "1 hour") for tooltips' "(N avg)" note,
160
+ * once per type; null when the provider has none. Failures (a rejection or a synchronous throw)
161
+ * give null and are forgotten, so they're retried.
162
+ */
163
+ export function intervalLookup() {
164
+ const intervals = new Map();
165
+ return function intervalFor(type, metadata) {
166
+ if (!metadata.monitorTypeInterval)
167
+ return Promise.resolve(null);
168
+ let found = intervals.get(type);
169
+ if (!found) {
170
+ let call;
171
+ try {
172
+ call = Promise.resolve(metadata.monitorTypeInterval(type));
173
+ }
174
+ catch {
175
+ return Promise.resolve(null);
176
+ }
177
+ const lookup = call.then((value) => (value === "unavailable" ? null : value), () => null);
178
+ found = lookup;
179
+ intervals.set(type, lookup);
180
+ void lookup.then((value) => {
181
+ if (value === null && intervals.get(type) === lookup)
182
+ intervals.delete(type);
183
+ });
184
+ }
185
+ return found;
186
+ };
187
+ }
188
+ /**
189
+ * Air-quality monitors as on today's live map: a shape per monitor type filled with its level's
190
+ * color, inactive monitors in the neutral color, inside monitors outlined, and per-type clusters
191
+ * colored by their average that zoom in when clicked.
192
+ */
193
+ export function monitorsPlugin(options) {
194
+ const clustered = options.clustered ?? true;
195
+ const intervalFor = intervalLookup();
196
+ return pointsPlugin({
197
+ id: options.id ?? "monitors",
198
+ source: options.source,
199
+ layerLabel: options.layerLabel,
200
+ legendTitle: "entry-type",
201
+ selectable: options.selectable ?? true,
202
+ selectedStyle: options.selectedStyle,
203
+ icon: {
204
+ category,
205
+ draw(cat, color) {
206
+ const [shape, location] = cat.split(":");
207
+ return SHAPES[shape](color, BORDER_WIDTH, location === "inside" ? MONITOR_INSIDE_OUTLINE : undefined);
208
+ },
209
+ size: ICON_SIZE,
210
+ muted: (props) => !props.isActive,
211
+ mutedColor: MONITOR_INACTIVE_COLOR
212
+ },
213
+ sortKey: monitorOrder,
214
+ cluster: clustered
215
+ ? {
216
+ radius: CLUSTER_RADIUS,
217
+ maxZoom: CLUSTER_MAX_ZOOM,
218
+ groupBy: (props) => props.type,
219
+ properties: {
220
+ sumValues: ["+", ["case", IS_READING, ["to-number", ["get", "value"], 0], 0]],
221
+ countValues: ["+", ["case", IS_READING, 1, 0]]
222
+ },
223
+ layers: clusterLayers,
224
+ tooltip: (cluster, env) => componentSnippet(MonitorClusterTooltip, {
225
+ cluster,
226
+ scale: env.scale,
227
+ messages: env.messages,
228
+ format: env.format,
229
+ metadata: env.metadata
230
+ })
231
+ }
232
+ : undefined,
233
+ tooltipShowsName: true,
234
+ tooltip: (item, props, env) => componentSnippet(MonitorTooltip, {
235
+ item,
236
+ timestamp: props.timestamp ?? null,
237
+ scale: env.scale,
238
+ messages: env.messages,
239
+ format: env.format,
240
+ interval: null
241
+ }, intervalFor(props.type, env.metadata).then((interval) => ({ interval })))
242
+ });
243
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Props for a component mounted outside the map's tree that can be updated by assigning to them.
3
+ */
4
+ export declare function reactiveProps<T extends object>(initial: T): T;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Props for a component mounted outside the map's tree that can be updated by assigning to them.
3
+ */
4
+ export function reactiveProps(initial) {
5
+ const props = $state(initial);
6
+ return props;
7
+ }
@@ -0,0 +1,24 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { DataSource } from "../data/types.js";
3
+ export interface RegionsPluginOptions {
4
+ /** Default "regions". */
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
+ * Region polygons (counties, cities, and other server regions) filled by a pollutant's level.
14
+ *
15
+ * The source's `scale` is expected to be the ScaleRef
16
+ * `{ dataset: "air-quality", entryType: <pollutant> }` (e.g. `pm25`).
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 region of any type, e.g.
20
+ * `region:county:fresno`. Use the region's `external_id` (its code), never the server's `id` (a
21
+ * sqid), which is not a ref. A picked feature selects
22
+ * `{ kind: "regions", ids: ["<type>:<code>"] }`.
23
+ */
24
+ export declare function regionsPlugin(options: RegionsPluginOptions): MapPlugin;
@@ -0,0 +1,22 @@
1
+ import { shapesPlugin } from "../plugins/shapes.svelte.js";
2
+ /**
3
+ * Region polygons (counties, cities, and other server regions) filled by a pollutant's level.
4
+ *
5
+ * The source's `scale` is expected to be the ScaleRef
6
+ * `{ dataset: "air-quality", entryType: <pollutant> }` (e.g. `pm25`).
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 region of any type, e.g.
10
+ * `region:county:fresno`. Use the region's `external_id` (its code), never the server's `id` (a
11
+ * sqid), which is not a ref. A picked feature selects
12
+ * `{ kind: "regions", ids: ["<type>:<code>"] }`.
13
+ */
14
+ export function regionsPlugin(options) {
15
+ return shapesPlugin({
16
+ id: options.id ?? "regions",
17
+ source: options.source,
18
+ layerLabel: options.layerLabel,
19
+ mode: "choropleth",
20
+ selectable: options.selectable ?? true
21
+ });
22
+ }
@@ -0,0 +1,16 @@
1
+ export type SignalOptions = {
2
+ signal?: AbortSignal;
3
+ };
4
+ /**
5
+ * A signal that aborts as soon as any input does, with that input's reason. Already aborted if
6
+ * any input is. Undefined inputs are ignored. Call `dispose()` to drop the listeners.
7
+ */
8
+ export declare function anySignal(...signals: Array<AbortSignal | undefined>): {
9
+ signal: AbortSignal;
10
+ dispose(): void;
11
+ };
12
+ /**
13
+ * Settles like `promise`, unless `signal` aborts first, in which case it rejects with
14
+ * `signal.reason`. The inner promise's later outcome is swallowed.
15
+ */
16
+ export declare function untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
@@ -0,0 +1,52 @@
1
+ // Ported from monitor-map (src/lib/abort.ts): the building blocks for cancelling requests.
2
+ /**
3
+ * A signal that aborts as soon as any input does, with that input's reason. Already aborted if
4
+ * any input is. Undefined inputs are ignored. Call `dispose()` to drop the listeners.
5
+ */
6
+ export function anySignal(...signals) {
7
+ const controller = new AbortController();
8
+ const cleanups = [];
9
+ const dispose = () => {
10
+ for (const cleanup of cleanups.splice(0))
11
+ cleanup();
12
+ };
13
+ for (const input of signals) {
14
+ if (!input)
15
+ continue;
16
+ if (input.aborted) {
17
+ controller.abort(input.reason);
18
+ dispose();
19
+ break;
20
+ }
21
+ const onAbort = () => {
22
+ controller.abort(input.reason);
23
+ dispose();
24
+ };
25
+ input.addEventListener("abort", onAbort, { once: true });
26
+ cleanups.push(() => input.removeEventListener("abort", onAbort));
27
+ }
28
+ return { signal: controller.signal, dispose };
29
+ }
30
+ /**
31
+ * Settles like `promise`, unless `signal` aborts first, in which case it rejects with
32
+ * `signal.reason`. The inner promise's later outcome is swallowed.
33
+ */
34
+ export function untilAborted(promise, signal) {
35
+ if (!signal)
36
+ return promise;
37
+ if (signal.aborted) {
38
+ promise.catch(() => { });
39
+ return Promise.reject(signal.reason);
40
+ }
41
+ return new Promise((resolve, reject) => {
42
+ const onAbort = () => reject(signal.reason);
43
+ signal.addEventListener("abort", onAbort, { once: true });
44
+ promise.then((value) => {
45
+ signal.removeEventListener("abort", onAbort);
46
+ resolve(value);
47
+ }, (error) => {
48
+ signal.removeEventListener("abort", onAbort);
49
+ reject(error);
50
+ });
51
+ });
52
+ }
@@ -0,0 +1,25 @@
1
+ import type { CollocationFeatureProperties, DataSource, DataStatus } from "../data/types.js";
2
+ import type { SignalOptions } from "./abort.js";
3
+ import type { MonitorsStore } from "./monitors-store.svelte.js";
4
+ export interface CollocationSitesStoreOptions {
5
+ /** The monitors and readings the sites join to; its entry type's scale colors them. */
6
+ monitors: MonitorsStore;
7
+ }
8
+ export interface CollocationSitesStore {
9
+ /** `loading` until the sites load, then `ready` or `error` (with `retry`). */
10
+ readonly status: DataStatus;
11
+ /**
12
+ * The sites for `collocationSitesPlugin`, each at its own position. Readings follow the
13
+ * monitors store's polling; a monitor missing from it gives null readings.
14
+ */
15
+ readonly dataSource: DataSource<CollocationFeatureProperties>;
16
+ /**
17
+ * Loads the sites once. Overlapping calls share one run, and a failed run can be retried. A
18
+ * caller's abort rejects only that caller; the run stops when every caller aborted. A no-op
19
+ * once loaded. It doesn't load the monitors store: init that too.
20
+ */
21
+ init(options?: SignalOptions): Promise<void>;
22
+ /** Cancels the in-flight request. A disposed store does nothing. */
23
+ dispose(): void;
24
+ }
25
+ export declare function createCollocationSitesStore(options: CollocationSitesStoreOptions): CollocationSitesStore;
@@ -0,0 +1,72 @@
1
+ import { getCollocationSites } from "@sjvair/sdk";
2
+ import { encodeRef } from "../data/refs.js";
3
+ import { createLoadOnce } from "./load-once.svelte.js";
4
+ function join(sites, monitors) {
5
+ const list = monitors.monitors;
6
+ const latest = monitors.latest;
7
+ const features = [];
8
+ for (const site of sites) {
9
+ if (!site.position)
10
+ continue;
11
+ const reference = list.get(site.reference_id);
12
+ const colocated = list.get(site.colocated_id);
13
+ const referenceReading = latest.get(site.reference_id);
14
+ const colocatedReading = latest.get(site.colocated_id);
15
+ features.push({
16
+ type: "Feature",
17
+ geometry: { type: "Point", coordinates: site.position.coordinates },
18
+ properties: {
19
+ refKey: encodeRef({ kind: "feature", id: site.id }),
20
+ name: site.name,
21
+ value: referenceReading?.value ?? null,
22
+ timestamp: referenceReading?.timestamp ?? null,
23
+ // A missing reference monitor draws as a sensor in the neutral color, not inactive
24
+ // gray.
25
+ type: reference?.type ?? "",
26
+ isActive: reference?.is_active ?? true,
27
+ isSjvair: reference?.is_sjvair ?? false,
28
+ location: reference?.location === "inside" ? "inside" : "outside",
29
+ referenceName: reference?.name ?? null,
30
+ colocatedId: site.colocated_id,
31
+ colocatedName: colocated?.name ?? null,
32
+ colocatedType: colocated?.type ?? null,
33
+ colocatedValue: colocatedReading?.value ?? null,
34
+ colocatedTimestamp: colocatedReading?.timestamp ?? null
35
+ }
36
+ });
37
+ }
38
+ return { type: "FeatureCollection", features };
39
+ }
40
+ export function createCollocationSitesStore(options) {
41
+ const { monitors } = options;
42
+ const loader = createLoadOnce((signal) => getCollocationSites({ signal }));
43
+ // Rebuilt only when an input changes, so every reader shares one object. Reading the inputs
44
+ // in the getter keeps it reactive to the monitors store's state.
45
+ let cached = null;
46
+ return {
47
+ get status() {
48
+ return loader.status;
49
+ },
50
+ dataSource: {
51
+ get status() {
52
+ return loader.status;
53
+ },
54
+ get features() {
55
+ const sites = loader.value;
56
+ if (sites === null)
57
+ return null;
58
+ const list = monitors.monitors;
59
+ const latest = monitors.latest;
60
+ if (cached?.sites !== sites || cached.list !== list || cached.latest !== latest) {
61
+ cached = { sites, list, latest, features: join(sites, monitors) };
62
+ }
63
+ return cached.features;
64
+ },
65
+ get scale() {
66
+ return monitors.dataSource.scale;
67
+ }
68
+ },
69
+ init: (initOptions) => loader.init(initOptions),
70
+ dispose: () => loader.dispose()
71
+ };
72
+ }
@@ -0,0 +1,38 @@
1
+ import { type MessageProvider } from "../data/messages.js";
2
+ import type { DataSource, DataStatus, FireFeatureProperties } from "../data/types.js";
3
+ import type { SignalOptions } from "./abort.js";
4
+ export interface HmsStoreOptions {
5
+ /** Names features ("Smoke", "Fire detection"). Default: the built-in English messages. */
6
+ messages?: MessageProvider;
7
+ }
8
+ export interface HmsSmokeStore {
9
+ /** `loading` until loaded, then `ready` (also when today has no plumes) or `error`. */
10
+ readonly status: DataStatus;
11
+ /** Today's plumes for `hmsSmokePlugin`; each feature's `value` is its density key. */
12
+ readonly dataSource: DataSource;
13
+ /**
14
+ * Loads today's smoke once. Overlapping calls share one run, and a failed run can be
15
+ * retried. A caller's abort rejects only that caller; the run stops when every caller
16
+ * aborted. A no-op once loaded.
17
+ */
18
+ init(options?: SignalOptions): Promise<void>;
19
+ /** Cancels the in-flight request. A disposed store does nothing. */
20
+ dispose(): void;
21
+ }
22
+ export interface HmsFireStore {
23
+ /** `loading` until loaded, then `ready` (also when today has no fires) or `error`. */
24
+ readonly status: DataStatus;
25
+ /**
26
+ * Nearby detections merged (`groupFires`), for `hmsFirePlugin`; `value` is the mean FRP and
27
+ * `count` how many detections it merges.
28
+ */
29
+ readonly groups: DataSource<FireFeatureProperties>;
30
+ /** Each detection (`cleanFires`), for `hmsFirePlugin`; `value` is its FRP. */
31
+ readonly hotspots: DataSource;
32
+ /** As `HmsSmokeStore.init`. */
33
+ init(options?: SignalOptions): Promise<void>;
34
+ /** Cancels the in-flight request. A disposed store does nothing. */
35
+ dispose(): void;
36
+ }
37
+ export declare function createHmsSmokeStore(options?: HmsStoreOptions): HmsSmokeStore;
38
+ export declare function createHmsFireStore(options?: HmsStoreOptions): HmsFireStore;
@@ -0,0 +1,97 @@
1
+ import { getHMSFire, getHMSSmoke } from "@sjvair/sdk";
2
+ import { encodeRef } from "../data/refs.js";
3
+ import { defaultMessages } from "../data/messages.js";
4
+ import { cleanFires, groupFires } from "../presets/fire-groups.js";
5
+ import { createLoadOnce } from "./load-once.svelte.js";
6
+ const SMOKE_SCALE = { dataset: "hms-smoke", entryType: "smoke-density" };
7
+ const FIRE_SCALE = { dataset: "hms-fire", entryType: "fire-frp" };
8
+ /** A source over a loader's value; `features` stays null until it loads. */
9
+ function sourceOf(loader, scale, pick) {
10
+ // Built once per loaded value, so every reader shares one object.
11
+ let cached = null;
12
+ return {
13
+ get status() {
14
+ return loader.status;
15
+ },
16
+ get features() {
17
+ const value = loader.value;
18
+ if (value === null)
19
+ return null;
20
+ if (cached?.value !== value)
21
+ cached = { value, features: pick(value) };
22
+ return cached.features;
23
+ },
24
+ scale
25
+ };
26
+ }
27
+ export function createHmsSmokeStore(options = {}) {
28
+ const messages = options.messages ?? defaultMessages;
29
+ const loader = createLoadOnce(async (signal) => {
30
+ const smoke = await getHMSSmoke(undefined, { signal });
31
+ const name = messages("mapsdk_hms_smoke_name");
32
+ const features = smoke.map((plume) => ({
33
+ type: "Feature",
34
+ geometry: plume.geometry,
35
+ properties: {
36
+ refKey: encodeRef({ kind: "feature", id: plume.id }),
37
+ name,
38
+ value: plume.density
39
+ }
40
+ }));
41
+ return { type: "FeatureCollection", features };
42
+ });
43
+ return {
44
+ get status() {
45
+ return loader.status;
46
+ },
47
+ dataSource: sourceOf(loader, SMOKE_SCALE, (collection) => collection),
48
+ init: (initOptions) => loader.init(initOptions),
49
+ dispose: () => loader.dispose()
50
+ };
51
+ }
52
+ export function createHmsFireStore(options = {}) {
53
+ const messages = options.messages ?? defaultMessages;
54
+ const loader = createLoadOnce(async (signal) => {
55
+ const fires = await getHMSFire(undefined, { signal });
56
+ const detections = fires.map((fire) => ({
57
+ id: fire.id,
58
+ coordinates: fire.geometry.coordinates,
59
+ frp: fire.frp,
60
+ timestamp: fire.timestamp
61
+ }));
62
+ const detection = messages("mapsdk_hms_fire_detection");
63
+ const hotspots = cleanFires(detections).map((fire) => ({
64
+ type: "Feature",
65
+ geometry: { type: "Point", coordinates: fire.coordinates },
66
+ properties: {
67
+ refKey: encodeRef({ kind: "feature", id: fire.id }),
68
+ name: detection,
69
+ value: fire.frp
70
+ }
71
+ }));
72
+ // Every group names its count, a group of 1 included ("1 fire detection").
73
+ const groups = groupFires(detections).map((group) => ({
74
+ type: "Feature",
75
+ geometry: { type: "Point", coordinates: group.coordinates },
76
+ properties: {
77
+ refKey: encodeRef({ kind: "feature", id: group.id }),
78
+ name: messages("mapsdk_hms_fire_detections", { count: group.count }),
79
+ value: group.avgFrp,
80
+ count: group.count
81
+ }
82
+ }));
83
+ return {
84
+ hotspots: { type: "FeatureCollection", features: hotspots },
85
+ groups: { type: "FeatureCollection", features: groups }
86
+ };
87
+ });
88
+ return {
89
+ get status() {
90
+ return loader.status;
91
+ },
92
+ groups: sourceOf(loader, FIRE_SCALE, (value) => value.groups),
93
+ hotspots: sourceOf(loader, FIRE_SCALE, (value) => value.hotspots),
94
+ init: (initOptions) => loader.init(initOptions),
95
+ dispose: () => loader.dispose()
96
+ };
97
+ }
@@ -0,0 +1,4 @@
1
+ export { createMonitorsStore, type MonitorReading, type MonitorsInitOptions, type MonitorsListItem, type MonitorsStore, type MonitorsStoreOptions } from "./monitors-store.svelte.js";
2
+ export { cleanFires, type FireDetection } from "../presets/fire-groups.js";
3
+ export { createHmsFireStore, createHmsSmokeStore, type HmsFireStore, type HmsSmokeStore, type HmsStoreOptions } from "./hms-stores.svelte.js";
4
+ export { createCollocationSitesStore, type CollocationSitesStore, type CollocationSitesStoreOptions } from "./collocation-store.svelte.js";
@@ -0,0 +1,4 @@
1
+ export { createMonitorsStore } from "./monitors-store.svelte.js";
2
+ export { cleanFires } from "../presets/fire-groups.js";
3
+ export { createHmsFireStore, createHmsSmokeStore } from "./hms-stores.svelte.js";
4
+ export { createCollocationSitesStore } from "./collocation-store.svelte.js";
@@ -0,0 +1,25 @@
1
+ import type { DataStatus } from "../data/types.js";
2
+ import { type SignalOptions } from "./abort.js";
3
+ export interface LoadOnceInitOptions extends SignalOptions {
4
+ /**
5
+ * Stops waiting: the returned promise rejects with the signal's reason. The shared run keeps
6
+ * going for the other calls waiting on it, and is cancelled only when every call waiting on it
7
+ * passed a signal and all of those aborted. Ignored once loaded.
8
+ */
9
+ signal?: AbortSignal;
10
+ }
11
+ export interface LoadOnce<T> {
12
+ /** `loading` until the first load succeeds; `error` (with `retry`) after a failed run. */
13
+ readonly status: DataStatus;
14
+ /** The loaded value, or null before the first successful load. */
15
+ readonly value: T | null;
16
+ /**
17
+ * Loads once. Overlapping calls share one run, a failed run can be retried (call again, or
18
+ * `status.retry()`), and a no-op once loaded or disposed.
19
+ */
20
+ init(options?: LoadOnceInitOptions): Promise<void>;
21
+ /** Cancels the in-flight run. A disposed loader does nothing. */
22
+ dispose(): void;
23
+ }
24
+ /** A loader that fetches once with `load`. Nothing is fetched until `init()`. */
25
+ export declare function createLoadOnce<T>(load: (signal: AbortSignal) => Promise<T>): LoadOnce<T>;