@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,40 @@
|
|
|
1
|
+
import type { SJVAirEntryLevel } from "@sjvair/sdk";
|
|
2
|
+
import type { MonitorsPollutant } from "../monitors/types";
|
|
3
|
+
export interface LegendCategory {
|
|
4
|
+
color: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
export type LegendBarData = {
|
|
8
|
+
kind: "gradient";
|
|
9
|
+
colors: Array<string>;
|
|
10
|
+
ticks: Array<string>;
|
|
11
|
+
} | {
|
|
12
|
+
kind: "blocks";
|
|
13
|
+
categories: Array<LegendCategory>;
|
|
14
|
+
breakpoints?: Array<string>;
|
|
15
|
+
};
|
|
16
|
+
export interface LegendSection {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
unit?: string;
|
|
20
|
+
bar: LegendBarData;
|
|
21
|
+
}
|
|
22
|
+
export interface SmokeDensityStyle {
|
|
23
|
+
label: string;
|
|
24
|
+
color: string;
|
|
25
|
+
opacity: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pollutant legend built from the monitors meta levels — the same levels the map markers are
|
|
29
|
+
* colored by, so the two can never disagree. One tick per level, at that level's color stop.
|
|
30
|
+
*/
|
|
31
|
+
export declare function pollutantLegendSection(pollutant: MonitorsPollutant, levels: ReadonlyArray<Pick<SJVAirEntryLevel, "color" | "range">>): LegendSection | null;
|
|
32
|
+
/** Fire & Smoke share one toggle, so they render as two independent bars. */
|
|
33
|
+
export declare function fireSmokeLegendSections(fireTiers: ReadonlyArray<LegendCategory>, smoke: ReadonlyArray<SmokeDensityStyle>): Array<LegendSection>;
|
|
34
|
+
/** Approximates how a translucent fill layer looks over a light basemap. */
|
|
35
|
+
export declare function swatchOverWhite(color: string, opacity: number): string;
|
|
36
|
+
export declare function gradientBackground(colors: ReadonlyArray<string>): string;
|
|
37
|
+
/** Hard-edged equal-width blocks: each color repeated at both edges of its own share. */
|
|
38
|
+
export declare function blocksBackground(colors: ReadonlyArray<string>): string;
|
|
39
|
+
/** Percent position of the index-th of `count` evenly spaced labels. */
|
|
40
|
+
export declare function tickPosition(index: number, count: number): number;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { mix } from "color2k";
|
|
2
|
+
const POLLUTANT_TITLES = {
|
|
3
|
+
pm25: { title: "PM2.5", unit: "µg/m³" },
|
|
4
|
+
o3: { title: "Ozone", unit: "ppb" }
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Pollutant legend built from the monitors meta levels — the same levels the map markers are
|
|
8
|
+
* colored by, so the two can never disagree. One tick per level, at that level's color stop.
|
|
9
|
+
*/
|
|
10
|
+
export function pollutantLegendSection(pollutant, levels) {
|
|
11
|
+
if (levels.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
id: "pollutant",
|
|
15
|
+
...POLLUTANT_TITLES[pollutant],
|
|
16
|
+
bar: {
|
|
17
|
+
kind: "gradient",
|
|
18
|
+
colors: levels.map((level) => level.color),
|
|
19
|
+
ticks: levels.map((level) => String(Math.floor(level.range[0])))
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Fire & Smoke share one toggle, so they render as two independent bars. */
|
|
24
|
+
export function fireSmokeLegendSections(fireTiers, smoke) {
|
|
25
|
+
return [
|
|
26
|
+
{ id: "fire", title: "Fire", unit: "MW", bar: { kind: "blocks", categories: [...fireTiers] } },
|
|
27
|
+
{
|
|
28
|
+
id: "smoke",
|
|
29
|
+
title: "Smoke Density",
|
|
30
|
+
bar: {
|
|
31
|
+
kind: "blocks",
|
|
32
|
+
categories: smoke.map(({ label, color, opacity }) => ({
|
|
33
|
+
label,
|
|
34
|
+
color: swatchOverWhite(color, opacity)
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
/** Approximates how a translucent fill layer looks over a light basemap. */
|
|
41
|
+
export function swatchOverWhite(color, opacity) {
|
|
42
|
+
return mix("#ffffff", color, opacity);
|
|
43
|
+
}
|
|
44
|
+
export function gradientBackground(colors) {
|
|
45
|
+
if (colors.length === 0)
|
|
46
|
+
return "none";
|
|
47
|
+
if (colors.length === 1)
|
|
48
|
+
return colors[0];
|
|
49
|
+
return `linear-gradient(to right, ${colors.join(", ")})`;
|
|
50
|
+
}
|
|
51
|
+
/** Hard-edged equal-width blocks: each color repeated at both edges of its own share. */
|
|
52
|
+
export function blocksBackground(colors) {
|
|
53
|
+
const count = colors.length;
|
|
54
|
+
if (count === 0)
|
|
55
|
+
return "none";
|
|
56
|
+
const stops = colors.map((color, i) => `${color} ${percent(i / count)}, ${color} ${percent((i + 1) / count)}`);
|
|
57
|
+
return `linear-gradient(to right, ${stops.join(", ")})`;
|
|
58
|
+
}
|
|
59
|
+
/** Percent position of the index-th of `count` evenly spaced labels. */
|
|
60
|
+
export function tickPosition(index, count) {
|
|
61
|
+
return count <= 1 ? 0 : (index / (count - 1)) * 100;
|
|
62
|
+
}
|
|
63
|
+
function percent(fraction) {
|
|
64
|
+
return `${(fraction * 100).toFixed(2)}%`;
|
|
65
|
+
}
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
* trigger the escape-hatch full-page navigation below.
|
|
20
20
|
*/
|
|
21
21
|
knownRoutes?: Array<string>;
|
|
22
|
-
/**
|
|
22
|
+
/** Menus (OptionsGroup/OptionsMenu) rendered inside the options toolbar/panel */
|
|
23
23
|
menu?: Snippet;
|
|
24
|
+
/** Rendered last in the toolbar (wide) or beside the menu button (narrow) */
|
|
25
|
+
search?: Snippet;
|
|
24
26
|
/** Freeform absolutely-positioned content over the map (legend, search, etc.) */
|
|
25
27
|
overlays?: Snippet;
|
|
26
28
|
/** The routed detail panel content */
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
<script lang="ts">
|
|
51
53
|
import LoadScreen, { disable as disableLoadScreen } from "../LoadScreen.svelte";
|
|
52
54
|
import Map from "./Map.svelte";
|
|
53
|
-
import
|
|
55
|
+
import OptionsBar from "../options/OptionsBar.svelte";
|
|
54
56
|
import { mapManager } from "./map.svelte";
|
|
55
57
|
import { useMonitorMapRouter } from "../router-context";
|
|
56
58
|
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
panelOpen,
|
|
61
63
|
knownRoutes = [],
|
|
62
64
|
menu,
|
|
65
|
+
search,
|
|
63
66
|
overlays,
|
|
64
67
|
children,
|
|
65
68
|
basePath: providedBasePath,
|
|
@@ -124,10 +127,8 @@
|
|
|
124
127
|
<div class="relative flex-1 overflow-hidden">
|
|
125
128
|
<Map {integrations} />
|
|
126
129
|
{@render overlays?.()}
|
|
127
|
-
{#if menu}
|
|
128
|
-
<
|
|
129
|
-
<Menu>{@render menu()}</Menu>
|
|
130
|
-
</div>
|
|
130
|
+
{#if menu || search}
|
|
131
|
+
<OptionsBar {search}>{@render menu?.()}</OptionsBar>
|
|
131
132
|
{/if}
|
|
132
133
|
</div>
|
|
133
134
|
<div
|
|
@@ -17,8 +17,10 @@ export interface MapShellProps {
|
|
|
17
17
|
* trigger the escape-hatch full-page navigation below.
|
|
18
18
|
*/
|
|
19
19
|
knownRoutes?: Array<string>;
|
|
20
|
-
/**
|
|
20
|
+
/** Menus (OptionsGroup/OptionsMenu) rendered inside the options toolbar/panel */
|
|
21
21
|
menu?: Snippet;
|
|
22
|
+
/** Rendered last in the toolbar (wide) or beside the menu button (narrow) */
|
|
23
|
+
search?: Snippet;
|
|
22
24
|
/** Freeform absolutely-positioned content over the map (legend, search, etc.) */
|
|
23
25
|
overlays?: Snippet;
|
|
24
26
|
/** The routed detail panel content */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The slices of MapTiler's MapStyleVariant / ReferenceMapStyle this module needs. */
|
|
2
|
+
export interface StyleVariantLike {
|
|
3
|
+
getType(): string;
|
|
4
|
+
}
|
|
5
|
+
export interface ReferenceStyleLike<V extends StyleVariantLike> {
|
|
6
|
+
hasVariant(type: string): boolean;
|
|
7
|
+
getVariant(type: string): V;
|
|
8
|
+
getDefaultVariant(): V;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Variant to show after switching basemaps: the same variant type (e.g. "DARK") when the new
|
|
12
|
+
* basemap offers it, otherwise the new basemap's default.
|
|
13
|
+
*/
|
|
14
|
+
export declare function variantForStyle<V extends StyleVariantLike>(style: ReferenceStyleLike<V>, previous: StyleVariantLike | null): V;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variant to show after switching basemaps: the same variant type (e.g. "DARK") when the new
|
|
3
|
+
* basemap offers it, otherwise the new basemap's default.
|
|
4
|
+
*/
|
|
5
|
+
export function variantForStyle(style, previous) {
|
|
6
|
+
if (previous && style.hasVariant(previous.getType())) {
|
|
7
|
+
return style.getVariant(previous.getType());
|
|
8
|
+
}
|
|
9
|
+
return style.getDefaultVariant();
|
|
10
|
+
}
|
|
@@ -6,6 +6,12 @@ export declare abstract class MapGeoJSONIntegration<T = Record<string, unknown>>
|
|
|
6
6
|
abstract features: Array<Feature<Geometry, T>>;
|
|
7
7
|
abstract mapSource: Parameters<MaptilerMap["addSource"]>[1];
|
|
8
8
|
remove(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Removes only this integration's own layer and source. Non-virtual on purpose: `apply()`
|
|
11
|
+
* paths call it to clear stale map state before re-adding, without triggering a subclass's
|
|
12
|
+
* `remove()` override (which also tears down tooltips/click handlers that apply just set up).
|
|
13
|
+
*/
|
|
14
|
+
protected removeLayerAndSource(): void;
|
|
9
15
|
}
|
|
10
16
|
export declare abstract class MapIconLayerIntegration<T = Record<string, unknown>> extends MapGeoJSONIntegration<T> {
|
|
11
17
|
abstract icons: MapIconManager;
|
|
@@ -2,7 +2,17 @@ import { MapLayerIntegration } from "./map-layer-integration.svelte";
|
|
|
2
2
|
import { mapManager } from "../map.svelte";
|
|
3
3
|
export class MapGeoJSONIntegration extends MapLayerIntegration {
|
|
4
4
|
remove() {
|
|
5
|
-
|
|
5
|
+
this.removeLayerAndSource();
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Removes only this integration's own layer and source. Non-virtual on purpose: `apply()`
|
|
9
|
+
* paths call it to clear stale map state before re-adding, without triggering a subclass's
|
|
10
|
+
* `remove()` override (which also tears down tooltips/click handlers that apply just set up).
|
|
11
|
+
*/
|
|
12
|
+
removeLayerAndSource() {
|
|
13
|
+
if (mapManager.map?.getLayer(this.referenceId)) {
|
|
14
|
+
mapManager.map.removeLayer(this.referenceId);
|
|
15
|
+
}
|
|
6
16
|
if (mapManager.map?.getSource(this.referenceId)) {
|
|
7
17
|
mapManager.map.removeSource(this.referenceId);
|
|
8
18
|
}
|
|
@@ -13,7 +23,10 @@ export class MapIconLayerIntegration extends MapGeoJSONIntegration {
|
|
|
13
23
|
if (!mapManager.map)
|
|
14
24
|
return;
|
|
15
25
|
this.icons.loadIcons().then(() => {
|
|
16
|
-
|
|
26
|
+
// A basemap change started while icons loaded; the refresh on "style.load" re-applies.
|
|
27
|
+
if (mapManager.styleLoading)
|
|
28
|
+
return;
|
|
29
|
+
this.removeLayerAndSource();
|
|
17
30
|
mapManager.map?.addSource(this.referenceId, this.mapSource);
|
|
18
31
|
super.apply();
|
|
19
32
|
});
|
|
@@ -8,13 +8,17 @@ export class MapLayerIntegration extends MapIntegration {
|
|
|
8
8
|
// Each integration manages its own lifecycle. Tracking both mapManager.map and
|
|
9
9
|
// this.enabled means apply/remove fire automatically on map load and on enabled
|
|
10
10
|
// changes. untrack on the call itself prevents reactive reads inside apply/remove
|
|
11
|
-
// from leaking back into this effect's dependency graph.
|
|
11
|
+
// from leaking back into this effect's dependency graph. While a new basemap style is
|
|
12
|
+
// loading nothing is applied or removed: the refresh on "style.load" does both.
|
|
12
13
|
$effect.root(() => {
|
|
13
14
|
$effect(() => {
|
|
14
|
-
|
|
15
|
+
const enabled = this.enabled;
|
|
16
|
+
if (!mapManager.map || mapManager.styleLoading)
|
|
17
|
+
return;
|
|
18
|
+
if (enabled) {
|
|
15
19
|
untrack(() => this.apply());
|
|
16
20
|
}
|
|
17
|
-
else
|
|
21
|
+
else {
|
|
18
22
|
untrack(() => this.remove());
|
|
19
23
|
}
|
|
20
24
|
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MapStyleVariant, ReferenceMapStyle } from "@maptiler/sdk";
|
|
2
|
+
/** Basemap + theme selection, shared so the Settings menu and Reset Map Options agree. */
|
|
3
|
+
declare class MapStyleState {
|
|
4
|
+
referenceStyle: ReferenceMapStyle;
|
|
5
|
+
variant: MapStyleVariant;
|
|
6
|
+
private applied;
|
|
7
|
+
selectReferenceStyle(style: ReferenceMapStyle): void;
|
|
8
|
+
selectVariant(variant: MapStyleVariant): void;
|
|
9
|
+
reset(): void;
|
|
10
|
+
private apply;
|
|
11
|
+
}
|
|
12
|
+
export declare const mapStyleState: MapStyleState;
|
|
13
|
+
export type { MapStyleState };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DefaultMapStyle, mapManager } from "./map.svelte";
|
|
2
|
+
import { integrationsManager } from "./integrations/integrations-manager";
|
|
3
|
+
import { variantForStyle } from "./basemap";
|
|
4
|
+
/** Basemap + theme selection, shared so the Settings menu and Reset Map Options agree. */
|
|
5
|
+
class MapStyleState {
|
|
6
|
+
referenceStyle = $state.raw(DefaultMapStyle.getReferenceStyle());
|
|
7
|
+
variant = $state.raw(DefaultMapStyle);
|
|
8
|
+
applied = DefaultMapStyle;
|
|
9
|
+
selectReferenceStyle(style) {
|
|
10
|
+
this.referenceStyle = style;
|
|
11
|
+
this.selectVariant(variantForStyle(style, this.variant));
|
|
12
|
+
}
|
|
13
|
+
selectVariant(variant) {
|
|
14
|
+
this.variant = variant;
|
|
15
|
+
this.apply();
|
|
16
|
+
}
|
|
17
|
+
reset() {
|
|
18
|
+
this.referenceStyle = DefaultMapStyle.getReferenceStyle();
|
|
19
|
+
this.selectVariant(DefaultMapStyle);
|
|
20
|
+
}
|
|
21
|
+
apply() {
|
|
22
|
+
const map = mapManager.map;
|
|
23
|
+
if (!map || this.variant === this.applied)
|
|
24
|
+
return;
|
|
25
|
+
this.applied = this.variant;
|
|
26
|
+
// setStyle wipes every custom source/layer; re-add enabled integrations once it loads.
|
|
27
|
+
map.once("style.load", () => {
|
|
28
|
+
mapManager.styleLoading = false;
|
|
29
|
+
integrationsManager.refresh();
|
|
30
|
+
});
|
|
31
|
+
mapManager.styleLoading = true;
|
|
32
|
+
map.setStyle(this.variant);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export const mapStyleState = new MapStyleState();
|
|
@@ -4,6 +4,12 @@ import type { Feature, GeoJsonProperties, Geometry } from "geojson";
|
|
|
4
4
|
export declare const DefaultMapStyle: MapStyleVariant;
|
|
5
5
|
declare class MapManager {
|
|
6
6
|
map: MaptilerMap | null;
|
|
7
|
+
/**
|
|
8
|
+
* True between `setStyle()` and the new style's "style.load". Plain (non-reactive) on purpose:
|
|
9
|
+
* integrations skip touching the style while it's set, and the `integrationsManager.refresh()`
|
|
10
|
+
* that runs on "style.load" applies/removes every integration afterwards.
|
|
11
|
+
*/
|
|
12
|
+
styleLoading: boolean;
|
|
7
13
|
setDataSource(sourceId: string, features: Array<Feature<Geometry, GeoJsonProperties>>): void;
|
|
8
14
|
refreshMap(): void;
|
|
9
15
|
removeExcludedLayers(): void;
|
|
@@ -5,6 +5,12 @@ export const DefaultMapStyle = MapStyle.STREETS_V4.DEFAULT;
|
|
|
5
5
|
const excludeLayers = ["Sport", "Tourism", "Culture", "Shopping", "Food", "Transport"];
|
|
6
6
|
class MapManager {
|
|
7
7
|
map = $state(null);
|
|
8
|
+
/**
|
|
9
|
+
* True between `setStyle()` and the new style's "style.load". Plain (non-reactive) on purpose:
|
|
10
|
+
* integrations skip touching the style while it's set, and the `integrationsManager.refresh()`
|
|
11
|
+
* that runs on "style.load" applies/removes every integration afterwards.
|
|
12
|
+
*/
|
|
13
|
+
styleLoading = false;
|
|
8
14
|
setDataSource(sourceId, features) {
|
|
9
15
|
const source = mapManager.map?.getSource(sourceId);
|
|
10
16
|
if (isGeoJSONSource(source)) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Monitor display-option keys shown under each Monitors menu group, in display order. */
|
|
2
|
+
export declare const REFERENCE_GRADE_TYPES: readonly ["airnow", "aqlite", "aqview", "bam1022"];
|
|
3
|
+
export declare const LOW_COST_TYPES: readonly ["airgradient", "purpleair", "vozbox"];
|
|
4
|
+
export type MonitorGroupType = (typeof REFERENCE_GRADE_TYPES)[number] | (typeof LOW_COST_TYPES)[number];
|
|
5
|
+
/** The slice of MonitorsMeta needed to know which pollutants a monitor type reports. */
|
|
6
|
+
export interface MonitorTypeMeta {
|
|
7
|
+
monitors: Record<string, {
|
|
8
|
+
entries: Record<string, unknown>;
|
|
9
|
+
} | undefined>;
|
|
10
|
+
}
|
|
11
|
+
export declare function typeSupportsPollutant(meta: MonitorTypeMeta | null, type: string, pollutant: string | null): boolean;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Monitor display-option keys shown under each Monitors menu group, in display order. */
|
|
2
|
+
export const REFERENCE_GRADE_TYPES = ["airnow", "aqlite", "aqview", "bam1022"];
|
|
3
|
+
export const LOW_COST_TYPES = ["airgradient", "purpleair", "vozbox"];
|
|
4
|
+
export function typeSupportsPollutant(meta, type, pollutant) {
|
|
5
|
+
const deviceMeta = meta?.monitors[type];
|
|
6
|
+
return !!deviceMeta && !!pollutant && pollutant in deviceMeta.entries;
|
|
7
|
+
}
|
|
@@ -26,15 +26,13 @@ export function getOrder(monitor) {
|
|
|
26
26
|
}
|
|
27
27
|
export function getTypeShape(type) {
|
|
28
28
|
switch (type) {
|
|
29
|
-
case "airgradient":
|
|
30
|
-
return "circle";
|
|
31
29
|
case "airnow":
|
|
32
30
|
case "aqview":
|
|
33
31
|
case "bam1022":
|
|
34
32
|
case "aqlite":
|
|
35
33
|
return "triangle";
|
|
34
|
+
case "airgradient":
|
|
36
35
|
case "purpleair":
|
|
37
|
-
return "square";
|
|
38
36
|
case "vozbox":
|
|
39
37
|
return "circle";
|
|
40
38
|
default:
|
|
@@ -8,18 +8,15 @@ const MONITOR_ICON_DISPLAY_COLOR = "#48c78e"; // light gray
|
|
|
8
8
|
export function getIconId(monitor, level) {
|
|
9
9
|
const id = `${monitor.location}-${monitor.is_active ? level.name : "default"}`;
|
|
10
10
|
switch (monitor.type) {
|
|
11
|
-
case "airgradient":
|
|
12
|
-
return `${id}-circle`;
|
|
13
11
|
case "airnow":
|
|
14
12
|
case "aqview":
|
|
15
13
|
case "bam1022":
|
|
14
|
+
case "aqlite":
|
|
16
15
|
return `${id}-triangle`;
|
|
16
|
+
case "airgradient":
|
|
17
17
|
case "purpleair":
|
|
18
|
-
return `${id}-${monitor.is_sjvair ? "circle" : "square"}`;
|
|
19
18
|
case "vozbox":
|
|
20
19
|
return `${id}-circle`;
|
|
21
|
-
case "aqlite":
|
|
22
|
-
return `${id}-triangle`;
|
|
23
20
|
default:
|
|
24
21
|
throw new Error(`Map icon for ${monitor.device} has not been set`);
|
|
25
22
|
}
|
|
@@ -14,13 +14,13 @@ declare class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMark
|
|
|
14
14
|
tooltipManager: TooltipManager;
|
|
15
15
|
private renderer;
|
|
16
16
|
displayOptions: {
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
airnow: MapDisplayOption;
|
|
18
|
+
aqlite: MapDisplayOption;
|
|
19
19
|
aqview: MapDisplayOption;
|
|
20
20
|
bam1022: MapDisplayOption;
|
|
21
|
-
|
|
21
|
+
airgradient: MapDisplayOption;
|
|
22
|
+
purpleair: MapDisplayOption;
|
|
22
23
|
vozbox: MapDisplayOption;
|
|
23
|
-
aqlite: MapDisplayOption;
|
|
24
24
|
inactive: MapDisplayOption;
|
|
25
25
|
inside: MapDisplayOption;
|
|
26
26
|
};
|
|
@@ -11,12 +11,6 @@ import { MonitorsClusterRenderer, monitorTooltip } from "./monitors-cluster-rend
|
|
|
11
11
|
const filters = {
|
|
12
12
|
monitor(deviceType) {
|
|
13
13
|
return ["==", ["get", "type"], deviceType];
|
|
14
|
-
},
|
|
15
|
-
purpleair() {
|
|
16
|
-
return ["all", ["==", ["get", "type"], "purpleair"], ["==", ["get", "is_sjvair"], false]];
|
|
17
|
-
},
|
|
18
|
-
sjvPurpleair() {
|
|
19
|
-
return ["all", ["==", ["get", "type"], "purpleair"], ["==", ["get", "is_sjvair"], true]];
|
|
20
14
|
}
|
|
21
15
|
};
|
|
22
16
|
class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
@@ -28,13 +22,13 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
28
22
|
tooltipManager = new TooltipManager();
|
|
29
23
|
renderer = new MonitorsClusterRenderer(this);
|
|
30
24
|
displayOptions = $derived.by(() => ({
|
|
31
|
-
purpleair: new MapDisplayOption("PurpleAir", true, this.icons.get("outside-display-square")),
|
|
32
|
-
sjvair: new MapDisplayOption("SJVAir non-FEM", true, this.icons.get("outside-display-circle")),
|
|
33
|
-
aqview: new MapDisplayOption("AQview", true, this.icons.get("outside-display-triangle")),
|
|
34
|
-
bam1022: new MapDisplayOption("SJVAir FEM", true, this.icons.get("outside-display-triangle")),
|
|
35
25
|
airnow: new MapDisplayOption("AirNow", true, this.icons.get("outside-display-triangle")),
|
|
36
|
-
vozbox: new MapDisplayOption("VOZbox", true, this.icons.get("outside-display-circle")),
|
|
37
26
|
aqlite: new MapDisplayOption("AQLite", true, this.icons.get("outside-display-triangle")),
|
|
27
|
+
aqview: new MapDisplayOption("AQview", true, this.icons.get("outside-display-triangle")),
|
|
28
|
+
bam1022: new MapDisplayOption("SJVAir", true, this.icons.get("outside-display-triangle")),
|
|
29
|
+
airgradient: new MapDisplayOption("AirGradient", true, this.icons.get("outside-display-circle")),
|
|
30
|
+
purpleair: new MapDisplayOption("PurpleAir", true, this.icons.get("outside-display-circle")),
|
|
31
|
+
vozbox: new MapDisplayOption("VOZbox", true, this.icons.get("outside-display-circle")),
|
|
38
32
|
inactive: new MapDisplayOption("Inactive", false, this.icons.get("outside-default-square")),
|
|
39
33
|
inside: new MapDisplayOption("Inside", false, this.icons.get("inside-display-square"))
|
|
40
34
|
}));
|
|
@@ -95,20 +89,20 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
95
89
|
["==", ["get", "location"], "outside"]
|
|
96
90
|
];
|
|
97
91
|
const statusFilters = ["any", ["==", ["get", "is_active"], true]];
|
|
98
|
-
if (this.displayOptions.
|
|
99
|
-
monitorFilters.push(filters.
|
|
92
|
+
if (this.displayOptions.airnow.value)
|
|
93
|
+
monitorFilters.push(filters.monitor("airnow"));
|
|
94
|
+
if (this.displayOptions.aqlite.value)
|
|
95
|
+
monitorFilters.push(filters.monitor("aqlite"));
|
|
100
96
|
if (this.displayOptions.aqview.value)
|
|
101
97
|
monitorFilters.push(filters.monitor("aqview"));
|
|
102
98
|
if (this.displayOptions.bam1022.value)
|
|
103
99
|
monitorFilters.push(filters.monitor("bam1022"));
|
|
104
|
-
if (this.displayOptions.
|
|
105
|
-
monitorFilters.push(filters.monitor("
|
|
100
|
+
if (this.displayOptions.airgradient.value)
|
|
101
|
+
monitorFilters.push(filters.monitor("airgradient"));
|
|
102
|
+
if (this.displayOptions.purpleair.value)
|
|
103
|
+
monitorFilters.push(filters.monitor("purpleair"));
|
|
106
104
|
if (this.displayOptions.vozbox.value)
|
|
107
105
|
monitorFilters.push(filters.monitor("vozbox"));
|
|
108
|
-
if (this.displayOptions.aqlite.value)
|
|
109
|
-
monitorFilters.push(filters.monitor("aqlite"));
|
|
110
|
-
if (this.displayOptions.sjvair.value)
|
|
111
|
-
monitorFilters.push(filters.sjvPurpleair(), filters.monitor("airgradient"));
|
|
112
106
|
if (this.displayOptions.inside.value)
|
|
113
107
|
locationFilters.push(["==", ["get", "location"], "inside"]);
|
|
114
108
|
if (this.displayOptions.inactive.value)
|
|
@@ -116,8 +110,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
116
110
|
return ["all", monitorFilters, locationFilters, statusFilters];
|
|
117
111
|
});
|
|
118
112
|
// Groups features by monitor type for per-type cluster sources, applying display option
|
|
119
|
-
// filters so cluster aggregates only include visible monitors.
|
|
120
|
-
// to "airgradient" since they share the same shape (circle).
|
|
113
|
+
// filters so cluster aggregates only include visible monitors.
|
|
121
114
|
featuresByType = $derived.by(() => {
|
|
122
115
|
const opts = this.displayOptions;
|
|
123
116
|
const byType = {};
|
|
@@ -128,20 +121,19 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
128
121
|
if (p.location === "inside" && !opts.inside.value)
|
|
129
122
|
continue;
|
|
130
123
|
const typeVisible = {
|
|
131
|
-
|
|
132
|
-
|
|
124
|
+
airnow: opts.airnow.value,
|
|
125
|
+
aqlite: opts.aqlite.value,
|
|
133
126
|
aqview: opts.aqview.value,
|
|
134
127
|
bam1022: opts.bam1022.value,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
airgradient: opts.airgradient.value,
|
|
129
|
+
purpleair: opts.purpleair.value,
|
|
130
|
+
vozbox: opts.vozbox.value
|
|
138
131
|
};
|
|
139
132
|
if (!(typeVisible[p.type] ?? true))
|
|
140
133
|
continue;
|
|
141
|
-
const
|
|
142
|
-
const arr = byType[key] ?? [];
|
|
134
|
+
const arr = byType[p.type] ?? [];
|
|
143
135
|
arr.push(feat);
|
|
144
|
-
byType[
|
|
136
|
+
byType[p.type] = arr;
|
|
145
137
|
}
|
|
146
138
|
return byType;
|
|
147
139
|
});
|
|
@@ -181,12 +173,20 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
181
173
|
this.dataSource = dataSource;
|
|
182
174
|
this.icons = new MonitorsIconManager(dataSource);
|
|
183
175
|
$effect.root(() => {
|
|
176
|
+
// Every effect below early-returns while disabled (Pollutant = None) so a data refresh
|
|
177
|
+
// can't re-add the layer. `enabled` is read untracked: the base class's own effect
|
|
178
|
+
// already applies/removes on enabled changes, and tracking it here would apply twice.
|
|
179
|
+
// The guard comes after each effect's tracked reads so its dependencies are always
|
|
180
|
+
// collected; returning before reading anything would leave the effect permanently dead.
|
|
184
181
|
// Push filter changes to the active layer(s) imperatively
|
|
185
182
|
$effect(() => {
|
|
186
183
|
const filter = this.filters;
|
|
184
|
+
const clustered = this.clustered;
|
|
187
185
|
if (!mapManager.map)
|
|
188
186
|
return;
|
|
189
|
-
if (this.
|
|
187
|
+
if (!untrack(() => this.enabled))
|
|
188
|
+
return;
|
|
189
|
+
if (clustered) {
|
|
190
190
|
this.renderer.syncFilter();
|
|
191
191
|
}
|
|
192
192
|
else {
|
|
@@ -200,6 +200,8 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
200
200
|
const features = this.features;
|
|
201
201
|
if (!mapManager.map || this.clustered)
|
|
202
202
|
return;
|
|
203
|
+
if (!untrack(() => this.enabled))
|
|
204
|
+
return;
|
|
203
205
|
mapManager.setDataSource(this.referenceId, features);
|
|
204
206
|
});
|
|
205
207
|
// Keep cluster source data in sync when features or display options change
|
|
@@ -207,6 +209,8 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
207
209
|
void this.featuresByType;
|
|
208
210
|
if (!mapManager.map || !this.clustered)
|
|
209
211
|
return;
|
|
212
|
+
if (!untrack(() => this.enabled))
|
|
213
|
+
return;
|
|
210
214
|
this.renderer.syncFeatures();
|
|
211
215
|
});
|
|
212
216
|
// Push updated icon expressions to cluster layers when the pollutant changes
|
|
@@ -214,6 +218,8 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
214
218
|
const thresholds = this.clusterIconThresholds;
|
|
215
219
|
if (!mapManager.map || !this.clustered || !thresholds.length)
|
|
216
220
|
return;
|
|
221
|
+
if (!untrack(() => this.enabled))
|
|
222
|
+
return;
|
|
217
223
|
this.renderer.syncThresholds();
|
|
218
224
|
});
|
|
219
225
|
// Re-apply when clustered mode switches or when data first arrives (cold load).
|
|
@@ -224,6 +230,8 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
224
230
|
const hasFeatures = Object.keys(this.featuresByType).length > 0;
|
|
225
231
|
if (!untrack(() => mapManager.map) || !hasFeatures)
|
|
226
232
|
return;
|
|
233
|
+
if (!untrack(() => this.enabled))
|
|
234
|
+
return;
|
|
227
235
|
untrack(() => this.apply());
|
|
228
236
|
});
|
|
229
237
|
// Sync selected icon scale via feature state
|
|
@@ -250,6 +258,8 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
250
258
|
this.tooltipManager.disable();
|
|
251
259
|
this.renderer.remove();
|
|
252
260
|
this.icons.loadIcons().then(() => {
|
|
261
|
+
if (mapManager.styleLoading)
|
|
262
|
+
return;
|
|
253
263
|
this.renderer.apply(this.handleMonitorClick);
|
|
254
264
|
this.tooltipManager.enable();
|
|
255
265
|
this.applySelectedState();
|
|
@@ -262,12 +272,15 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
262
272
|
clickManager.unregister([this.referenceId]);
|
|
263
273
|
super.apply();
|
|
264
274
|
this.icons.loadIcons().then(() => {
|
|
275
|
+
if (mapManager.styleLoading)
|
|
276
|
+
return;
|
|
265
277
|
clickManager.register([this.referenceId], this.handleMonitorClick);
|
|
266
278
|
this.applySelectedState();
|
|
267
279
|
});
|
|
268
280
|
}
|
|
269
281
|
}
|
|
270
282
|
remove() {
|
|
283
|
+
this.tooltipManager.disable();
|
|
271
284
|
clickManager.unregister([this.referenceId]);
|
|
272
285
|
this.renderer.remove();
|
|
273
286
|
super.remove();
|
|
@@ -10,6 +10,7 @@ declare class MonitorsManager implements MonitorsDataSource {
|
|
|
10
10
|
meta: MonitorsMeta | null;
|
|
11
11
|
pollutant: "pm25" | "o3" | null;
|
|
12
12
|
levels: Array<SJVAirEntryLevel> | null;
|
|
13
|
+
constructor();
|
|
13
14
|
init(urlPollutant?: string | number | boolean): Promise<void>;
|
|
14
15
|
update(): Promise<void>;
|
|
15
16
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getMonitors, getMonitorsList, getMonitorsMeta } from "@sjvair/sdk";
|
|
2
2
|
import { XMap } from "@tstk/builtin-extensions";
|
|
3
3
|
import { Interval } from "@tstk/utils";
|
|
4
|
+
import { untrack } from "svelte";
|
|
4
5
|
class MonitorsManager {
|
|
5
6
|
autoUpdate = new Interval(async () => await this.update(), 2 * 60 * 1000);
|
|
6
7
|
initialized = $state(false);
|
|
@@ -9,6 +10,27 @@ class MonitorsManager {
|
|
|
9
10
|
meta = $state(null);
|
|
10
11
|
pollutant = $state(null);
|
|
11
12
|
levels = $derived(this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null);
|
|
13
|
+
constructor() {
|
|
14
|
+
// Refetch monitors when the pollutant changes after the initial load, wherever the change
|
|
15
|
+
// comes from (Pollutant menu, URL sync, a host's own UI). Clearing first blanks the markers
|
|
16
|
+
// instead of briefly coloring them with the previous pollutant's readings.
|
|
17
|
+
let fetchedPollutant = null;
|
|
18
|
+
$effect.root(() => {
|
|
19
|
+
$effect(() => {
|
|
20
|
+
const pollutant = this.pollutant;
|
|
21
|
+
if (!this.initialized)
|
|
22
|
+
return;
|
|
23
|
+
if (fetchedPollutant !== null && fetchedPollutant !== pollutant) {
|
|
24
|
+
untrack(() => {
|
|
25
|
+
this.list = [];
|
|
26
|
+
this.latest = null;
|
|
27
|
+
this.update();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
fetchedPollutant = pollutant;
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
12
34
|
async init(urlPollutant) {
|
|
13
35
|
if (this.initialized)
|
|
14
36
|
return;
|