@sjvair/monitor-map 4.2.0 → 5.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +61 -126
- package/dist/lib/MonitorDetailPanel.svelte +23 -20
- package/dist/lib/MonitorMapLayout.svelte +147 -77
- package/dist/lib/abort.d.ts +16 -0
- package/dist/lib/abort.js +51 -0
- package/dist/lib/components/DataBox.svelte +4 -1
- package/dist/lib/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
- package/dist/lib/config.svelte.d.ts +7 -0
- package/dist/lib/config.svelte.js +10 -0
- package/dist/lib/core.d.ts +2 -2
- package/dist/lib/core.js +2 -1
- package/dist/lib/data-chart/DataChart.svelte +54 -24
- package/dist/lib/data-chart/DateRange.d.ts +13 -4
- package/dist/lib/data-chart/DateRange.js +50 -14
- package/dist/lib/data-chart/chart-utils.js +5 -7
- package/dist/lib/data-chart/level-colors.d.ts +3 -4
- package/dist/lib/data-chart/level-colors.js +7 -4
- package/dist/lib/data-chart/pacific-time.d.ts +9 -0
- package/dist/lib/data-chart/pacific-time.js +25 -0
- package/dist/lib/data-chart/requests.d.ts +2 -1
- package/dist/lib/data-chart/requests.js +5 -2
- package/dist/lib/data-chart/tooltip.js +2 -2
- package/{src/lib/ev-stations/components/EvStationTooltip.svelte → dist/lib/ev-stations/EvPopup.svelte} +6 -5
- package/dist/lib/ev-stations/EvPopup.svelte.d.ts +7 -0
- package/dist/lib/ev-stations/ev-plugin.svelte.d.ts +14 -0
- package/dist/lib/ev-stations/ev-plugin.svelte.js +71 -0
- package/dist/lib/ev-stations/ev-station-icons.d.ts +1 -4
- package/dist/lib/ev-stations/ev-station-icons.js +1 -13
- package/dist/lib/ev-stations/ev-stations.d.ts +7 -0
- package/dist/lib/ev-stations/ev-stations.js +53 -0
- package/dist/lib/ev-stations/ev-store.svelte.d.ts +21 -0
- package/dist/lib/ev-stations/ev-store.svelte.js +83 -0
- package/dist/lib/ev-stations/station-popup.svelte.d.ts +14 -0
- package/dist/lib/ev-stations/station-popup.svelte.js +54 -0
- package/dist/lib/ev-stations/types.d.ts +0 -29
- package/dist/lib/ev-stations/zip-codes.d.ts +11 -0
- package/dist/lib/ev-stations/zip-codes.js +28 -16
- package/dist/lib/index.d.ts +8 -56
- package/dist/lib/index.js +7 -46
- package/dist/lib/legend/Legend.svelte +18 -19
- package/dist/lib/legend/Legend.svelte.d.ts +2 -2
- package/dist/lib/levels.d.ts +7 -0
- package/dist/lib/levels.js +19 -0
- package/dist/lib/map/MapShell.svelte +17 -24
- package/dist/lib/map/MapShell.svelte.d.ts +4 -4
- package/dist/lib/map/basemaps.d.ts +25 -0
- package/dist/lib/map/basemaps.js +66 -0
- package/dist/lib/map/live-map.svelte.d.ts +3 -0
- package/dist/lib/map/live-map.svelte.js +22 -0
- package/dist/lib/map/sync-plugins.svelte.d.ts +19 -0
- package/dist/lib/map/sync-plugins.svelte.js +52 -0
- package/dist/lib/monitors/components/MonitorSubscriptionManager.svelte +82 -82
- package/dist/lib/monitors/filtered-source.svelte.d.ts +9 -0
- package/dist/lib/monitors/filtered-source.svelte.js +28 -0
- package/dist/lib/monitors/monitor-details.svelte.js +11 -3
- package/dist/lib/monitors/monitors.svelte.d.ts +29 -26
- package/dist/lib/monitors/monitors.svelte.js +35 -126
- package/dist/lib/monitors/retry-load.d.ts +8 -0
- package/dist/lib/monitors/retry-load.js +11 -0
- package/dist/lib/monitors/subscriptions.d.ts +3 -0
- package/dist/lib/monitors/subscriptions.js +9 -0
- package/dist/lib/monitors/types.d.ts +1 -41
- package/dist/lib/options/map-option-defaults.d.ts +25 -36
- package/dist/lib/options/map-option-defaults.js +12 -30
- package/dist/lib/options/map-options.svelte.d.ts +13 -0
- package/dist/lib/options/map-options.svelte.js +26 -0
- package/dist/lib/options/menus/LayersMenu.svelte +8 -20
- package/dist/lib/options/menus/LayersMenu.svelte.d.ts +17 -2
- package/dist/lib/options/menus/MonitorsMenu.svelte +34 -18
- package/dist/lib/options/menus/OverlaysMenu.svelte +2 -2
- package/dist/lib/options/menus/PollutantMenu.svelte +4 -9
- package/dist/lib/options/menus/SettingsMenu.svelte +32 -25
- package/dist/lib/options/menus/SettingsMenu.svelte.d.ts +5 -16
- package/dist/lib/options/plugin-order.d.ts +8 -0
- package/dist/lib/options/plugin-order.js +36 -0
- package/dist/lib/options/pollutant-param.d.ts +7 -5
- package/dist/lib/options/pollutant-param.js +9 -5
- package/dist/lib/options/row-icons.d.ts +8 -0
- package/dist/lib/options/row-icons.js +15 -0
- package/dist/lib/options/rows/MarkerIcon.svelte +5 -5
- package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +1 -2
- package/dist/lib/search/Search.svelte +20 -33
- package/dist/lib/search/Search.svelte.d.ts +5 -1
- package/dist/lib/search/search-pin.svelte.d.ts +11 -0
- package/dist/lib/search/search-pin.svelte.js +28 -0
- package/dist/lib/search/service.d.ts +7 -2
- package/dist/lib/search/service.js +17 -10
- package/dist/lib/stores.svelte.d.ts +8 -0
- package/dist/lib/stores.svelte.js +9 -0
- package/dist/lib/wind/wind-plugin.d.ts +11 -0
- package/dist/lib/wind/wind-plugin.js +25 -0
- package/package.json +12 -10
- package/src/lib/MonitorDetailPanel.svelte +23 -20
- package/src/lib/MonitorMapLayout.svelte +147 -77
- package/src/lib/abort.ts +59 -0
- package/src/lib/components/DataBox.svelte +4 -1
- package/src/lib/config.svelte.ts +16 -0
- package/src/lib/core.ts +2 -2
- package/src/lib/data-chart/DataChart.svelte +54 -24
- package/src/lib/data-chart/DateRange.ts +58 -15
- package/src/lib/data-chart/chart-utils.ts +6 -7
- package/src/lib/data-chart/level-colors.ts +8 -9
- package/src/lib/data-chart/pacific-time.ts +31 -0
- package/src/lib/data-chart/requests.ts +14 -7
- package/src/lib/data-chart/tooltip.ts +2 -2
- package/{dist/lib/ev-stations/components/EvStationTooltip.svelte → src/lib/ev-stations/EvPopup.svelte} +6 -5
- package/src/lib/ev-stations/ev-plugin.svelte.ts +86 -0
- package/src/lib/ev-stations/ev-station-icons.ts +1 -17
- package/src/lib/ev-stations/ev-stations.ts +72 -0
- package/src/lib/ev-stations/ev-store.svelte.ts +117 -0
- package/src/lib/ev-stations/station-popup.svelte.ts +64 -0
- package/src/lib/ev-stations/types.ts +0 -34
- package/src/lib/ev-stations/zip-codes.ts +29 -16
- package/src/lib/index.ts +8 -99
- package/src/lib/legend/Legend.svelte +18 -19
- package/src/lib/levels.ts +24 -0
- package/src/lib/map/MapShell.svelte +17 -24
- package/src/lib/map/basemaps.ts +76 -0
- package/src/lib/map/live-map.svelte.ts +24 -0
- package/src/lib/map/sync-plugins.svelte.ts +69 -0
- package/src/lib/monitors/components/MonitorSubscriptionManager.svelte +82 -82
- package/src/lib/monitors/filtered-source.svelte.ts +38 -0
- package/src/lib/monitors/monitor-details.svelte.ts +12 -3
- package/src/lib/monitors/monitors.svelte.ts +62 -153
- package/src/lib/monitors/retry-load.ts +14 -0
- package/src/lib/monitors/subscriptions.ts +17 -0
- package/src/lib/monitors/types.ts +1 -47
- package/src/lib/options/map-option-defaults.ts +30 -53
- package/src/lib/options/map-options.svelte.ts +30 -0
- package/src/lib/options/menus/LayersMenu.svelte +8 -20
- package/src/lib/options/menus/MonitorsMenu.svelte +34 -18
- package/src/lib/options/menus/OverlaysMenu.svelte +2 -2
- package/src/lib/options/menus/PollutantMenu.svelte +4 -9
- package/src/lib/options/menus/SettingsMenu.svelte +32 -25
- package/src/lib/options/plugin-order.ts +38 -0
- package/src/lib/options/pollutant-param.ts +14 -9
- package/src/lib/options/row-icons.ts +17 -0
- package/src/lib/options/rows/MarkerIcon.svelte +5 -5
- package/src/lib/search/Search.svelte +20 -33
- package/src/lib/search/search-pin.svelte.ts +35 -0
- package/src/lib/search/service.ts +34 -15
- package/src/lib/stores.svelte.ts +15 -0
- package/src/lib/wind/wind-plugin.ts +33 -0
- package/dist/lib/collocation-sites/CollocationTooltip.svelte +0 -98
- package/dist/lib/collocation-sites/CollocationTooltip.svelte.d.ts +0 -7
- package/dist/lib/collocation-sites/collocations-icon-manager.svelte.d.ts +0 -7
- package/dist/lib/collocation-sites/collocations-icon-manager.svelte.js +0 -18
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +0 -21
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +0 -113
- package/dist/lib/collocation-sites/collocations.svelte.d.ts +0 -9
- package/dist/lib/collocation-sites/collocations.svelte.js +0 -17
- package/dist/lib/collocation-sites/types.d.ts +0 -8
- package/dist/lib/collocation-sites/types.js +0 -1
- package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
- package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte.d.ts +0 -7
- package/dist/lib/ev-stations/components/EvStationTooltip.svelte.d.ts +0 -7
- package/dist/lib/ev-stations/ev-stations-cluster-renderer.d.ts +0 -19
- package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +0 -131
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.d.ts +0 -31
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +0 -226
- package/dist/lib/ev-stations/ev-stations.svelte.d.ts +0 -9
- package/dist/lib/ev-stations/ev-stations.svelte.js +0 -55
- package/dist/lib/hms/hms-fire-icon-manager.d.ts +0 -13
- package/dist/lib/hms/hms-fire-icon-manager.js +0 -39
- package/dist/lib/hms/hms-fire-map-integration.svelte.d.ts +0 -23
- package/dist/lib/hms/hms-fire-map-integration.svelte.js +0 -94
- package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +0 -39
- package/dist/lib/hms/hms-smoke-map-integration.svelte.js +0 -87
- package/dist/lib/hms/hms.svelte.d.ts +0 -19
- package/dist/lib/hms/hms.svelte.js +0 -227
- package/dist/lib/legend/LegendBar.svelte +0 -45
- package/dist/lib/legend/LegendBar.svelte.d.ts +0 -7
- package/dist/lib/legend/legend-data.d.ts +0 -40
- package/dist/lib/legend/legend-data.js +0 -65
- package/dist/lib/map/Map.svelte +0 -13
- package/dist/lib/map/Map.svelte.d.ts +0 -8
- package/dist/lib/map/basemap.d.ts +0 -14
- package/dist/lib/map/basemap.js +0 -10
- package/dist/lib/map/icons.d.ts +0 -7
- package/dist/lib/map/icons.js +0 -33
- package/dist/lib/map/integrations/base-layer-seperator.d.ts +0 -9
- package/dist/lib/map/integrations/base-layer-seperator.js +0 -11
- package/dist/lib/map/integrations/click-manager.d.ts +0 -14
- package/dist/lib/map/integrations/click-manager.js +0 -52
- package/dist/lib/map/integrations/integrations-manager.d.ts +0 -8
- package/dist/lib/map/integrations/integrations-manager.js +0 -20
- package/dist/lib/map/integrations/map-display-option.svelte.d.ts +0 -7
- package/dist/lib/map/integrations/map-display-option.svelte.js +0 -10
- package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +0 -32
- package/dist/lib/map/integrations/map-geojson-integration.svelte.js +0 -60
- package/dist/lib/map/integrations/map-icon-manager.d.ts +0 -15
- package/dist/lib/map/integrations/map-icon-manager.js +0 -52
- package/dist/lib/map/integrations/map-integration.svelte.d.ts +0 -6
- package/dist/lib/map/integrations/map-integration.svelte.js +0 -2
- package/dist/lib/map/integrations/map-layer-integration.svelte.d.ts +0 -11
- package/dist/lib/map/integrations/map-layer-integration.svelte.js +0 -40
- package/dist/lib/map/integrations/tooltip.svelte.d.ts +0 -36
- package/dist/lib/map/integrations/tooltip.svelte.js +0 -131
- package/dist/lib/map/integrations/types.d.ts +0 -14
- package/dist/lib/map/integrations/types.js +0 -1
- package/dist/lib/map/map-manager.svelte.d.ts +0 -20
- package/dist/lib/map/map-manager.svelte.js +0 -63
- package/dist/lib/map/map-style-state.svelte.d.ts +0 -23
- package/dist/lib/map/map-style-state.svelte.js +0 -86
- package/dist/lib/map/utils.d.ts +0 -7
- package/dist/lib/map/utils.js +0 -36
- package/dist/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
- package/dist/lib/monitors/components/MonitorClusterTooltip.svelte.d.ts +0 -7
- package/dist/lib/monitors/components/MonitorTooltip.svelte +0 -61
- package/dist/lib/monitors/components/MonitorTooltip.svelte.d.ts +0 -7
- package/dist/lib/monitors/monitor-utils.d.ts +0 -4
- package/dist/lib/monitors/monitor-utils.js +0 -41
- package/dist/lib/monitors/monitors-cluster-renderer.d.ts +0 -33
- package/dist/lib/monitors/monitors-cluster-renderer.js +0 -221
- package/dist/lib/monitors/monitors-icon-manager.svelte.d.ts +0 -15
- package/dist/lib/monitors/monitors-icon-manager.svelte.js +0 -78
- package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +0 -43
- package/dist/lib/monitors/monitors-map-integration.svelte.js +0 -301
- package/dist/lib/options/reset-map-options.d.ts +0 -2
- package/dist/lib/options/reset-map-options.js +0 -24
- package/dist/lib/reactivity.svelte.d.ts +0 -0
- package/dist/lib/reactivity.svelte.js +0 -83
- package/dist/lib/wind/wind.svelte.d.ts +0 -10
- package/dist/lib/wind/wind.svelte.js +0 -13
- package/src/lib/collocation-sites/CollocationTooltip.svelte +0 -98
- package/src/lib/collocation-sites/collocations-icon-manager.svelte.ts +0 -21
- package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +0 -136
- package/src/lib/collocation-sites/collocations.svelte.ts +0 -24
- package/src/lib/collocation-sites/types.ts +0 -10
- package/src/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
- package/src/lib/ev-stations/ev-stations-cluster-renderer.ts +0 -162
- package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +0 -248
- package/src/lib/ev-stations/ev-stations.svelte.ts +0 -79
- package/src/lib/hms/hms-fire-icon-manager.ts +0 -48
- package/src/lib/hms/hms-fire-map-integration.svelte.ts +0 -109
- package/src/lib/hms/hms-smoke-map-integration.svelte.ts +0 -105
- package/src/lib/hms/hms.svelte.ts +0 -264
- package/src/lib/legend/LegendBar.svelte +0 -45
- package/src/lib/legend/legend-data.ts +0 -101
- package/src/lib/map/Map.svelte +0 -13
- package/src/lib/map/basemap.ts +0 -24
- package/src/lib/map/icons.ts +0 -42
- package/src/lib/map/integrations/base-layer-seperator.ts +0 -17
- package/src/lib/map/integrations/click-manager.ts +0 -64
- package/src/lib/map/integrations/integrations-manager.ts +0 -25
- package/src/lib/map/integrations/map-display-option.svelte.ts +0 -13
- package/src/lib/map/integrations/map-geojson-integration.svelte.ts +0 -77
- package/src/lib/map/integrations/map-icon-manager.ts +0 -67
- package/src/lib/map/integrations/map-integration.svelte.ts +0 -8
- package/src/lib/map/integrations/map-layer-integration.svelte.ts +0 -49
- package/src/lib/map/integrations/tooltip.svelte.ts +0 -153
- package/src/lib/map/integrations/types.ts +0 -24
- package/src/lib/map/map-manager.svelte.ts +0 -83
- package/src/lib/map/map-style-state.svelte.ts +0 -94
- package/src/lib/map/utils.ts +0 -53
- package/src/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
- package/src/lib/monitors/components/MonitorTooltip.svelte +0 -61
- package/src/lib/monitors/monitor-utils.ts +0 -57
- package/src/lib/monitors/monitors-cluster-renderer.ts +0 -285
- package/src/lib/monitors/monitors-icon-manager.svelte.ts +0 -98
- package/src/lib/monitors/monitors-map-integration.svelte.ts +0 -318
- package/src/lib/options/reset-map-options.ts +0 -28
- package/src/lib/reactivity.svelte.ts +0 -82
- package/src/lib/wind/wind.svelte.ts +0 -20
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { MapStyle } from "@maptiler/sdk";
|
|
2
|
+
import type { ReferenceMapStyle } from "@maptiler/sdk";
|
|
3
|
+
import type { BasemapConfig } from "@sjvair/map-sdk";
|
|
4
|
+
|
|
5
|
+
/** Every MapTiler style at its highest `_V<n>` version, sorted by name. */
|
|
6
|
+
const BASEMAP_STYLES: ReferenceMapStyle[] = (() => {
|
|
7
|
+
const latest = new Map<string, { version: number; style: ReferenceMapStyle }>();
|
|
8
|
+
for (const style of Object.values(MapStyle) as ReferenceMapStyle[]) {
|
|
9
|
+
const id = style.getId();
|
|
10
|
+
const match = id.match(/^(.+?)_V(\d+)$/i);
|
|
11
|
+
const baseName = match ? match[1] : id;
|
|
12
|
+
const version = match ? parseInt(match[2], 10) : 0;
|
|
13
|
+
const existing = latest.get(baseName);
|
|
14
|
+
if (!existing || version > existing.version) {
|
|
15
|
+
latest.set(baseName, { version, style });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return Array.from(latest.values())
|
|
19
|
+
.map((e) => e.style)
|
|
20
|
+
.sort((a, b) => a.getName().localeCompare(b.getName()));
|
|
21
|
+
})();
|
|
22
|
+
|
|
23
|
+
/** The basemap the map opens on, and Reset selects: Streets v4's default variant. */
|
|
24
|
+
export function defaultBasemapId(): string {
|
|
25
|
+
return MapStyle.STREETS_V4.getDefaultVariant().getId();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The monitor map's basemaps: every MapTiler style's variants (style URLs, so map-sdk's fallback
|
|
30
|
+
* can match a failed style by URL), opening on Streets v4, with no layers removed.
|
|
31
|
+
*/
|
|
32
|
+
export function monitorMapBasemaps(): BasemapConfig {
|
|
33
|
+
const seen = new Set<string>();
|
|
34
|
+
const styles: BasemapConfig["styles"] = [];
|
|
35
|
+
for (const style of BASEMAP_STYLES) {
|
|
36
|
+
for (const variant of style.getVariants()) {
|
|
37
|
+
const id = variant.getId();
|
|
38
|
+
if (seen.has(id)) continue;
|
|
39
|
+
seen.add(id);
|
|
40
|
+
styles.push({ id, style: variant.getExpandedStyleURL() });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { styles, defaultId: defaultBasemapId(), excludedLayers: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BasemapChoice {
|
|
47
|
+
name: string;
|
|
48
|
+
variants: Array<{ id: string; type: string; name: string }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The Basemap select's options (one per style name) and each one's Theme radios. */
|
|
52
|
+
export function basemapChoices(): BasemapChoice[] {
|
|
53
|
+
return BASEMAP_STYLES.map((style) => ({
|
|
54
|
+
name: style.getName(),
|
|
55
|
+
variants: style
|
|
56
|
+
.getVariants()
|
|
57
|
+
.map((variant) => ({ id: variant.getId(), type: variant.getType(), name: variant.getName() }))
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The choice that has a variant with this id. */
|
|
62
|
+
export function choiceForId(id: string): BasemapChoice | undefined {
|
|
63
|
+
return basemapChoices().find((choice) => choice.variants.some((v) => v.id === id));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The variant to show after switching to the basemap named `toBaseName`: the same variant type
|
|
68
|
+
* (e.g. "DARK") as `currentId` when the new basemap offers it, otherwise its default.
|
|
69
|
+
*/
|
|
70
|
+
export function switchBasemap(currentId: string, toBaseName: string): string {
|
|
71
|
+
const target = BASEMAP_STYLES.find((style) => style.getName() === toBaseName);
|
|
72
|
+
if (!target) return currentId;
|
|
73
|
+
const type = choiceForId(currentId)?.variants.find((v) => v.id === currentId)?.type;
|
|
74
|
+
if (type && target.hasVariant(type)) return target.getVariant(type).getId();
|
|
75
|
+
return target.getDefaultVariant().getId();
|
|
76
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createMap, type MapContext } from "@sjvair/map-sdk";
|
|
2
|
+
import { monitorMapBasemaps } from "./basemaps";
|
|
3
|
+
import { monitorMapConfig } from "../config.svelte.js";
|
|
4
|
+
|
|
5
|
+
/** The San Joaquin Valley view the map opens on. */
|
|
6
|
+
const INITIAL_VIEW = {
|
|
7
|
+
center: [-119.7987626619462, 36.76272050981146] as [number, number],
|
|
8
|
+
zoom: 7,
|
|
9
|
+
bearing: 0,
|
|
10
|
+
pitch: 0
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** The monitor map's map context: a globe over space on Streets v4, with every basemap to choose. */
|
|
14
|
+
export function createLiveMap(): MapContext {
|
|
15
|
+
return createMap({
|
|
16
|
+
config: {
|
|
17
|
+
maptilerKey: monitorMapConfig().maptilerKey,
|
|
18
|
+
initialView: INITIAL_VIEW,
|
|
19
|
+
projection: "globe",
|
|
20
|
+
space: { preset: "milkyway" },
|
|
21
|
+
basemaps: monitorMapBasemaps()
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
import type { MapContext, MapPlugin, PluginOptions } from "@sjvair/map-sdk";
|
|
3
|
+
import { beforeId, DRAW_ORDER, type PluginKey } from "../options/plugin-order";
|
|
4
|
+
|
|
5
|
+
/** A plugin, or a plugin with the add options it needs (e.g. its selection mode). */
|
|
6
|
+
export type PluginEntry =
|
|
7
|
+
MapPlugin | { plugin: MapPlugin; options?: Omit<PluginOptions, "before"> };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds a fresh plugin for each key (plugin objects are single-use). Called untracked. A key
|
|
11
|
+
* with no factory is never added, and never removed either: another `syncPlugins` call may own it.
|
|
12
|
+
*/
|
|
13
|
+
export type PluginFactories = Partial<Record<PluginKey, () => PluginEntry>>;
|
|
14
|
+
|
|
15
|
+
function entryOf(entry: PluginEntry): { plugin: MapPlugin; options?: PluginOptions } {
|
|
16
|
+
return "plugin" in entry ? entry : { plugin: entry };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Keeps `map`'s plugins in step with `wanted()`, in `DRAW_ORDER`: adds a wanted plugin below the
|
|
21
|
+
* first present plugin above it and removes one that is no longer wanted. A change to anything
|
|
22
|
+
* `deps()` reads removes and re-adds every present plugin of `factories` (e.g. `clustered`,
|
|
23
|
+
* which a plugin only takes when it is built). Call it during component setup.
|
|
24
|
+
*/
|
|
25
|
+
export function syncPlugins(
|
|
26
|
+
map: MapContext,
|
|
27
|
+
wanted: () => Set<PluginKey>,
|
|
28
|
+
factories: PluginFactories,
|
|
29
|
+
deps?: () => unknown
|
|
30
|
+
): void {
|
|
31
|
+
const keys = DRAW_ORDER.filter((key) => factories[key]);
|
|
32
|
+
// Bumped when a dep changes; separate from `wanted` so an option change that leaves the
|
|
33
|
+
// wanted set as it is never rebuilds anything.
|
|
34
|
+
let generation = $state(0);
|
|
35
|
+
let built = 0;
|
|
36
|
+
|
|
37
|
+
if (deps) {
|
|
38
|
+
let first = true;
|
|
39
|
+
$effect(() => {
|
|
40
|
+
deps();
|
|
41
|
+
if (first) first = false;
|
|
42
|
+
else untrack(() => generation++);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
$effect(() => {
|
|
47
|
+
const want = wanted();
|
|
48
|
+
const current = generation;
|
|
49
|
+
untrack(() => {
|
|
50
|
+
const rebuild = current !== built;
|
|
51
|
+
built = current;
|
|
52
|
+
// A scratch copy of the ids for this pass; nothing reads it reactively.
|
|
53
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
54
|
+
const present = new Set(map.plugins.map((p) => p.id));
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
if (present.has(key) && (rebuild || !want.has(key))) {
|
|
57
|
+
map.removePlugin(key);
|
|
58
|
+
present.delete(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
if (!want.has(key) || present.has(key)) continue;
|
|
63
|
+
const { plugin, options } = entryOf(factories[key]!());
|
|
64
|
+
map.addPlugin(plugin, { ...options, before: beforeId(key, present) });
|
|
65
|
+
present.add(key);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { BellCheckIcon, BellOffIcon, ChevronDownIcon } from "@lucide/svelte";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { subscribe, unsubscribe } from "@sjvair/sdk/account";
|
|
4
|
+
import LoadError from "$lib/LoadError.svelte";
|
|
5
|
+
import { monitorsManager } from "$lib/monitors/monitors.svelte.js";
|
|
6
|
+
import { getApiToken, loadSubscriptionLevel } from "$lib/monitors/subscriptions";
|
|
5
7
|
|
|
6
8
|
interface MonitorSubscriptionManagerProps {
|
|
7
9
|
monitorId: string;
|
|
@@ -21,38 +23,38 @@
|
|
|
21
23
|
let saving = $state(false);
|
|
22
24
|
let subscribedLevel: string | null = $state(null);
|
|
23
25
|
|
|
26
|
+
// The alert rows, in this order, from the PM2.5 scale; a key the scale lacks is skipped.
|
|
24
27
|
const alertLevels = $derived.by(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return monitorsManager.meta
|
|
28
|
-
.entryType("pm25")
|
|
29
|
-
.asIter.levels!.filter((level) =>
|
|
30
|
-
ALERT_LEVEL_NAMES.includes(level.name as (typeof ALERT_LEVEL_NAMES)[number])
|
|
31
|
-
);
|
|
32
|
-
} catch {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
28
|
+
const levels = monitorsManager.meta?.entryType("pm25").scale?.levels ?? [];
|
|
29
|
+
return ALERT_LEVEL_NAMES.flatMap((key) => levels.find((level) => level.key === key) ?? []);
|
|
35
30
|
});
|
|
36
31
|
|
|
32
|
+
let loadError = $state(false);
|
|
33
|
+
let attempt = $state(0);
|
|
34
|
+
|
|
37
35
|
$effect(() => {
|
|
38
|
-
|
|
36
|
+
const id = monitorId;
|
|
37
|
+
void attempt;
|
|
39
38
|
open = false;
|
|
40
39
|
subscribedLevel = null;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
async function loadSubscription() {
|
|
45
|
-
const id = monitorId;
|
|
40
|
+
loadError = false;
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
const { signal } = controller;
|
|
46
43
|
loading = true;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
44
|
+
loadSubscriptionLevel(id, { signal })
|
|
45
|
+
.then((level) => {
|
|
46
|
+
if (!signal.aborted) subscribedLevel = level;
|
|
47
|
+
})
|
|
48
|
+
.catch((error) => {
|
|
49
|
+
if (signal.aborted) return;
|
|
50
|
+
console.error("Could not load subscription status", error);
|
|
51
|
+
loadError = true;
|
|
52
|
+
})
|
|
53
|
+
.finally(() => {
|
|
54
|
+
if (!signal.aborted) loading = false;
|
|
55
|
+
});
|
|
56
|
+
return () => controller.abort();
|
|
57
|
+
});
|
|
56
58
|
|
|
57
59
|
async function handleLevelClick(level: string | null) {
|
|
58
60
|
if (saving || !level) return;
|
|
@@ -75,61 +77,59 @@
|
|
|
75
77
|
}
|
|
76
78
|
open = false;
|
|
77
79
|
}
|
|
78
|
-
|
|
79
|
-
function getApiToken(): string {
|
|
80
|
-
return (
|
|
81
|
-
(globalThis as unknown as Record<string, Record<string, string>>)["USER"]?.api_token ?? ""
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
80
|
</script>
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
82
|
+
{#if loadError}
|
|
83
|
+
<LoadError message="Couldn't load your alert settings." onretry={() => attempt++} />
|
|
84
|
+
{:else}
|
|
85
|
+
<div class="relative select-none">
|
|
86
|
+
<button
|
|
87
|
+
class={[
|
|
88
|
+
"flex w-full cursor-pointer items-center justify-center gap-2 rounded border px-3 py-1.5 text-sm font-semibold text-white",
|
|
89
|
+
subscribedLevel
|
|
90
|
+
? "border-green-700 bg-green-600 hover:bg-green-700"
|
|
91
|
+
: "border-blue-600 bg-blue-500 hover:bg-blue-600"
|
|
92
|
+
]}
|
|
93
|
+
disabled={loading}
|
|
94
|
+
onclick={() => (open = !open)}
|
|
95
|
+
>
|
|
96
|
+
{subscribedLevel ? "Manage Subscription" : "Subscribe to Alerts"}
|
|
97
|
+
<ChevronDownIcon
|
|
98
|
+
size={16}
|
|
99
|
+
class={["transition-transform duration-300", open ? "rotate-180" : ""]}
|
|
100
|
+
/>
|
|
101
|
+
</button>
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</div>
|
|
103
|
+
{#if open}
|
|
104
|
+
<button aria-label="Close" class="fixed inset-0 z-10" onclick={() => (open = false)}></button>
|
|
105
|
+
<div class="absolute top-full left-0 z-20 mt-1 w-full overflow-hidden rounded shadow-lg">
|
|
106
|
+
{#if subscribedLevel}
|
|
107
|
+
<button
|
|
108
|
+
class="flex w-full cursor-pointer items-center justify-between rounded-t border border-black/30 px-4 py-2 text-sm hover:bg-[#F5F5F3]"
|
|
109
|
+
onclick={() => handleLevelClick(subscribedLevel)}
|
|
110
|
+
>
|
|
111
|
+
Unsubscribe
|
|
112
|
+
<BellOffIcon size={14} />
|
|
113
|
+
</button>
|
|
114
|
+
{/if}
|
|
115
|
+
{#each alertLevels as level (level.key)}
|
|
116
|
+
{@const isSubscribed = level.key === subscribedLevel}
|
|
117
|
+
<button
|
|
118
|
+
class={[
|
|
119
|
+
"flex w-full items-center justify-between px-4 py-2 text-sm text-white last:rounded-b",
|
|
120
|
+
!isSubscribed ? "cursor-pointer first:rounded-t hover:brightness-90" : ""
|
|
121
|
+
]}
|
|
122
|
+
style="background-color: {level.color}"
|
|
123
|
+
disabled={saving}
|
|
124
|
+
onclick={!isSubscribed ? () => handleLevelClick(level.key) : null}
|
|
125
|
+
>
|
|
126
|
+
<span>{level.label}</span>
|
|
127
|
+
{#if isSubscribed}
|
|
128
|
+
<BellCheckIcon size={14} />
|
|
129
|
+
{/if}
|
|
130
|
+
</button>
|
|
131
|
+
{/each}
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
135
|
+
{/if}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DataSource, MonitorFeatureProperties } from "@sjvair/map-sdk/data";
|
|
2
|
+
import type { MapOptions } from "../options/map-option-defaults";
|
|
3
|
+
|
|
4
|
+
type Options = Pick<MapOptions, "monitorTypes" | "inactive" | "inside">;
|
|
5
|
+
|
|
6
|
+
function shown(props: MonitorFeatureProperties, options: Options): boolean {
|
|
7
|
+
return (
|
|
8
|
+
options.monitorTypes[props.type] === true &&
|
|
9
|
+
(props.isActive || options.inactive) &&
|
|
10
|
+
(props.location !== "inside" || options.inside)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `source` with only the monitors the Monitors menu shows: types that are on, inactive and
|
|
16
|
+
* inside ones only when those are on. Clusters are built from these, so their averages follow.
|
|
17
|
+
*/
|
|
18
|
+
export function filteredMonitors(
|
|
19
|
+
source: DataSource<MonitorFeatureProperties>,
|
|
20
|
+
options: Options
|
|
21
|
+
): DataSource<MonitorFeatureProperties> {
|
|
22
|
+
const features = $derived.by(() => {
|
|
23
|
+
const all = source.features;
|
|
24
|
+
if (!all) return null;
|
|
25
|
+
return { ...all, features: all.features.filter((f) => shown(f.properties, options)) };
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
get status() {
|
|
29
|
+
return source.status;
|
|
30
|
+
},
|
|
31
|
+
get features() {
|
|
32
|
+
return features;
|
|
33
|
+
},
|
|
34
|
+
get scale() {
|
|
35
|
+
return source.scale;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -13,15 +13,17 @@ export class MonitorDetailsState {
|
|
|
13
13
|
#id: string | null = null;
|
|
14
14
|
// Bumped per request; a response for anything but the latest request is dropped.
|
|
15
15
|
#request = 0;
|
|
16
|
+
#controller: AbortController | null = null;
|
|
16
17
|
|
|
17
18
|
async load(id: string): Promise<void> {
|
|
18
19
|
this.#id = id;
|
|
19
20
|
const request = ++this.#request;
|
|
21
|
+
const { signal } = this.#restart();
|
|
20
22
|
this.monitor = null;
|
|
21
23
|
this.status = "loading";
|
|
22
24
|
|
|
23
25
|
try {
|
|
24
|
-
const monitor = await getMonitorDetails(id);
|
|
26
|
+
const monitor = await getMonitorDetails(id, { signal });
|
|
25
27
|
if (request !== this.#request) return;
|
|
26
28
|
this.monitor = monitor;
|
|
27
29
|
this.status = "ready";
|
|
@@ -40,16 +42,23 @@ export class MonitorDetailsState {
|
|
|
40
42
|
async refresh(): Promise<void> {
|
|
41
43
|
if (this.status !== "ready" || this.#id === null) return;
|
|
42
44
|
const request = ++this.#request;
|
|
45
|
+
const { signal } = this.#restart();
|
|
43
46
|
|
|
44
47
|
try {
|
|
45
|
-
const monitor = await getMonitorDetails(this.#id);
|
|
48
|
+
const monitor = await getMonitorDetails(this.#id, { signal });
|
|
46
49
|
if (request !== this.#request) return;
|
|
47
50
|
this.monitor = monitor;
|
|
48
51
|
} catch (error) {
|
|
49
|
-
if (request === this.#request) console.error(error);
|
|
52
|
+
if (!signal.aborted && request === this.#request) console.error(error);
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
/** Aborts whatever request is in flight and starts a new controller for the next one. */
|
|
57
|
+
#restart(): AbortController {
|
|
58
|
+
this.#controller?.abort();
|
|
59
|
+
return (this.#controller = new AbortController());
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
retry(): Promise<void> {
|
|
54
63
|
return this.#id === null ? Promise.resolve() : this.load(this.#id);
|
|
55
64
|
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
getMonitorsMeta,
|
|
5
|
-
type MonitorData,
|
|
6
|
-
type MonitorLatestType,
|
|
7
|
-
type MonitorsMeta,
|
|
8
|
-
type SJVAirEntryLevel
|
|
9
|
-
} from "@sjvair/sdk";
|
|
10
|
-
import { XMap } from "@tstk/builtin-extensions";
|
|
11
|
-
import { Interval } from "@tstk/utils";
|
|
12
|
-
import { untrack } from "svelte";
|
|
13
|
-
import type { MonitorsDataSource, MonitorsPollutant } from "./types";
|
|
1
|
+
import type { MonitorData, MonitorsMeta } from "@sjvair/sdk";
|
|
2
|
+
import type { MonitorsStore } from "@sjvair/map-sdk/stores";
|
|
3
|
+
import { monitorsStore } from "../stores.svelte.js";
|
|
14
4
|
|
|
15
5
|
export interface MonitorsInitOptions {
|
|
16
6
|
/**
|
|
@@ -18,153 +8,72 @@ export interface MonitorsInitOptions {
|
|
|
18
8
|
* load the data without showing a map (`resumeAutoUpdate()` starts polling later).
|
|
19
9
|
*/
|
|
20
10
|
autoUpdate?: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
class MonitorsManager implements MonitorsDataSource {
|
|
24
|
-
autoUpdate: Interval = new Interval(async () => await this.update(), 2 * 60 * 1000);
|
|
25
|
-
initialized: boolean = $state(false);
|
|
26
|
-
|
|
27
|
-
latest: XMap<string, MonitorLatestType<"pm25" | "o3">> | null = $state(null);
|
|
28
|
-
list: Array<MonitorData> | null = $state(null);
|
|
29
|
-
|
|
30
|
-
meta: MonitorsMeta | null = $state(null);
|
|
31
|
-
|
|
32
|
-
pollutant: "pm25" | "o3" | null = $state(null);
|
|
33
|
-
|
|
34
|
-
levels: Array<SJVAirEntryLevel> | null = $derived(
|
|
35
|
-
this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
#initPromise: Promise<void> | null = null;
|
|
39
|
-
#requestedPollutant: MonitorsPollutant | null = null;
|
|
40
|
-
#requestedAutoUpdate = false;
|
|
41
|
-
|
|
42
|
-
constructor() {
|
|
43
|
-
// Refetch monitors when the pollutant changes after the initial load, wherever the change
|
|
44
|
-
// comes from (Pollutant menu, URL sync, a host's own UI). Clearing first blanks the markers
|
|
45
|
-
// instead of briefly coloring them with the previous pollutant's readings.
|
|
46
|
-
let fetchedPollutant: string | null = null;
|
|
47
|
-
$effect.root(() => {
|
|
48
|
-
$effect(() => {
|
|
49
|
-
const pollutant = this.pollutant;
|
|
50
|
-
if (!this.initialized) return;
|
|
51
|
-
if (fetchedPollutant !== null && fetchedPollutant !== pollutant) {
|
|
52
|
-
untrack(() => {
|
|
53
|
-
this.list = [];
|
|
54
|
-
this.latest = null;
|
|
55
|
-
this.update();
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
fetchedPollutant = pollutant;
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
11
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* unless every call sharing it passed `autoUpdate: false`. A no-op once initialized, so it
|
|
68
|
-
* never restarts polling either: that is `resumeAutoUpdate()`.
|
|
12
|
+
* Stops waiting: the returned promise rejects with the signal's reason. The shared run keeps
|
|
13
|
+
* going for the other calls waiting on it, and is cancelled only when every call waiting on it
|
|
14
|
+
* passed a signal and all of those aborted. Ignored once initialized.
|
|
69
15
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (options.autoUpdate !== false) this.#requestedAutoUpdate = true;
|
|
74
|
-
|
|
75
|
-
if (urlPollutant === "pm25" || urlPollutant === "o3") {
|
|
76
|
-
this.#requestedPollutant = urlPollutant;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.#initPromise ??= this.#runInit().finally(() => {
|
|
80
|
-
this.#initPromise = null;
|
|
81
|
-
this.#requestedPollutant = null;
|
|
82
|
-
this.#requestedAutoUpdate = false;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
return this.#initPromise;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async #runInit(): Promise<void> {
|
|
89
|
-
// The pollutant the latest readings were requested for.
|
|
90
|
-
let fetchedPollutant = this.#requestedPollutant;
|
|
91
|
-
|
|
92
|
-
const metaRequest = getMonitorsMeta();
|
|
93
|
-
const listAndMeta = Promise.all([getMonitorsList(), metaRequest]).then(([list, meta]) => {
|
|
94
|
-
this.list = list;
|
|
95
|
-
this.meta = meta;
|
|
96
|
-
return meta;
|
|
97
|
-
});
|
|
98
|
-
// The latest readings only need the pollutant, so they never wait for the list. An explicit
|
|
99
|
-
// pollutant may still arrive before meta does, in which case the default is never fetched.
|
|
100
|
-
const latestRequest = fetchedPollutant
|
|
101
|
-
? getMonitorsLatestMap(fetchedPollutant)
|
|
102
|
-
: metaRequest.then((meta) => {
|
|
103
|
-
const pollutant = this.#requestedPollutant ?? meta.default_pollutant;
|
|
104
|
-
fetchedPollutant = pollutant;
|
|
105
|
-
return getMonitorsLatestMap(pollutant);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// One Promise.all so a failure in either never leaves the other unhandled.
|
|
109
|
-
const [meta, firstLatest] = await Promise.all([listAndMeta, latestRequest]);
|
|
110
|
-
let latest = firstLatest;
|
|
111
|
-
|
|
112
|
-
// An explicit pollutant may have arrived while the requests above were in flight.
|
|
113
|
-
let wanted = this.#requestedPollutant ?? meta.default_pollutant;
|
|
114
|
-
while (wanted !== fetchedPollutant) {
|
|
115
|
-
latest = await getMonitorsLatestMap(wanted);
|
|
116
|
-
fetchedPollutant = wanted;
|
|
117
|
-
wanted = this.#requestedPollutant ?? meta.default_pollutant;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
this.pollutant = fetchedPollutant;
|
|
121
|
-
this.latest = latest;
|
|
122
|
-
if (this.#requestedAutoUpdate) this.autoUpdate.start();
|
|
123
|
-
this.initialized = true;
|
|
124
|
-
}
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}
|
|
125
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The live monitors data, as hosts have used it since 4.x: a thin view over map-sdk's monitors
|
|
21
|
+
* store, which owns the data, the shared init and the polling.
|
|
22
|
+
*/
|
|
23
|
+
export interface MonitorsManager {
|
|
24
|
+
/** Loads the monitors. Only "pm25" and "o3" pass through; anything else means the default. */
|
|
25
|
+
init(pollutant?: unknown, options?: MonitorsInitOptions): Promise<void>;
|
|
26
|
+
readonly initialized: boolean;
|
|
27
|
+
readonly meta: MonitorsMeta | null;
|
|
126
28
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* be stale. A no-op before init or while polling.
|
|
29
|
+
* Every monitor, or null until initialized. The same array between reads until the store's
|
|
30
|
+
* monitors are replaced.
|
|
130
31
|
*/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const pollutant = this.pollutant;
|
|
142
|
-
const [list, latest] = await Promise.all([
|
|
143
|
-
getMonitorsList(),
|
|
144
|
-
getMonitorsLatestMap(pollutant || this.meta?.default_pollutant || "pm25")
|
|
145
|
-
]);
|
|
146
|
-
|
|
147
|
-
// The pollutant changed while this was in flight: these readings belong to the previous
|
|
148
|
-
// one, and the change has already started its own update.
|
|
149
|
-
if (this.pollutant !== pollutant) return;
|
|
150
|
-
|
|
151
|
-
this.list = list;
|
|
152
|
-
this.latest = latest;
|
|
153
|
-
}
|
|
32
|
+
readonly list: MonitorData[] | null;
|
|
33
|
+
/** The pollutant the readings are for, or null before init. */
|
|
34
|
+
readonly pollutant: string | null;
|
|
35
|
+
setPollutant(pollutant: "pm25" | "o3"): void;
|
|
36
|
+
update(options?: { signal?: AbortSignal }): Promise<void>;
|
|
37
|
+
resumeAutoUpdate(): void;
|
|
38
|
+
pauseAutoUpdate(): void;
|
|
39
|
+
/** The store underneath, for map-sdk plugins and stores. */
|
|
40
|
+
readonly store: MonitorsStore;
|
|
154
41
|
}
|
|
155
42
|
|
|
156
|
-
|
|
157
|
-
pollutant
|
|
158
|
-
|
|
159
|
-
const monitors = await getMonitors(pollutant);
|
|
160
|
-
const latest = new XMap<string, MonitorLatestType<"pm25" | "o3">>();
|
|
161
|
-
|
|
162
|
-
for (const monitor of monitors) {
|
|
163
|
-
latest.set(monitor.id, monitor);
|
|
164
|
-
}
|
|
43
|
+
function initPollutant(pollutant: unknown): "pm25" | "o3" | undefined {
|
|
44
|
+
return pollutant === "pm25" || pollutant === "o3" ? pollutant : undefined;
|
|
45
|
+
}
|
|
165
46
|
|
|
166
|
-
|
|
47
|
+
export function createMonitorsManager(store: MonitorsStore): MonitorsManager {
|
|
48
|
+
// The list built from `store.monitors`, which the store replaces rather than mutates.
|
|
49
|
+
let listFrom: MonitorsStore["monitors"] | null = null;
|
|
50
|
+
let list: MonitorData[] = [];
|
|
51
|
+
return {
|
|
52
|
+
init: (pollutant, options) => store.init(initPollutant(pollutant), options),
|
|
53
|
+
get initialized() {
|
|
54
|
+
return store.initialized;
|
|
55
|
+
},
|
|
56
|
+
get meta() {
|
|
57
|
+
return store.meta;
|
|
58
|
+
},
|
|
59
|
+
get list() {
|
|
60
|
+
if (!store.initialized) return null;
|
|
61
|
+
const monitors = store.monitors;
|
|
62
|
+
if (monitors !== listFrom) {
|
|
63
|
+
listFrom = monitors;
|
|
64
|
+
list = [...monitors.values()];
|
|
65
|
+
}
|
|
66
|
+
return list;
|
|
67
|
+
},
|
|
68
|
+
get pollutant() {
|
|
69
|
+
return store.entryType;
|
|
70
|
+
},
|
|
71
|
+
setPollutant: (pollutant) => store.entryTypeFor(pollutant),
|
|
72
|
+
update: (options) => store.update(options),
|
|
73
|
+
resumeAutoUpdate: () => store.resumeAutoUpdate(),
|
|
74
|
+
pauseAutoUpdate: () => store.pauseAutoUpdate(),
|
|
75
|
+
store
|
|
76
|
+
};
|
|
167
77
|
}
|
|
168
78
|
|
|
169
|
-
export const monitorsManager =
|
|
170
|
-
export type { MonitorsManager };
|
|
79
|
+
export const monitorsManager: MonitorsManager = createMonitorsManager(monitorsStore);
|