@sjvair/monitor-map 3.6.3 → 4.0.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/README.md +2 -2
- package/dist/lib/MonitorMapLayout.svelte +74 -36
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +3 -0
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +15 -1
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +27 -6
- package/dist/lib/hms/hms-fire-icon-manager.d.ts +8 -0
- package/dist/lib/hms/hms-fire-icon-manager.js +10 -1
- package/dist/lib/hms/hms-fire-map-integration.svelte.js +1 -1
- package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +20 -0
- package/dist/lib/hms/hms-smoke-map-integration.svelte.js +22 -7
- package/dist/lib/index.d.ts +21 -7
- package/dist/lib/index.js +22 -7
- package/dist/lib/legend/Legend.svelte +40 -0
- package/dist/lib/legend/Legend.svelte.d.ts +7 -0
- package/dist/lib/legend/LegendBar.svelte +45 -0
- package/dist/lib/legend/LegendBar.svelte.d.ts +7 -0
- package/dist/lib/legend/legend-data.d.ts +40 -0
- package/dist/lib/legend/legend-data.js +65 -0
- package/dist/lib/map/MapShell.svelte +7 -6
- package/dist/lib/map/MapShell.svelte.d.ts +3 -1
- package/dist/lib/map/basemap.d.ts +14 -0
- package/dist/lib/map/basemap.js +10 -0
- package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +6 -0
- package/dist/lib/map/integrations/map-geojson-integration.svelte.js +15 -2
- package/dist/lib/map/integrations/map-layer-integration.svelte.js +7 -3
- package/dist/lib/map/map-style-state.svelte.d.ts +13 -0
- package/dist/lib/map/map-style-state.svelte.js +35 -0
- package/dist/lib/map/map.svelte.d.ts +6 -0
- package/dist/lib/map/map.svelte.js +6 -0
- package/dist/lib/monitors/monitor-groups.d.ts +11 -0
- package/dist/lib/monitors/monitor-groups.js +7 -0
- package/dist/lib/monitors/monitor-utils.js +1 -3
- package/dist/lib/monitors/monitors-icon-manager.svelte.js +2 -5
- package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +4 -4
- package/dist/lib/monitors/monitors-map-integration.svelte.js +43 -30
- package/dist/lib/monitors/monitors.svelte.d.ts +1 -0
- package/dist/lib/monitors/monitors.svelte.js +22 -0
- package/dist/lib/options/OptionsBar.svelte +96 -0
- package/dist/lib/options/OptionsBar.svelte.d.ts +10 -0
- package/dist/lib/options/OptionsGroup.svelte +12 -0
- package/dist/lib/options/OptionsGroup.svelte.d.ts +7 -0
- package/dist/lib/options/OptionsMenu.svelte +71 -0
- package/dist/lib/options/OptionsMenu.svelte.d.ts +15 -0
- package/dist/lib/options/group-state.d.ts +7 -0
- package/dist/lib/options/group-state.js +15 -0
- package/dist/lib/options/layout.d.ts +7 -0
- package/dist/lib/options/layout.js +9 -0
- package/dist/lib/options/map-option-defaults.d.ts +56 -0
- package/dist/lib/options/map-option-defaults.js +47 -0
- package/dist/lib/options/menus/LayersMenu.svelte +47 -0
- package/dist/lib/options/menus/LayersMenu.svelte.d.ts +3 -0
- package/dist/lib/options/menus/MonitorsMenu.svelte +68 -0
- package/dist/lib/options/menus/MonitorsMenu.svelte.d.ts +3 -0
- package/dist/lib/options/menus/OverlaysMenu.svelte +18 -0
- package/dist/lib/{MapLegend.svelte.d.ts → options/menus/OverlaysMenu.svelte.d.ts} +3 -3
- package/dist/lib/options/menus/PollutantMenu.svelte +79 -0
- package/dist/lib/options/menus/PollutantMenu.svelte.d.ts +3 -0
- package/dist/lib/options/menus/SettingsMenu.svelte +73 -0
- package/dist/lib/{hms/components/HMSFireLegend.svelte.d.ts → options/menus/SettingsMenu.svelte.d.ts} +3 -3
- package/dist/lib/options/options-state.svelte.d.ts +12 -0
- package/dist/lib/options/options-state.svelte.js +42 -0
- package/dist/lib/options/pollutant-param.d.ts +17 -0
- package/dist/lib/options/pollutant-param.js +14 -0
- package/dist/lib/options/reset-map-options.d.ts +2 -0
- package/dist/lib/options/reset-map-options.js +24 -0
- package/dist/lib/options/rows/CheckboxRow.svelte +51 -0
- package/dist/lib/options/rows/CheckboxRow.svelte.d.ts +13 -0
- package/dist/lib/options/rows/GroupRow.svelte +43 -0
- package/dist/lib/options/rows/GroupRow.svelte.d.ts +17 -0
- package/dist/lib/options/rows/MarkerIcon.svelte +10 -0
- package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +8 -0
- package/dist/lib/options/rows/RadioRow.svelte +33 -0
- package/dist/lib/options/rows/RadioRow.svelte.d.ts +13 -0
- package/dist/lib/options/rows/SubmenuRow.svelte +22 -0
- package/dist/lib/options/rows/SubmenuRow.svelte.d.ts +11 -0
- package/dist/lib/search/Search.svelte +6 -6
- package/package.json +5 -3
- package/src/lib/MonitorMapLayout.svelte +74 -36
- package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +17 -1
- package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +26 -6
- package/src/lib/hms/hms-fire-icon-manager.ts +16 -2
- package/src/lib/hms/hms-fire-map-integration.svelte.ts +1 -1
- package/src/lib/hms/hms-smoke-map-integration.svelte.ts +25 -7
- package/src/lib/index.ts +31 -7
- package/src/lib/legend/Legend.svelte +40 -0
- package/src/lib/legend/LegendBar.svelte +45 -0
- package/src/lib/legend/legend-data.ts +101 -0
- package/src/lib/map/MapShell.svelte +7 -6
- package/src/lib/map/basemap.ts +24 -0
- package/src/lib/map/integrations/map-geojson-integration.svelte.ts +15 -2
- package/src/lib/map/integrations/map-layer-integration.svelte.ts +6 -3
- package/src/lib/map/map-style-state.svelte.ts +42 -0
- package/src/lib/map/map.svelte.ts +6 -0
- package/src/lib/monitors/monitor-groups.ts +20 -0
- package/src/lib/monitors/monitor-utils.ts +1 -5
- package/src/lib/monitors/monitors-icon-manager.svelte.ts +2 -8
- package/src/lib/monitors/monitors-map-integration.svelte.ts +37 -27
- package/src/lib/monitors/monitors.svelte.ts +22 -0
- package/src/lib/options/OptionsBar.svelte +96 -0
- package/src/lib/options/OptionsGroup.svelte +12 -0
- package/src/lib/options/OptionsMenu.svelte +71 -0
- package/src/lib/options/group-state.ts +16 -0
- package/src/lib/options/layout.ts +10 -0
- package/src/lib/options/map-option-defaults.ts +71 -0
- package/src/lib/options/menus/LayersMenu.svelte +47 -0
- package/src/lib/options/menus/MonitorsMenu.svelte +68 -0
- package/src/lib/options/menus/OverlaysMenu.svelte +18 -0
- package/src/lib/options/menus/PollutantMenu.svelte +79 -0
- package/src/lib/options/menus/SettingsMenu.svelte +73 -0
- package/src/lib/options/options-state.svelte.ts +48 -0
- package/src/lib/options/pollutant-param.ts +35 -0
- package/src/lib/options/reset-map-options.ts +28 -0
- package/src/lib/options/rows/CheckboxRow.svelte +51 -0
- package/src/lib/options/rows/GroupRow.svelte +43 -0
- package/src/lib/options/rows/MarkerIcon.svelte +10 -0
- package/src/lib/options/rows/RadioRow.svelte +33 -0
- package/src/lib/options/rows/SubmenuRow.svelte +22 -0
- package/src/lib/search/Search.svelte +6 -6
- package/dist/lib/MapLegend.svelte +0 -15
- package/dist/lib/collocation-sites/collocation-utils.d.ts +0 -3
- package/dist/lib/collocation-sites/collocation-utils.js +0 -30
- package/dist/lib/components/DisplayOption.svelte +0 -7
- package/dist/lib/components/DisplayOption.svelte.d.ts +0 -5
- package/dist/lib/components/MapLayersDisplayOptions.svelte +0 -42
- package/dist/lib/components/MapLayersDisplayOptions.svelte.d.ts +0 -18
- package/dist/lib/components/SegmentedControl.svelte +0 -52
- package/dist/lib/components/SegmentedControl.svelte.d.ts +0 -11
- package/dist/lib/components/ToggleSwitch.svelte +0 -93
- package/dist/lib/components/ToggleSwitch.svelte.d.ts +0 -9
- package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
- package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte.d.ts +0 -18
- package/dist/lib/hms/components/HMSFireLegend.svelte +0 -21
- package/dist/lib/map/MapStyleDisplayOptions.svelte +0 -61
- package/dist/lib/map/MapStyleDisplayOptions.svelte.d.ts +0 -3
- package/dist/lib/map/Menu.svelte +0 -65
- package/dist/lib/map/Menu.svelte.d.ts +0 -5
- package/dist/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
- package/dist/lib/monitors/components/MonitorMarkersLegend.svelte.d.ts +0 -3
- package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte +0 -85
- package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte.d.ts +0 -3
- package/src/lib/MapLegend.svelte +0 -15
- package/src/lib/collocation-sites/collocation-utils.ts +0 -41
- package/src/lib/components/DisplayOption.svelte +0 -7
- package/src/lib/components/MapLayersDisplayOptions.svelte +0 -42
- package/src/lib/components/SegmentedControl.svelte +0 -52
- package/src/lib/components/ToggleSwitch.svelte +0 -93
- package/src/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
- package/src/lib/hms/components/HMSFireLegend.svelte +0 -21
- package/src/lib/map/MapStyleDisplayOptions.svelte +0 -61
- package/src/lib/map/Menu.svelte +0 -65
- package/src/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
- package/src/lib/monitors/components/MonitorsDisplayOptions.svelte +0 -85
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Monitor display-option keys shown under each Monitors menu group, in display order. */
|
|
2
|
+
export const REFERENCE_GRADE_TYPES = ["airnow", "aqlite", "aqview", "bam1022"] as const;
|
|
3
|
+
export const LOW_COST_TYPES = ["airgradient", "purpleair", "vozbox"] as const;
|
|
4
|
+
|
|
5
|
+
export type MonitorGroupType =
|
|
6
|
+
(typeof REFERENCE_GRADE_TYPES)[number] | (typeof LOW_COST_TYPES)[number];
|
|
7
|
+
|
|
8
|
+
/** The slice of MonitorsMeta needed to know which pollutants a monitor type reports. */
|
|
9
|
+
export interface MonitorTypeMeta {
|
|
10
|
+
monitors: Record<string, { entries: Record<string, unknown> } | undefined>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function typeSupportsPollutant(
|
|
14
|
+
meta: MonitorTypeMeta | null,
|
|
15
|
+
type: string,
|
|
16
|
+
pollutant: string | null
|
|
17
|
+
): boolean {
|
|
18
|
+
const deviceMeta = meta?.monitors[type];
|
|
19
|
+
return !!deviceMeta && !!pollutant && pollutant in deviceMeta.entries;
|
|
20
|
+
}
|
|
@@ -40,18 +40,14 @@ export function getOrder(monitor: MonitorData): number {
|
|
|
40
40
|
|
|
41
41
|
export function getTypeShape(type: string): string {
|
|
42
42
|
switch (type) {
|
|
43
|
-
case "airgradient":
|
|
44
|
-
return "circle";
|
|
45
|
-
|
|
46
43
|
case "airnow":
|
|
47
44
|
case "aqview":
|
|
48
45
|
case "bam1022":
|
|
49
46
|
case "aqlite":
|
|
50
47
|
return "triangle";
|
|
51
48
|
|
|
49
|
+
case "airgradient":
|
|
52
50
|
case "purpleair":
|
|
53
|
-
return "square";
|
|
54
|
-
|
|
55
51
|
case "vozbox":
|
|
56
52
|
return "circle";
|
|
57
53
|
|
|
@@ -13,23 +13,17 @@ export function getIconId<T extends MonitorData>(monitor: T, level: SJVAirEntryL
|
|
|
13
13
|
const id = `${monitor.location}-${monitor.is_active ? level.name : "default"}`;
|
|
14
14
|
|
|
15
15
|
switch (monitor.type) {
|
|
16
|
-
case "airgradient":
|
|
17
|
-
return `${id}-circle`;
|
|
18
|
-
|
|
19
16
|
case "airnow":
|
|
20
17
|
case "aqview":
|
|
21
18
|
case "bam1022":
|
|
19
|
+
case "aqlite":
|
|
22
20
|
return `${id}-triangle`;
|
|
23
21
|
|
|
22
|
+
case "airgradient":
|
|
24
23
|
case "purpleair":
|
|
25
|
-
return `${id}-${monitor.is_sjvair ? "circle" : "square"}`;
|
|
26
|
-
|
|
27
24
|
case "vozbox":
|
|
28
25
|
return `${id}-circle`;
|
|
29
26
|
|
|
30
|
-
case "aqlite":
|
|
31
|
-
return `${id}-triangle`;
|
|
32
|
-
|
|
33
27
|
default:
|
|
34
28
|
throw new Error(`Map icon for ${monitor.device} has not been set`);
|
|
35
29
|
}
|
|
@@ -20,12 +20,6 @@ import type { MonitorMapFeature, MonitorMarkerProperties, MonitorsDataSource } f
|
|
|
20
20
|
const filters = {
|
|
21
21
|
monitor(deviceType: MonitorType): ExpressionSpecification {
|
|
22
22
|
return ["==", ["get", "type"], deviceType];
|
|
23
|
-
},
|
|
24
|
-
purpleair(): ExpressionSpecification {
|
|
25
|
-
return ["all", ["==", ["get", "type"], "purpleair"], ["==", ["get", "is_sjvair"], false]];
|
|
26
|
-
},
|
|
27
|
-
sjvPurpleair(): ExpressionSpecification {
|
|
28
|
-
return ["all", ["==", ["get", "type"], "purpleair"], ["==", ["get", "is_sjvair"], true]];
|
|
29
23
|
}
|
|
30
24
|
};
|
|
31
25
|
|
|
@@ -39,13 +33,17 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
39
33
|
private renderer: MonitorsClusterRenderer = new MonitorsClusterRenderer(this);
|
|
40
34
|
|
|
41
35
|
displayOptions = $derived.by(() => ({
|
|
42
|
-
purpleair: new MapDisplayOption("PurpleAir", true, this.icons.get("outside-display-square")),
|
|
43
|
-
sjvair: new MapDisplayOption("SJVAir non-FEM", true, this.icons.get("outside-display-circle")),
|
|
44
|
-
aqview: new MapDisplayOption("AQview", true, this.icons.get("outside-display-triangle")),
|
|
45
|
-
bam1022: new MapDisplayOption("SJVAir FEM", true, this.icons.get("outside-display-triangle")),
|
|
46
36
|
airnow: new MapDisplayOption("AirNow", true, this.icons.get("outside-display-triangle")),
|
|
47
|
-
vozbox: new MapDisplayOption("VOZbox", true, this.icons.get("outside-display-circle")),
|
|
48
37
|
aqlite: new MapDisplayOption("AQLite", true, this.icons.get("outside-display-triangle")),
|
|
38
|
+
aqview: new MapDisplayOption("AQview", true, this.icons.get("outside-display-triangle")),
|
|
39
|
+
bam1022: new MapDisplayOption("SJVAir", true, this.icons.get("outside-display-triangle")),
|
|
40
|
+
airgradient: new MapDisplayOption(
|
|
41
|
+
"AirGradient",
|
|
42
|
+
true,
|
|
43
|
+
this.icons.get("outside-display-circle")
|
|
44
|
+
),
|
|
45
|
+
purpleair: new MapDisplayOption("PurpleAir", true, this.icons.get("outside-display-circle")),
|
|
46
|
+
vozbox: new MapDisplayOption("VOZbox", true, this.icons.get("outside-display-circle")),
|
|
49
47
|
inactive: new MapDisplayOption("Inactive", false, this.icons.get("outside-default-square")),
|
|
50
48
|
inside: new MapDisplayOption("Inside", false, this.icons.get("inside-display-square"))
|
|
51
49
|
}));
|
|
@@ -115,14 +113,13 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
115
113
|
];
|
|
116
114
|
const statusFilters: ExpressionSpecification = ["any", ["==", ["get", "is_active"], true]];
|
|
117
115
|
|
|
118
|
-
if (this.displayOptions.
|
|
116
|
+
if (this.displayOptions.airnow.value) monitorFilters.push(filters.monitor("airnow"));
|
|
117
|
+
if (this.displayOptions.aqlite.value) monitorFilters.push(filters.monitor("aqlite"));
|
|
119
118
|
if (this.displayOptions.aqview.value) monitorFilters.push(filters.monitor("aqview"));
|
|
120
119
|
if (this.displayOptions.bam1022.value) monitorFilters.push(filters.monitor("bam1022"));
|
|
121
|
-
if (this.displayOptions.
|
|
120
|
+
if (this.displayOptions.airgradient.value) monitorFilters.push(filters.monitor("airgradient"));
|
|
121
|
+
if (this.displayOptions.purpleair.value) monitorFilters.push(filters.monitor("purpleair"));
|
|
122
122
|
if (this.displayOptions.vozbox.value) monitorFilters.push(filters.monitor("vozbox"));
|
|
123
|
-
if (this.displayOptions.aqlite.value) monitorFilters.push(filters.monitor("aqlite"));
|
|
124
|
-
if (this.displayOptions.sjvair.value)
|
|
125
|
-
monitorFilters.push(filters.sjvPurpleair(), filters.monitor("airgradient"));
|
|
126
123
|
if (this.displayOptions.inside.value)
|
|
127
124
|
locationFilters.push(["==", ["get", "location"], "inside"]);
|
|
128
125
|
if (this.displayOptions.inactive.value) statusFilters.push(["==", ["get", "is_active"], false]);
|
|
@@ -131,8 +128,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
131
128
|
});
|
|
132
129
|
|
|
133
130
|
// Groups features by monitor type for per-type cluster sources, applying display option
|
|
134
|
-
// filters so cluster aggregates only include visible monitors.
|
|
135
|
-
// to "airgradient" since they share the same shape (circle).
|
|
131
|
+
// filters so cluster aggregates only include visible monitors.
|
|
136
132
|
featuresByType: Record<string, MonitorMapFeature[]> = $derived.by(() => {
|
|
137
133
|
const opts = this.displayOptions;
|
|
138
134
|
const byType: Record<string, MonitorMapFeature[]> = {};
|
|
@@ -144,20 +140,19 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
144
140
|
if (p.location === "inside" && !opts.inside.value) continue;
|
|
145
141
|
|
|
146
142
|
const typeVisible: Partial<Record<MonitorType, boolean>> = {
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
airnow: opts.airnow.value,
|
|
144
|
+
aqlite: opts.aqlite.value,
|
|
149
145
|
aqview: opts.aqview.value,
|
|
150
146
|
bam1022: opts.bam1022.value,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
airgradient: opts.airgradient.value,
|
|
148
|
+
purpleair: opts.purpleair.value,
|
|
149
|
+
vozbox: opts.vozbox.value
|
|
154
150
|
};
|
|
155
151
|
if (!(typeVisible[p.type] ?? true)) continue;
|
|
156
152
|
|
|
157
|
-
const
|
|
158
|
-
const arr = byType[key] ?? [];
|
|
153
|
+
const arr = byType[p.type] ?? [];
|
|
159
154
|
arr.push(feat);
|
|
160
|
-
byType[
|
|
155
|
+
byType[p.type] = arr;
|
|
161
156
|
}
|
|
162
157
|
return byType;
|
|
163
158
|
});
|
|
@@ -200,12 +195,20 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
200
195
|
this.icons = new MonitorsIconManager(dataSource);
|
|
201
196
|
|
|
202
197
|
$effect.root(() => {
|
|
198
|
+
// Every effect below early-returns while disabled (Pollutant = None) so a data refresh
|
|
199
|
+
// can't re-add the layer. `enabled` is read untracked: the base class's own effect
|
|
200
|
+
// already applies/removes on enabled changes, and tracking it here would apply twice.
|
|
201
|
+
// The guard comes after each effect's tracked reads so its dependencies are always
|
|
202
|
+
// collected; returning before reading anything would leave the effect permanently dead.
|
|
203
|
+
|
|
203
204
|
// Push filter changes to the active layer(s) imperatively
|
|
204
205
|
$effect(() => {
|
|
205
206
|
const filter = this.filters;
|
|
207
|
+
const clustered = this.clustered;
|
|
206
208
|
if (!mapManager.map) return;
|
|
209
|
+
if (!untrack(() => this.enabled)) return;
|
|
207
210
|
|
|
208
|
-
if (
|
|
211
|
+
if (clustered) {
|
|
209
212
|
this.renderer.syncFilter();
|
|
210
213
|
} else {
|
|
211
214
|
if (mapManager.map.getLayer(this.referenceId)) {
|
|
@@ -218,6 +221,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
218
221
|
$effect(() => {
|
|
219
222
|
const features = this.features;
|
|
220
223
|
if (!mapManager.map || this.clustered) return;
|
|
224
|
+
if (!untrack(() => this.enabled)) return;
|
|
221
225
|
mapManager.setDataSource(this.referenceId, features);
|
|
222
226
|
});
|
|
223
227
|
|
|
@@ -225,6 +229,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
225
229
|
$effect(() => {
|
|
226
230
|
void this.featuresByType;
|
|
227
231
|
if (!mapManager.map || !this.clustered) return;
|
|
232
|
+
if (!untrack(() => this.enabled)) return;
|
|
228
233
|
this.renderer.syncFeatures();
|
|
229
234
|
});
|
|
230
235
|
|
|
@@ -232,6 +237,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
232
237
|
$effect(() => {
|
|
233
238
|
const thresholds = this.clusterIconThresholds;
|
|
234
239
|
if (!mapManager.map || !this.clustered || !thresholds.length) return;
|
|
240
|
+
if (!untrack(() => this.enabled)) return;
|
|
235
241
|
this.renderer.syncThresholds();
|
|
236
242
|
});
|
|
237
243
|
|
|
@@ -242,6 +248,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
242
248
|
void this.clustered;
|
|
243
249
|
const hasFeatures = Object.keys(this.featuresByType).length > 0;
|
|
244
250
|
if (!untrack(() => mapManager.map) || !hasFeatures) return;
|
|
251
|
+
if (!untrack(() => this.enabled)) return;
|
|
245
252
|
untrack(() => this.apply());
|
|
246
253
|
});
|
|
247
254
|
|
|
@@ -268,6 +275,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
268
275
|
this.tooltipManager.disable();
|
|
269
276
|
this.renderer.remove();
|
|
270
277
|
this.icons.loadIcons().then(() => {
|
|
278
|
+
if (mapManager.styleLoading) return;
|
|
271
279
|
this.renderer.apply(this.handleMonitorClick);
|
|
272
280
|
this.tooltipManager.enable();
|
|
273
281
|
this.applySelectedState();
|
|
@@ -279,6 +287,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
279
287
|
clickManager.unregister([this.referenceId]);
|
|
280
288
|
super.apply();
|
|
281
289
|
this.icons.loadIcons().then(() => {
|
|
290
|
+
if (mapManager.styleLoading) return;
|
|
282
291
|
clickManager.register([this.referenceId], this.handleMonitorClick);
|
|
283
292
|
this.applySelectedState();
|
|
284
293
|
});
|
|
@@ -286,6 +295,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
286
295
|
}
|
|
287
296
|
|
|
288
297
|
remove() {
|
|
298
|
+
this.tooltipManager.disable();
|
|
289
299
|
clickManager.unregister([this.referenceId]);
|
|
290
300
|
this.renderer.remove();
|
|
291
301
|
super.remove();
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@sjvair/sdk";
|
|
10
10
|
import { XMap } from "@tstk/builtin-extensions";
|
|
11
11
|
import { Interval } from "@tstk/utils";
|
|
12
|
+
import { untrack } from "svelte";
|
|
12
13
|
import type { MonitorsDataSource } from "./types";
|
|
13
14
|
|
|
14
15
|
class MonitorsManager implements MonitorsDataSource {
|
|
@@ -26,6 +27,27 @@ class MonitorsManager implements MonitorsDataSource {
|
|
|
26
27
|
this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null
|
|
27
28
|
);
|
|
28
29
|
|
|
30
|
+
constructor() {
|
|
31
|
+
// Refetch monitors when the pollutant changes after the initial load, wherever the change
|
|
32
|
+
// comes from (Pollutant menu, URL sync, a host's own UI). Clearing first blanks the markers
|
|
33
|
+
// instead of briefly coloring them with the previous pollutant's readings.
|
|
34
|
+
let fetchedPollutant: string | null = null;
|
|
35
|
+
$effect.root(() => {
|
|
36
|
+
$effect(() => {
|
|
37
|
+
const pollutant = this.pollutant;
|
|
38
|
+
if (!this.initialized) return;
|
|
39
|
+
if (fetchedPollutant !== null && fetchedPollutant !== pollutant) {
|
|
40
|
+
untrack(() => {
|
|
41
|
+
this.list = [];
|
|
42
|
+
this.latest = null;
|
|
43
|
+
this.update();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
fetchedPollutant = pollutant;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
async init(urlPollutant?: string | number | boolean): Promise<void> {
|
|
30
52
|
if (this.initialized) return;
|
|
31
53
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { Attachment } from "svelte/attachments";
|
|
4
|
+
import { MenuIcon, XIcon } from "@lucide/svelte";
|
|
5
|
+
import { provideOptionsMenus } from "./options-state.svelte.js";
|
|
6
|
+
import { isWideLayout, WIDE_LAYOUT_QUERY } from "./layout";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
/** OptionsGroup / OptionsMenu content */
|
|
10
|
+
children?: Snippet;
|
|
11
|
+
/** Rendered last in the toolbar (wide) or beside the menu button (narrow) */
|
|
12
|
+
search?: Snippet;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { children, search }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const menus = provideOptionsMenus();
|
|
18
|
+
const panelId = "monitor-map-options-panel";
|
|
19
|
+
let panelOpen = $state(false);
|
|
20
|
+
|
|
21
|
+
// Toolbar only: clicks outside close any click-opened dropdown; clicks inside never do.
|
|
22
|
+
const closeOnOutsideClick: Attachment<HTMLElement> = (node) => {
|
|
23
|
+
function handler(e: MouseEvent) {
|
|
24
|
+
if (isWideLayout() && !node.contains(e.target as Node)) menus.closeAll();
|
|
25
|
+
}
|
|
26
|
+
document.addEventListener("click", handler);
|
|
27
|
+
return () => document.removeEventListener("click", handler);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Open menus mean different things in the toolbar (dropdowns) and the panel (expanded
|
|
31
|
+
// sections), so crossing the breakpoint (e.g. rotating a phone) starts from a closed state.
|
|
32
|
+
$effect(() => {
|
|
33
|
+
const query = window.matchMedia(WIDE_LAYOUT_QUERY);
|
|
34
|
+
function onchange() {
|
|
35
|
+
menus.closeAll();
|
|
36
|
+
panelOpen = false;
|
|
37
|
+
}
|
|
38
|
+
query.addEventListener("change", onchange);
|
|
39
|
+
return () => query.removeEventListener("change", onchange);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function onkeydown(e: KeyboardEvent) {
|
|
43
|
+
if (e.key !== "Escape") return;
|
|
44
|
+
panelOpen = false;
|
|
45
|
+
if (isWideLayout()) menus.closeAll();
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<svelte:window {onkeydown} />
|
|
50
|
+
|
|
51
|
+
<!-- Covers the map area so the narrow panel can fill it; passes pointer events through. -->
|
|
52
|
+
<div class="pointer-events-none absolute inset-0 z-10">
|
|
53
|
+
<div
|
|
54
|
+
{@attach closeOnOutsideClick}
|
|
55
|
+
class="md:absolute md:top-4 md:right-16 md:left-4 md:flex md:flex-wrap md:items-start md:gap-2"
|
|
56
|
+
>
|
|
57
|
+
<div
|
|
58
|
+
class="pointer-events-auto flex items-start gap-2 max-md:absolute max-md:top-4 max-md:left-4 md:contents"
|
|
59
|
+
>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="flex size-10 shrink-0 items-center justify-center rounded-md bg-white shadow md:hidden"
|
|
63
|
+
aria-label="Open map options"
|
|
64
|
+
aria-expanded={panelOpen}
|
|
65
|
+
aria-controls={panelId}
|
|
66
|
+
onclick={() => (panelOpen = true)}
|
|
67
|
+
>
|
|
68
|
+
<MenuIcon size={22} />
|
|
69
|
+
</button>
|
|
70
|
+
{#if search}
|
|
71
|
+
<div class="md:order-last">{@render search()}</div>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
74
|
+
<div
|
|
75
|
+
id={panelId}
|
|
76
|
+
class={[
|
|
77
|
+
"pointer-events-auto md:contents",
|
|
78
|
+
"max-md:absolute max-md:inset-0 max-md:z-20 max-md:flex max-md:flex-col max-md:gap-2 max-md:overflow-y-auto max-md:bg-white max-md:p-4 max-md:transition-[translate,visibility] max-md:duration-200",
|
|
79
|
+
!panelOpen && "max-md:invisible max-md:-translate-x-full"
|
|
80
|
+
]}
|
|
81
|
+
>
|
|
82
|
+
<div class="mb-2 flex items-center justify-between md:hidden">
|
|
83
|
+
<p class="text-lg font-semibold">Map Options</p>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
class="flex size-9 items-center justify-center rounded-md hover:bg-gray-100"
|
|
87
|
+
aria-label="Close map options"
|
|
88
|
+
onclick={() => (panelOpen = false)}
|
|
89
|
+
>
|
|
90
|
+
<XIcon size={20} />
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
{@render children?.()}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
|
|
4
|
+
let { children }: { children: Snippet } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<!-- Joined button cluster on the toolbar; a plain stack of sections in the panel. -->
|
|
8
|
+
<div
|
|
9
|
+
class="flex flex-col gap-2 md:flex-row md:gap-0 md:divide-x md:divide-gray-200 md:rounded-md md:bg-white md:shadow"
|
|
10
|
+
>
|
|
11
|
+
{@render children()}
|
|
12
|
+
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import { ChevronDownIcon } from "@lucide/svelte";
|
|
4
|
+
import { useOptionsMenus } from "./options-state.svelte.js";
|
|
5
|
+
import { isWideLayout } from "./layout";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
icon: Snippet;
|
|
11
|
+
children: Snippet;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
/** Anchor the toolbar dropdown to the trigger's right edge */
|
|
14
|
+
alignRight?: boolean;
|
|
15
|
+
/** Show only the icon on the toolbar; the panel section still shows the label */
|
|
16
|
+
iconOnly?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
id,
|
|
21
|
+
label,
|
|
22
|
+
icon,
|
|
23
|
+
children,
|
|
24
|
+
disabled = false,
|
|
25
|
+
alignRight = false,
|
|
26
|
+
iconOnly = false
|
|
27
|
+
}: Props = $props();
|
|
28
|
+
|
|
29
|
+
const menus = useOptionsMenus();
|
|
30
|
+
const contentId = $derived(`monitor-map-options-${id}`);
|
|
31
|
+
const open = $derived(menus.isOpen(id) && !disabled);
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
role="presentation"
|
|
36
|
+
class="group/menu relative md:first:[&>button]:rounded-l-md md:last:[&>button]:rounded-r-md"
|
|
37
|
+
onmouseenter={() => {
|
|
38
|
+
if (isWideLayout()) menus.closeOthers(id);
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
{disabled}
|
|
44
|
+
aria-expanded={open}
|
|
45
|
+
aria-controls={contentId}
|
|
46
|
+
aria-label={iconOnly ? label : undefined}
|
|
47
|
+
class="flex h-10 w-full items-center gap-2 px-3 text-sm font-medium whitespace-nowrap select-none disabled:cursor-not-allowed max-md:rounded-md max-md:border max-md:border-gray-200 max-md:bg-white md:w-auto md:hover:bg-gray-100 md:disabled:hover:bg-transparent"
|
|
48
|
+
onclick={() => menus.toggle(id)}
|
|
49
|
+
>
|
|
50
|
+
<span class={["flex size-5 items-center justify-center", disabled && "opacity-50"]}>
|
|
51
|
+
{@render icon()}
|
|
52
|
+
</span>
|
|
53
|
+
<span class={[disabled && "opacity-50", iconOnly && "md:hidden"]}>{label}</span>
|
|
54
|
+
<span class={["ml-auto", disabled && "opacity-50", iconOnly && "md:hidden"]}>
|
|
55
|
+
<ChevronDownIcon size={16} />
|
|
56
|
+
</span>
|
|
57
|
+
</button>
|
|
58
|
+
<div
|
|
59
|
+
id={contentId}
|
|
60
|
+
class={[
|
|
61
|
+
"max-md:pt-1 md:absolute md:top-full md:z-20 md:min-w-64 md:pt-1",
|
|
62
|
+
alignRight ? "md:right-0" : "md:left-0",
|
|
63
|
+
open ? "block" : "hidden",
|
|
64
|
+
!disabled && "md:group-hover/menu:block md:group-has-[:focus-visible]/menu:block"
|
|
65
|
+
]}
|
|
66
|
+
>
|
|
67
|
+
<div class="py-1 md:max-h-[69vh] md:overflow-y-auto md:rounded-md md:bg-white md:shadow-lg">
|
|
68
|
+
{@render children()}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type GroupState = "checked" | "unchecked" | "indeterminate" | "empty";
|
|
2
|
+
|
|
3
|
+
/** Select-all state of a group, computed only from its currently visible rows. */
|
|
4
|
+
export function groupState(values: ReadonlyArray<boolean>): GroupState {
|
|
5
|
+
if (values.length === 0) return "empty";
|
|
6
|
+
const checkedCount = values.filter(Boolean).length;
|
|
7
|
+
if (checkedCount === 0) return "unchecked";
|
|
8
|
+
return checkedCount === values.length ? "checked" : "indeterminate";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Applies a select-all toggle to the given (visible) rows only. */
|
|
12
|
+
export function setGroup(options: ReadonlyArray<{ value: boolean }>, value: boolean): void {
|
|
13
|
+
for (const option of options) {
|
|
14
|
+
option.value = value;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tailwind's default `md` breakpoint (48rem). Layout itself is pure CSS (`md:` / `max-md:`);
|
|
3
|
+
* this is only read at event time for the few behaviors that differ between the toolbar and
|
|
4
|
+
* the options panel.
|
|
5
|
+
*/
|
|
6
|
+
export const WIDE_LAYOUT_QUERY = "(min-width: 48rem)";
|
|
7
|
+
|
|
8
|
+
export function isWideLayout(): boolean {
|
|
9
|
+
return window.matchMedia(WIDE_LAYOUT_QUERY).matches;
|
|
10
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { MonitorsPollutant } from "$lib/monitors/types";
|
|
2
|
+
import { applyPollutantSelection, type PollutantTargets } from "./pollutant-param";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Defaults restored by "Reset Map Options". Keep in sync with the constructor defaults in each
|
|
6
|
+
* integration (monitors/EV display options, `enabled`, `clustered`) so a reset matches a fresh load.
|
|
7
|
+
*/
|
|
8
|
+
export const MAP_OPTION_DEFAULTS = {
|
|
9
|
+
monitorTypes: {
|
|
10
|
+
airnow: true,
|
|
11
|
+
aqlite: true,
|
|
12
|
+
aqview: true,
|
|
13
|
+
bam1022: true,
|
|
14
|
+
airgradient: true,
|
|
15
|
+
purpleair: true,
|
|
16
|
+
vozbox: true,
|
|
17
|
+
inactive: false,
|
|
18
|
+
inside: false
|
|
19
|
+
},
|
|
20
|
+
clustered: true,
|
|
21
|
+
collocationSites: false,
|
|
22
|
+
fireAndSmoke: true,
|
|
23
|
+
evStations: false,
|
|
24
|
+
evLevels: { lvl2: true, lvl3: true },
|
|
25
|
+
wind: false
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
type Toggle = { enabled: boolean };
|
|
29
|
+
type Option = { value: boolean };
|
|
30
|
+
|
|
31
|
+
export interface MapOptionTargets extends PollutantTargets {
|
|
32
|
+
integration: PollutantTargets["integration"] & {
|
|
33
|
+
clustered: boolean;
|
|
34
|
+
displayOptions: Record<string, Option>;
|
|
35
|
+
};
|
|
36
|
+
collocationSites: Toggle;
|
|
37
|
+
hmsFire: Toggle;
|
|
38
|
+
hmsSmoke: Toggle;
|
|
39
|
+
evStations: Toggle & { clustered: boolean; displayOptions: { lvl2: Option; lvl3: Option } };
|
|
40
|
+
wind: Toggle;
|
|
41
|
+
mapStyle: { reset(): void };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function applyMapOptionDefaults(
|
|
45
|
+
targets: MapOptionTargets,
|
|
46
|
+
defaultPollutant: MonitorsPollutant | null
|
|
47
|
+
): void {
|
|
48
|
+
if (defaultPollutant) {
|
|
49
|
+
applyPollutantSelection(defaultPollutant, targets);
|
|
50
|
+
} else {
|
|
51
|
+
targets.integration.enabled = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [key, value] of Object.entries(MAP_OPTION_DEFAULTS.monitorTypes)) {
|
|
55
|
+
const option = targets.integration.displayOptions[key];
|
|
56
|
+
if (option) option.value = value;
|
|
57
|
+
}
|
|
58
|
+
targets.integration.clustered = MAP_OPTION_DEFAULTS.clustered;
|
|
59
|
+
|
|
60
|
+
targets.collocationSites.enabled = MAP_OPTION_DEFAULTS.collocationSites;
|
|
61
|
+
targets.hmsFire.enabled = MAP_OPTION_DEFAULTS.fireAndSmoke;
|
|
62
|
+
targets.hmsSmoke.enabled = MAP_OPTION_DEFAULTS.fireAndSmoke;
|
|
63
|
+
|
|
64
|
+
targets.evStations.enabled = MAP_OPTION_DEFAULTS.evStations;
|
|
65
|
+
targets.evStations.clustered = MAP_OPTION_DEFAULTS.clustered;
|
|
66
|
+
targets.evStations.displayOptions.lvl2.value = MAP_OPTION_DEFAULTS.evLevels.lvl2;
|
|
67
|
+
targets.evStations.displayOptions.lvl3.value = MAP_OPTION_DEFAULTS.evLevels.lvl3;
|
|
68
|
+
|
|
69
|
+
targets.wind.enabled = MAP_OPTION_DEFAULTS.wind;
|
|
70
|
+
targets.mapStyle.reset();
|
|
71
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { FlameIcon, LayersIcon, EvChargerIcon } from "@lucide/svelte";
|
|
3
|
+
import OptionsMenu from "../OptionsMenu.svelte";
|
|
4
|
+
import CheckboxRow from "../rows/CheckboxRow.svelte";
|
|
5
|
+
import SubmenuRow from "../rows/SubmenuRow.svelte";
|
|
6
|
+
import MarkerIcon from "../rows/MarkerIcon.svelte";
|
|
7
|
+
import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte";
|
|
8
|
+
import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte";
|
|
9
|
+
import { evStationsMapIntegration } from "$lib/ev-stations/ev-stations-map-integration.svelte";
|
|
10
|
+
|
|
11
|
+
const levels = $derived(evStationsMapIntegration.displayOptions);
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<OptionsMenu id="layers" label="Layers">
|
|
15
|
+
{#snippet icon()}
|
|
16
|
+
<LayersIcon size={20} color="#708090" />
|
|
17
|
+
{/snippet}
|
|
18
|
+
<CheckboxRow
|
|
19
|
+
label="Fire & Smoke"
|
|
20
|
+
bind:checked={
|
|
21
|
+
() => hmsFireMapIntegration.enabled && hmsSmokeMapIntegration.enabled,
|
|
22
|
+
(value) => {
|
|
23
|
+
hmsFireMapIntegration.enabled = value;
|
|
24
|
+
hmsSmokeMapIntegration.enabled = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
{#snippet icon()}
|
|
29
|
+
<FlameIcon size={18} color="#ff4500" />
|
|
30
|
+
{/snippet}
|
|
31
|
+
</CheckboxRow>
|
|
32
|
+
<SubmenuRow label="EV Chargers" bind:checked={evStationsMapIntegration.enabled}>
|
|
33
|
+
{#snippet icon()}
|
|
34
|
+
<EvChargerIcon size={18} color="#708090" />
|
|
35
|
+
{/snippet}
|
|
36
|
+
<CheckboxRow label="Level 2" bind:checked={evStationsMapIntegration.displayOptions.lvl2.value}>
|
|
37
|
+
{#snippet icon()}
|
|
38
|
+
<MarkerIcon icon={levels.lvl2.icon} class="w-5" />
|
|
39
|
+
{/snippet}
|
|
40
|
+
</CheckboxRow>
|
|
41
|
+
<CheckboxRow label="Level 3" bind:checked={evStationsMapIntegration.displayOptions.lvl3.value}>
|
|
42
|
+
{#snippet icon()}
|
|
43
|
+
<MarkerIcon icon={levels.lvl3.icon} class="w-5" />
|
|
44
|
+
{/snippet}
|
|
45
|
+
</CheckboxRow>
|
|
46
|
+
</SubmenuRow>
|
|
47
|
+
</OptionsMenu>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CrosshairIcon, RadioTowerIcon } from "@lucide/svelte";
|
|
3
|
+
import OptionsMenu from "../OptionsMenu.svelte";
|
|
4
|
+
import CheckboxRow from "../rows/CheckboxRow.svelte";
|
|
5
|
+
import GroupRow from "../rows/GroupRow.svelte";
|
|
6
|
+
import MarkerIcon from "../rows/MarkerIcon.svelte";
|
|
7
|
+
import { monitorsManager } from "$lib/monitors/monitors.svelte";
|
|
8
|
+
import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
|
|
9
|
+
import { collocationSitesMapIntegration } from "$lib/collocation-sites/collocations-map-integration.svelte";
|
|
10
|
+
import {
|
|
11
|
+
LOW_COST_TYPES,
|
|
12
|
+
REFERENCE_GRADE_TYPES,
|
|
13
|
+
typeSupportsPollutant,
|
|
14
|
+
type MonitorGroupType
|
|
15
|
+
} from "$lib/monitors/monitor-groups";
|
|
16
|
+
|
|
17
|
+
// Rows for types that don't report the current pollutant are hidden (meta-driven).
|
|
18
|
+
function visibleRows(types: ReadonlyArray<MonitorGroupType>) {
|
|
19
|
+
const displayOptions = monitorsMapIntegration.displayOptions;
|
|
20
|
+
return types
|
|
21
|
+
.filter((type) =>
|
|
22
|
+
typeSupportsPollutant(monitorsManager.meta, type, monitorsManager.pollutant)
|
|
23
|
+
)
|
|
24
|
+
.map((type) => ({
|
|
25
|
+
key: type,
|
|
26
|
+
label: displayOptions[type].label,
|
|
27
|
+
option: displayOptions[type]
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const referenceGrade = $derived(visibleRows(REFERENCE_GRADE_TYPES));
|
|
32
|
+
const lowCost = $derived(visibleRows(LOW_COST_TYPES));
|
|
33
|
+
const icons = monitorsMapIntegration.icons;
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<OptionsMenu id="monitors" label="Monitors" disabled={!monitorsMapIntegration.enabled}>
|
|
37
|
+
{#snippet icon()}
|
|
38
|
+
<RadioTowerIcon size={20} color="#4682b4" />
|
|
39
|
+
{/snippet}
|
|
40
|
+
<GroupRow label="Reference-grade" options={referenceGrade}>
|
|
41
|
+
{#snippet icon()}
|
|
42
|
+
<MarkerIcon icon={icons.get("outside-display-triangle")} />
|
|
43
|
+
{/snippet}
|
|
44
|
+
</GroupRow>
|
|
45
|
+
<GroupRow label="Low-cost sensors" options={lowCost}>
|
|
46
|
+
{#snippet icon()}
|
|
47
|
+
<MarkerIcon icon={icons.get("outside-display-circle")} />
|
|
48
|
+
{/snippet}
|
|
49
|
+
</GroupRow>
|
|
50
|
+
<hr class="my-1 border-gray-200" />
|
|
51
|
+
<CheckboxRow label="Inactive" bind:checked={monitorsMapIntegration.displayOptions.inactive.value}>
|
|
52
|
+
{#snippet icon()}
|
|
53
|
+
<MarkerIcon icon={monitorsMapIntegration.displayOptions.inactive.icon} />
|
|
54
|
+
{/snippet}
|
|
55
|
+
</CheckboxRow>
|
|
56
|
+
<CheckboxRow label="Inside" bind:checked={monitorsMapIntegration.displayOptions.inside.value}>
|
|
57
|
+
{#snippet icon()}
|
|
58
|
+
<MarkerIcon icon={monitorsMapIntegration.displayOptions.inside.icon} />
|
|
59
|
+
{/snippet}
|
|
60
|
+
</CheckboxRow>
|
|
61
|
+
{#if monitorsManager.pollutant === "pm25"}
|
|
62
|
+
<CheckboxRow label="Collocation Sites" bind:checked={collocationSitesMapIntegration.enabled}>
|
|
63
|
+
{#snippet icon()}
|
|
64
|
+
<CrosshairIcon size={16} color="#4A5FC6" />
|
|
65
|
+
{/snippet}
|
|
66
|
+
</CheckboxRow>
|
|
67
|
+
{/if}
|
|
68
|
+
</OptionsMenu>
|