@sjvair/monitor-map 4.0.2 → 4.1.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 +42 -0
- package/dist/lib/MonitorMapLayout.svelte +2 -0
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +1 -1
- package/dist/lib/core.d.ts +12 -0
- package/dist/lib/core.js +10 -0
- package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +1 -1
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +1 -1
- package/dist/lib/hms/hms-fire-map-integration.svelte.js +1 -1
- package/dist/lib/hms/hms-smoke-map-integration.svelte.js +1 -1
- package/dist/lib/index.d.ts +7 -2
- package/dist/lib/index.js +6 -1
- package/dist/lib/lazy-routes.d.ts +10 -0
- package/dist/lib/lazy-routes.js +13 -0
- package/dist/lib/map/Map.svelte +1 -1
- package/dist/lib/map/MapShell.svelte +2 -2
- package/dist/lib/map/integrations/click-manager.js +1 -1
- package/dist/lib/map/integrations/map-geojson-integration.svelte.js +1 -1
- package/dist/lib/map/integrations/map-icon-manager.js +1 -1
- package/dist/lib/map/integrations/map-layer-integration.svelte.js +1 -1
- package/dist/lib/map/integrations/tooltip.svelte.js +1 -1
- package/dist/lib/map/map-style-state.svelte.js +1 -1
- package/dist/lib/monitors/components/MonitorClusterTooltip.svelte +1 -1
- package/dist/lib/monitors/monitors-cluster-renderer.js +1 -1
- package/dist/lib/monitors/monitors-icon-manager.svelte.js +1 -1
- package/dist/lib/monitors/monitors-map-integration.svelte.js +1 -1
- package/dist/lib/monitors/monitors.svelte.d.ts +11 -0
- package/dist/lib/monitors/monitors.svelte.js +67 -8
- package/dist/lib/search/Search.svelte +1 -1
- package/package.json +15 -4
- package/src/lib/MonitorMapLayout.svelte +2 -0
- package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +1 -1
- package/src/lib/core.ts +17 -0
- package/src/lib/ev-stations/ev-stations-cluster-renderer.ts +1 -1
- package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +1 -1
- package/src/lib/hms/hms-fire-map-integration.svelte.ts +1 -1
- package/src/lib/hms/hms-smoke-map-integration.svelte.ts +1 -1
- package/src/lib/index.ts +7 -2
- package/src/lib/lazy-routes.ts +15 -0
- package/src/lib/map/Map.svelte +1 -1
- package/src/lib/map/MapShell.svelte +2 -2
- package/src/lib/map/integrations/click-manager.ts +1 -1
- package/src/lib/map/integrations/map-geojson-integration.svelte.ts +1 -1
- package/src/lib/map/integrations/map-icon-manager.ts +1 -1
- package/src/lib/map/integrations/map-layer-integration.svelte.ts +1 -1
- package/src/lib/map/integrations/tooltip.svelte.ts +1 -1
- package/src/lib/map/map-style-state.svelte.ts +1 -1
- package/src/lib/monitors/components/MonitorClusterTooltip.svelte +1 -1
- package/src/lib/monitors/monitors-cluster-renderer.ts +1 -1
- package/src/lib/monitors/monitors-icon-manager.svelte.ts +1 -1
- package/src/lib/monitors/monitors-map-integration.svelte.ts +1 -1
- package/src/lib/monitors/monitors.svelte.ts +77 -9
- package/src/lib/search/Search.svelte +1 -1
- /package/dist/lib/map/{map.svelte.d.ts → map-manager.svelte.d.ts} +0 -0
- /package/dist/lib/map/{map.svelte.js → map-manager.svelte.js} +0 -0
- /package/src/lib/map/{map.svelte.ts → map-manager.svelte.ts} +0 -0
package/README.md
CHANGED
|
@@ -50,6 +50,48 @@ export const { route, navigate, p, isActive, Router } = createRouter({
|
|
|
50
50
|
This is what the sjvair.com widget and `sjvair-mobile` use today, via `MonitorMapLayout`
|
|
51
51
|
(the thin wrapper `monitorMapRoutes` points to).
|
|
52
52
|
|
|
53
|
+
### Keeping the map off a host app's startup path
|
|
54
|
+
|
|
55
|
+
The package root re-exports everything, and its `monitorMapRoutes` imports the map layout
|
|
56
|
+
eagerly, so referencing it in a router puts maplibre (about 2 MB) in the startup bundle. A
|
|
57
|
+
host whose first screen is not the map uses the two subpath entries instead:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// router.ts
|
|
61
|
+
import { createRouter } from "sv-router";
|
|
62
|
+
import { monitorMapRoutes } from "@sjvair/monitor-map/routes";
|
|
63
|
+
|
|
64
|
+
export const { route, navigate, p, isActive, Router } = createRouter({
|
|
65
|
+
"/my-map": monitorMapRoutes
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```svelte
|
|
70
|
+
<!-- app root -->
|
|
71
|
+
<script>
|
|
72
|
+
import { provideMonitorMapRouter } from "@sjvair/monitor-map/core";
|
|
73
|
+
import { route, navigate, p, isActive, Router } from "./router";
|
|
74
|
+
|
|
75
|
+
provideMonitorMapRouter({ route, navigate, p, isActive, basePath: "/my-map" });
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<Router />
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- `@sjvair/monitor-map/core` exports the router context (`provideMonitorMapRouter`,
|
|
82
|
+
`useMonitorMapRouter`, `ROUTER_KEY`, `MonitorMapRouterApi`) and `monitorsManager`, with
|
|
83
|
+
no map code behind them.
|
|
84
|
+
- `@sjvair/monitor-map/routes` exports a `monitorMapRoutes` with the same route tree, whose
|
|
85
|
+
layout and routes load on demand. On a cold deep link into the map, the router outlet is
|
|
86
|
+
empty until that chunk arrives.
|
|
87
|
+
|
|
88
|
+
Both entries share their modules with the package root, so `monitorsManager` is the same
|
|
89
|
+
instance whichever entry it is imported from.
|
|
90
|
+
|
|
91
|
+
`monitorsManager.init()` may be called from several places (for example, a background
|
|
92
|
+
preload and the map layout): overlapping calls share one set of requests, a failed call can
|
|
93
|
+
be retried, and an explicit `"pm25"`/`"o3"` argument wins over a call without one.
|
|
94
|
+
|
|
53
95
|
### Embedding `MapShell` directly, with a reduced integration set
|
|
54
96
|
|
|
55
97
|
`MonitorMapLayout` is an opinionated wrapper around the lower-level `MapShell`
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
monitorsManager.init(route.search.pollutant);
|
|
65
|
+
// A previous instance of this layout stopped polling in onDestroy; init() is a no-op by now.
|
|
66
|
+
monitorsManager.resumeAutoUpdate();
|
|
65
67
|
collocationSitesManager.init();
|
|
66
68
|
hmsManager.init();
|
|
67
69
|
monitorsMapIntegration.onMonitorClick = (id: string) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapManager } from "../map/map.svelte";
|
|
1
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
2
2
|
import { MapIconLayerIntegration } from "../map/integrations/map-geojson-integration.svelte";
|
|
3
3
|
import { CollocationIconManager, getCollocationIconId } from "./collocations-icon-manager.svelte";
|
|
4
4
|
import { TooltipManager } from "../map/integrations/tooltip.svelte";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light entry (`@sjvair/monitor-map/core`): what a host app needs at startup, without the map.
|
|
3
|
+
*
|
|
4
|
+
* Nothing imported here may reach @maptiler/sdk, @maptiler/weather or uplot at runtime;
|
|
5
|
+
* `npm run test:bundle` enforces it. Everything is re-exported from the same modules the
|
|
6
|
+
* main entry uses, so `monitorsManager` and the router context stay single instances
|
|
7
|
+
* whichever entries a host mixes.
|
|
8
|
+
*/
|
|
9
|
+
export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter, type MonitorMapRouterApi } from "./router-context";
|
|
10
|
+
export { monitorsManager } from "./monitors/monitors.svelte.js";
|
|
11
|
+
export type { MonitorsManager } from "./monitors/monitors.svelte.js";
|
|
12
|
+
export type { MonitorsDataSource, MonitorsPollutant } from "./monitors/types";
|
package/dist/lib/core.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light entry (`@sjvair/monitor-map/core`): what a host app needs at startup, without the map.
|
|
3
|
+
*
|
|
4
|
+
* Nothing imported here may reach @maptiler/sdk, @maptiler/weather or uplot at runtime;
|
|
5
|
+
* `npm run test:bundle` enforces it. Everything is re-exported from the same modules the
|
|
6
|
+
* main entry uses, so `monitorsManager` and the router context stay single instances
|
|
7
|
+
* whichever entries a host mixes.
|
|
8
|
+
*/
|
|
9
|
+
export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter } from "./router-context";
|
|
10
|
+
export { monitorsManager } from "./monitors/monitors.svelte.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapManager } from "../map/map.svelte";
|
|
1
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
2
2
|
import { mountPopup } from "../map/utils";
|
|
3
3
|
import { clickManager } from "../map/integrations/click-manager";
|
|
4
4
|
import { EV_STATION_LEVELS } from "./ev-station-icons";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { untrack } from "svelte";
|
|
2
|
-
import { mapManager } from "../map/map.svelte";
|
|
2
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
3
3
|
import { clickManager } from "../map/integrations/click-manager";
|
|
4
4
|
import { MapIconLayerIntegration } from "../map/integrations/map-geojson-integration.svelte";
|
|
5
5
|
import { MapDisplayOption } from "../map/integrations/map-display-option.svelte";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapManager } from "../map/map.svelte";
|
|
1
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
2
2
|
import { MapIconLayerIntegration } from "../map/integrations/map-geojson-integration.svelte";
|
|
3
3
|
import { hmsManager } from "./hms.svelte";
|
|
4
4
|
import { HMSFireIconManager, getTierIconId } from "./hms-fire-icon-manager";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mapManager } from "../map/map.svelte";
|
|
1
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
2
2
|
import { MapGeoJSONIntegration } from "../map/integrations/map-geojson-integration.svelte";
|
|
3
3
|
import { hmsManager } from "./hms.svelte";
|
|
4
4
|
/** Smoke fill styles, shared by the map layer and the legend so they can't drift apart. */
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
*
|
|
24
24
|
* <Router />
|
|
25
25
|
* ```
|
|
26
|
+
*
|
|
27
|
+
* A host that must stay light at startup imports from the subpath entries instead:
|
|
28
|
+
* `provideMonitorMapRouter`/`monitorsManager` from "@sjvair/monitor-map/core" (no map), and
|
|
29
|
+
* `monitorMapRoutes` from "@sjvair/monitor-map/routes" (same tree, lazy layout and routes).
|
|
30
|
+
* `monitorMapRoutes` from this entry stays eager.
|
|
26
31
|
*/
|
|
27
32
|
export { monitorMapRoutes } from "./routes";
|
|
28
33
|
export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter, type MonitorMapRouterApi } from "./router-context";
|
|
@@ -34,8 +39,8 @@ export { evStationsManager } from "./ev-stations/ev-stations.svelte";
|
|
|
34
39
|
export type { EvStationsManager } from "./ev-stations/ev-stations.svelte";
|
|
35
40
|
export { hmsManager } from "./hms/hms.svelte";
|
|
36
41
|
export type { HMSManager } from "./hms/hms.svelte";
|
|
37
|
-
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
|
|
38
|
-
export type { MapManager } from "./map/map.svelte";
|
|
42
|
+
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map-manager.svelte.js";
|
|
43
|
+
export type { MapManager } from "./map/map-manager.svelte.js";
|
|
39
44
|
export { monitorsMapIntegration, MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
40
45
|
export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
|
|
41
46
|
export { evStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
|
package/dist/lib/index.js
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
*
|
|
24
24
|
* <Router />
|
|
25
25
|
* ```
|
|
26
|
+
*
|
|
27
|
+
* A host that must stay light at startup imports from the subpath entries instead:
|
|
28
|
+
* `provideMonitorMapRouter`/`monitorsManager` from "@sjvair/monitor-map/core" (no map), and
|
|
29
|
+
* `monitorMapRoutes` from "@sjvair/monitor-map/routes" (same tree, lazy layout and routes).
|
|
30
|
+
* `monitorMapRoutes` from this entry stays eager.
|
|
26
31
|
*/
|
|
27
32
|
export { monitorMapRoutes } from "./routes";
|
|
28
33
|
export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter } from "./router-context";
|
|
@@ -31,7 +36,7 @@ export { monitorsManager } from "./monitors/monitors.svelte";
|
|
|
31
36
|
export { collocationSitesManager } from "./collocation-sites/collocations.svelte";
|
|
32
37
|
export { evStationsManager } from "./ev-stations/ev-stations.svelte";
|
|
33
38
|
export { hmsManager } from "./hms/hms.svelte";
|
|
34
|
-
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
|
|
39
|
+
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map-manager.svelte.js";
|
|
35
40
|
// Map integrations
|
|
36
41
|
export { monitorsMapIntegration, MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
37
42
|
export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Routes } from "sv-router";
|
|
2
|
+
/**
|
|
3
|
+
* Lazy routes entry (`@sjvair/monitor-map/routes`): the same route tree as the main entry's
|
|
4
|
+
* `monitorMapRoutes`, but the layout and every route load on demand, so a host router can
|
|
5
|
+
* reference it at startup without loading the map.
|
|
6
|
+
*
|
|
7
|
+
* Each component must stay a literal `() => import(...)`: sv-router recognises lazy
|
|
8
|
+
* components by matching the function's source text, so a helper wrapper would break it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const monitorMapRoutes: Routes;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy routes entry (`@sjvair/monitor-map/routes`): the same route tree as the main entry's
|
|
3
|
+
* `monitorMapRoutes`, but the layout and every route load on demand, so a host router can
|
|
4
|
+
* reference it at startup without loading the map.
|
|
5
|
+
*
|
|
6
|
+
* Each component must stay a literal `() => import(...)`: sv-router recognises lazy
|
|
7
|
+
* components by matching the function's source text, so a helper wrapper would break it.
|
|
8
|
+
*/
|
|
9
|
+
export const monitorMapRoutes = {
|
|
10
|
+
layout: () => import("./MonitorMapLayout.svelte"),
|
|
11
|
+
"/": () => import("./MainMapView.svelte"),
|
|
12
|
+
"/monitor/:id": () => import("./MonitorDetailPanel.svelte")
|
|
13
|
+
};
|
package/dist/lib/map/Map.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import "@maptiler/sdk/dist/maptiler-sdk.css";
|
|
3
|
-
import { initializeMap } from "./map.svelte";
|
|
3
|
+
import { initializeMap } from "./map-manager.svelte.js";
|
|
4
4
|
import { integrationsManager } from "./integrations/integrations-manager";
|
|
5
5
|
import type { SomeMapIntegration } from "./integrations/types";
|
|
6
6
|
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
import LoadScreen, { disable as disableLoadScreen } from "../LoadScreen.svelte";
|
|
54
54
|
import Map from "./Map.svelte";
|
|
55
55
|
import OptionsBar from "../options/OptionsBar.svelte";
|
|
56
|
-
import { mapManager } from "./map.svelte";
|
|
56
|
+
import { mapManager } from "./map-manager.svelte.js";
|
|
57
57
|
import { useMonitorMapRouter } from "../router-context";
|
|
58
58
|
|
|
59
59
|
let {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
$effect(() => {
|
|
76
76
|
// mapManager.map is only ever assigned once its own initial "load" has
|
|
77
|
-
// already fired (see initializeMap in map.svelte.ts), so by the time this
|
|
77
|
+
// already fired (see initializeMap in map-manager.svelte.ts), so by the time this
|
|
78
78
|
// effect sees a non-null map, it's already safe to reveal — waiting for a
|
|
79
79
|
// further "idle" event here is unreliable since one may never fire again.
|
|
80
80
|
if (mapManager.map && ready) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MapLayerIntegration } from "./map-layer-integration.svelte";
|
|
2
|
-
import { mapManager } from "../map.svelte";
|
|
2
|
+
import { mapManager } from "../map-manager.svelte.js";
|
|
3
3
|
export class MapGeoJSONIntegration extends MapLayerIntegration {
|
|
4
4
|
remove() {
|
|
5
5
|
this.removeLayerAndSource();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { untrack } from "svelte";
|
|
2
|
-
import { mapManager } from "../map.svelte";
|
|
2
|
+
import { mapManager } from "../map-manager.svelte.js";
|
|
3
3
|
import { MapIntegration } from "./map-integration.svelte";
|
|
4
4
|
export class MapLayerIntegration extends MapIntegration {
|
|
5
5
|
beforeLayer;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DefaultMapStyle, mapManager } from "./map.svelte";
|
|
1
|
+
import { DefaultMapStyle, mapManager } from "./map-manager.svelte.js";
|
|
2
2
|
import { integrationsManager } from "./integrations/integrations-manager";
|
|
3
3
|
import { variantForStyle } from "./basemap";
|
|
4
4
|
/** Basemap + theme selection, shared so the Settings menu and Reset Map Options agree. */
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { area } from "@turf/area";
|
|
7
7
|
import { convertArea } from "@turf/helpers";
|
|
8
8
|
import DataBox from "../../components/DataBox.svelte";
|
|
9
|
-
import { mapManager } from "../../map/map.svelte";
|
|
9
|
+
import { mapManager } from "../../map/map-manager.svelte.js";
|
|
10
10
|
import type { MonitorClusterMapFeature, MonitorMapFeature } from "../types";
|
|
11
11
|
import { monitorsManager } from "../monitors.svelte";
|
|
12
12
|
import { getCurrentLevel } from "../monitor-utils";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { asDataURI, circle, square, triangle } from "../map/icons";
|
|
2
2
|
import { MapIconManager } from "../map/integrations/map-icon-manager";
|
|
3
|
-
import { mapManager } from "../map/map.svelte";
|
|
3
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
4
4
|
const MONITOR_ICONS = { circle, square, triangle };
|
|
5
5
|
const MONITOR_ICON_BORDER_WIDTH = 2;
|
|
6
6
|
const MONITOR_ICON_DEFAULT_COLOR = "#969696"; // light gray
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { untrack } from "svelte";
|
|
2
|
-
import { mapManager } from "../map/map.svelte";
|
|
2
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
3
3
|
import { clickManager } from "../map/integrations/click-manager";
|
|
4
4
|
import { MapIconLayerIntegration } from "../map/integrations/map-geojson-integration.svelte";
|
|
5
5
|
import { monitorsManager } from "./monitors.svelte";
|
|
@@ -3,6 +3,7 @@ import { XMap } from "@tstk/builtin-extensions";
|
|
|
3
3
|
import { Interval } from "@tstk/utils";
|
|
4
4
|
import type { MonitorsDataSource } from "./types";
|
|
5
5
|
declare class MonitorsManager implements MonitorsDataSource {
|
|
6
|
+
#private;
|
|
6
7
|
autoUpdate: Interval;
|
|
7
8
|
initialized: boolean;
|
|
8
9
|
latest: XMap<string, MonitorLatestType<"pm25" | "o3">> | null;
|
|
@@ -11,7 +12,17 @@ declare class MonitorsManager implements MonitorsDataSource {
|
|
|
11
12
|
pollutant: "pm25" | "o3" | null;
|
|
12
13
|
levels: Array<SJVAirEntryLevel> | null;
|
|
13
14
|
constructor();
|
|
15
|
+
/**
|
|
16
|
+
* Safe to call from several places: overlapping calls share one run, and a failed run can
|
|
17
|
+
* be retried. An explicit "pm25"/"o3" wins over a call without one, whichever came first;
|
|
18
|
+
* between two explicit values the most recent wins.
|
|
19
|
+
*/
|
|
14
20
|
init(urlPollutant?: string | number | boolean): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Restarts polling after something stopped it (MonitorMapLayout stops it on destroy),
|
|
23
|
+
* refreshing first since the data may be stale. A no-op before init or while polling.
|
|
24
|
+
*/
|
|
25
|
+
resumeAutoUpdate(): void;
|
|
15
26
|
update(): Promise<void>;
|
|
16
27
|
}
|
|
17
28
|
export declare const monitorsManager: MonitorsManager;
|
|
@@ -10,6 +10,8 @@ class MonitorsManager {
|
|
|
10
10
|
meta = $state(null);
|
|
11
11
|
pollutant = $state(null);
|
|
12
12
|
levels = $derived(this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null);
|
|
13
|
+
#initPromise = null;
|
|
14
|
+
#requestedPollutant = null;
|
|
13
15
|
constructor() {
|
|
14
16
|
// Refetch monitors when the pollutant changes after the initial load, wherever the change
|
|
15
17
|
// comes from (Pollutant menu, URL sync, a host's own UI). Clearing first blanks the markers
|
|
@@ -31,23 +33,80 @@ class MonitorsManager {
|
|
|
31
33
|
});
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Safe to call from several places: overlapping calls share one run, and a failed run can
|
|
38
|
+
* be retried. An explicit "pm25"/"o3" wins over a call without one, whichever came first;
|
|
39
|
+
* between two explicit values the most recent wins.
|
|
40
|
+
*/
|
|
41
|
+
init(urlPollutant) {
|
|
35
42
|
if (this.initialized)
|
|
36
|
-
return;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this
|
|
43
|
+
return Promise.resolve();
|
|
44
|
+
if (urlPollutant === "pm25" || urlPollutant === "o3") {
|
|
45
|
+
this.#requestedPollutant = urlPollutant;
|
|
46
|
+
}
|
|
47
|
+
this.#initPromise ??= this.#runInit().finally(() => {
|
|
48
|
+
this.#initPromise = null;
|
|
49
|
+
this.#requestedPollutant = null;
|
|
50
|
+
});
|
|
51
|
+
return this.#initPromise;
|
|
52
|
+
}
|
|
53
|
+
async #runInit() {
|
|
54
|
+
// The pollutant the latest readings were requested for.
|
|
55
|
+
let fetchedPollutant = this.#requestedPollutant;
|
|
56
|
+
const metaRequest = getMonitorsMeta();
|
|
57
|
+
const listAndMeta = Promise.all([getMonitorsList(), metaRequest]).then(([list, meta]) => {
|
|
58
|
+
this.list = list;
|
|
59
|
+
this.meta = meta;
|
|
60
|
+
return meta;
|
|
61
|
+
});
|
|
62
|
+
// The latest readings only need the pollutant, so they never wait for the list. An explicit
|
|
63
|
+
// pollutant may still arrive before meta does, in which case the default is never fetched.
|
|
64
|
+
const latestRequest = fetchedPollutant
|
|
65
|
+
? getMonitorsLatestMap(fetchedPollutant)
|
|
66
|
+
: metaRequest.then((meta) => {
|
|
67
|
+
const pollutant = this.#requestedPollutant ?? meta.default_pollutant;
|
|
68
|
+
fetchedPollutant = pollutant;
|
|
69
|
+
return getMonitorsLatestMap(pollutant);
|
|
70
|
+
});
|
|
71
|
+
// One Promise.all so a failure in either never leaves the other unhandled.
|
|
72
|
+
const [meta, firstLatest] = await Promise.all([listAndMeta, latestRequest]);
|
|
73
|
+
let latest = firstLatest;
|
|
74
|
+
// An explicit pollutant may have arrived while the requests above were in flight.
|
|
75
|
+
let wanted = this.#requestedPollutant ?? meta.default_pollutant;
|
|
76
|
+
while (wanted !== fetchedPollutant) {
|
|
77
|
+
latest = await getMonitorsLatestMap(wanted);
|
|
78
|
+
fetchedPollutant = wanted;
|
|
79
|
+
wanted = this.#requestedPollutant ?? meta.default_pollutant;
|
|
80
|
+
}
|
|
81
|
+
this.pollutant = fetchedPollutant;
|
|
82
|
+
this.latest = latest;
|
|
41
83
|
this.autoUpdate.start();
|
|
42
84
|
this.initialized = true;
|
|
43
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Restarts polling after something stopped it (MonitorMapLayout stops it on destroy),
|
|
88
|
+
* refreshing first since the data may be stale. A no-op before init or while polling.
|
|
89
|
+
*/
|
|
90
|
+
resumeAutoUpdate() {
|
|
91
|
+
if (!this.initialized || this.autoUpdate.isActive)
|
|
92
|
+
return;
|
|
93
|
+
this.update().catch(console.error);
|
|
94
|
+
this.autoUpdate.start();
|
|
95
|
+
}
|
|
44
96
|
async update() {
|
|
45
97
|
if (!this.initialized)
|
|
46
98
|
return;
|
|
47
|
-
|
|
99
|
+
const pollutant = this.pollutant;
|
|
100
|
+
const [list, latest] = await Promise.all([
|
|
48
101
|
getMonitorsList(),
|
|
49
|
-
getMonitorsLatestMap(
|
|
102
|
+
getMonitorsLatestMap(pollutant || this.meta?.default_pollutant || "pm25")
|
|
50
103
|
]);
|
|
104
|
+
// The pollutant changed while this was in flight: these readings belong to the previous
|
|
105
|
+
// one, and the change has already started its own update.
|
|
106
|
+
if (this.pollutant !== pollutant)
|
|
107
|
+
return;
|
|
108
|
+
this.list = list;
|
|
109
|
+
this.latest = latest;
|
|
51
110
|
}
|
|
52
111
|
}
|
|
53
112
|
async function getMonitorsLatestMap(pollutant) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Marker } from "@maptiler/sdk";
|
|
4
4
|
import { MapPinIcon, SearchIcon } from "@lucide/svelte";
|
|
5
5
|
import { useMonitorMapRouter } from "../router-context";
|
|
6
|
-
import { mapManager } from "../map/map.svelte";
|
|
6
|
+
import { mapManager } from "../map/map-manager.svelte.js";
|
|
7
7
|
import { debounce } from "../utils";
|
|
8
8
|
import { monitorSearch, geocode, type SearchResult } from "./service";
|
|
9
9
|
import { monitorsMapIntegration } from "../monitors/monitors-map-integration.svelte";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjvair/monitor-map",
|
|
3
3
|
"description": "SJVAir's interactive monitor map",
|
|
4
|
-
"version": "4.0
|
|
4
|
+
"version": "4.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/SJVAir/monitor-map"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
|
17
17
|
"format": "prettier --write .",
|
|
18
18
|
"lint": "prettier --check . && eslint .",
|
|
19
|
-
"test": "vitest run"
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:bundle": "npm run build:lib && node tests/bundle/check-bundle.mjs"
|
|
20
21
|
},
|
|
21
22
|
"type": "module",
|
|
22
23
|
"svelte": "./dist/lib/index.js",
|
|
@@ -26,6 +27,16 @@
|
|
|
26
27
|
"svelte": "./dist/lib/index.js",
|
|
27
28
|
"types": "./dist/lib/index.d.ts",
|
|
28
29
|
"default": "./dist/lib/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./core": {
|
|
32
|
+
"svelte": "./dist/lib/core.js",
|
|
33
|
+
"types": "./dist/lib/core.d.ts",
|
|
34
|
+
"default": "./dist/lib/core.js"
|
|
35
|
+
},
|
|
36
|
+
"./routes": {
|
|
37
|
+
"svelte": "./dist/lib/lazy-routes.js",
|
|
38
|
+
"types": "./dist/lib/lazy-routes.d.ts",
|
|
39
|
+
"default": "./dist/lib/lazy-routes.js"
|
|
29
40
|
}
|
|
30
41
|
},
|
|
31
42
|
"files": [
|
|
@@ -37,7 +48,7 @@
|
|
|
37
48
|
],
|
|
38
49
|
"peerDependencies": {
|
|
39
50
|
"@internationalized/date": "^3.12.4",
|
|
40
|
-
"@sveltejs/enhanced-img": "^0.11.0",
|
|
51
|
+
"@sveltejs/enhanced-img": "^0.11.0 || ^1.0.0",
|
|
41
52
|
"bits-ui": "^2.19.2",
|
|
42
53
|
"sv-router": "^0.19.0",
|
|
43
54
|
"svelte": "^5.0.0"
|
|
@@ -46,7 +57,7 @@
|
|
|
46
57
|
"@eslint/compat": "^2.1.1",
|
|
47
58
|
"@eslint/js": "^10.0.1",
|
|
48
59
|
"@fontsource-variable/inter": "^5.3.0",
|
|
49
|
-
"@sveltejs/enhanced-img": "^0.
|
|
60
|
+
"@sveltejs/enhanced-img": "^1.0.0",
|
|
50
61
|
"@sveltejs/package": "^2.5.8",
|
|
51
62
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
52
63
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
monitorsManager.init(route.search.pollutant);
|
|
65
|
+
// A previous instance of this layout stopped polling in onDestroy; init() is a no-op by now.
|
|
66
|
+
monitorsManager.resumeAutoUpdate();
|
|
65
67
|
collocationSitesManager.init();
|
|
66
68
|
hmsManager.init();
|
|
67
69
|
monitorsMapIntegration.onMonitorClick = (id: string) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Popup, type MapLayerEventType, type Map as MaptilerMap } from "@maptiler/sdk";
|
|
2
2
|
import type { Geometry } from "geojson";
|
|
3
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
3
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
4
4
|
import { MapIconLayerIntegration } from "$lib/map/integrations/map-geojson-integration.svelte";
|
|
5
5
|
import { CollocationIconManager, getCollocationIconId } from "./collocations-icon-manager.svelte";
|
|
6
6
|
import { TooltipManager } from "$lib/map/integrations/tooltip.svelte";
|
package/src/lib/core.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Light entry (`@sjvair/monitor-map/core`): what a host app needs at startup, without the map.
|
|
3
|
+
*
|
|
4
|
+
* Nothing imported here may reach @maptiler/sdk, @maptiler/weather or uplot at runtime;
|
|
5
|
+
* `npm run test:bundle` enforces it. Everything is re-exported from the same modules the
|
|
6
|
+
* main entry uses, so `monitorsManager` and the router context stay single instances
|
|
7
|
+
* whichever entries a host mixes.
|
|
8
|
+
*/
|
|
9
|
+
export {
|
|
10
|
+
ROUTER_KEY,
|
|
11
|
+
provideMonitorMapRouter,
|
|
12
|
+
useMonitorMapRouter,
|
|
13
|
+
type MonitorMapRouterApi
|
|
14
|
+
} from "./router-context";
|
|
15
|
+
export { monitorsManager } from "./monitors/monitors.svelte.js";
|
|
16
|
+
export type { MonitorsManager } from "./monitors/monitors.svelte.js";
|
|
17
|
+
export type { MonitorsDataSource, MonitorsPollutant } from "./monitors/types";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GeoJSONSource, MapLayerEventType, Popup } from "@maptiler/sdk";
|
|
2
2
|
import type { Point } from "geojson";
|
|
3
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
3
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
4
4
|
import { mountPopup } from "$lib/map/utils";
|
|
5
5
|
import { clickManager, type ClickHandler } from "$lib/map/integrations/click-manager";
|
|
6
6
|
import { TooltipManager } from "$lib/map/integrations/tooltip.svelte";
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
} from "@maptiler/sdk";
|
|
7
7
|
import type { Geometry } from "geojson";
|
|
8
8
|
import { untrack } from "svelte";
|
|
9
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
9
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
10
10
|
import { clickManager, type ClickHandler } from "$lib/map/integrations/click-manager";
|
|
11
11
|
import { MapIconLayerIntegration } from "$lib/map/integrations/map-geojson-integration.svelte";
|
|
12
12
|
import { MapDisplayOption } from "$lib/map/integrations/map-display-option.svelte";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Map as MaptilerMap } from "@maptiler/sdk";
|
|
2
2
|
import type { Feature } from "geojson";
|
|
3
3
|
import type { Point } from "geojson";
|
|
4
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
4
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
5
5
|
import { MapIconLayerIntegration } from "$lib/map/integrations/map-geojson-integration.svelte";
|
|
6
6
|
import { hmsManager } from "./hms.svelte";
|
|
7
7
|
import { HMSFireIconManager, getTierIconId } from "./hms-fire-icon-manager";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Map as MaptilerMap } from "@maptiler/sdk";
|
|
2
2
|
import type { Feature, MultiPolygon } from "geojson";
|
|
3
3
|
import type { HMSSmokeGeoJSON } from "@sjvair/sdk/hms";
|
|
4
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
4
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
5
5
|
import { MapGeoJSONIntegration } from "$lib/map/integrations/map-geojson-integration.svelte";
|
|
6
6
|
import { hmsManager } from "./hms.svelte";
|
|
7
7
|
import type { SmokeDensityStyle } from "$lib/legend/legend-data";
|
package/src/lib/index.ts
CHANGED
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
*
|
|
24
24
|
* <Router />
|
|
25
25
|
* ```
|
|
26
|
+
*
|
|
27
|
+
* A host that must stay light at startup imports from the subpath entries instead:
|
|
28
|
+
* `provideMonitorMapRouter`/`monitorsManager` from "@sjvair/monitor-map/core" (no map), and
|
|
29
|
+
* `monitorMapRoutes` from "@sjvair/monitor-map/routes" (same tree, lazy layout and routes).
|
|
30
|
+
* `monitorMapRoutes` from this entry stays eager.
|
|
26
31
|
*/
|
|
27
32
|
export { monitorMapRoutes } from "./routes";
|
|
28
33
|
export {
|
|
@@ -41,8 +46,8 @@ export { evStationsManager } from "./ev-stations/ev-stations.svelte";
|
|
|
41
46
|
export type { EvStationsManager } from "./ev-stations/ev-stations.svelte";
|
|
42
47
|
export { hmsManager } from "./hms/hms.svelte";
|
|
43
48
|
export type { HMSManager } from "./hms/hms.svelte";
|
|
44
|
-
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
|
|
45
|
-
export type { MapManager } from "./map/map.svelte";
|
|
49
|
+
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map-manager.svelte.js";
|
|
50
|
+
export type { MapManager } from "./map/map-manager.svelte.js";
|
|
46
51
|
|
|
47
52
|
// Map integrations
|
|
48
53
|
export {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Routes } from "sv-router";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy routes entry (`@sjvair/monitor-map/routes`): the same route tree as the main entry's
|
|
5
|
+
* `monitorMapRoutes`, but the layout and every route load on demand, so a host router can
|
|
6
|
+
* reference it at startup without loading the map.
|
|
7
|
+
*
|
|
8
|
+
* Each component must stay a literal `() => import(...)`: sv-router recognises lazy
|
|
9
|
+
* components by matching the function's source text, so a helper wrapper would break it.
|
|
10
|
+
*/
|
|
11
|
+
export const monitorMapRoutes: Routes = {
|
|
12
|
+
layout: () => import("./MonitorMapLayout.svelte"),
|
|
13
|
+
"/": () => import("./MainMapView.svelte"),
|
|
14
|
+
"/monitor/:id": () => import("./MonitorDetailPanel.svelte")
|
|
15
|
+
};
|
package/src/lib/map/Map.svelte
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import "@maptiler/sdk/dist/maptiler-sdk.css";
|
|
3
|
-
import { initializeMap } from "./map.svelte";
|
|
3
|
+
import { initializeMap } from "./map-manager.svelte.js";
|
|
4
4
|
import { integrationsManager } from "$lib/map/integrations/integrations-manager";
|
|
5
5
|
import type { SomeMapIntegration } from "./integrations/types";
|
|
6
6
|
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
import LoadScreen, { disable as disableLoadScreen } from "$lib/LoadScreen.svelte";
|
|
54
54
|
import Map from "$lib/map/Map.svelte";
|
|
55
55
|
import OptionsBar from "$lib/options/OptionsBar.svelte";
|
|
56
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
56
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
57
57
|
import { useMonitorMapRouter } from "$lib/router-context";
|
|
58
58
|
|
|
59
59
|
let {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
|
|
75
75
|
$effect(() => {
|
|
76
76
|
// mapManager.map is only ever assigned once its own initial "load" has
|
|
77
|
-
// already fired (see initializeMap in map.svelte.ts), so by the time this
|
|
77
|
+
// already fired (see initializeMap in map-manager.svelte.ts), so by the time this
|
|
78
78
|
// effect sees a non-null map, it's already safe to reveal — waiting for a
|
|
79
79
|
// further "idle" event here is unreliable since one may never fire again.
|
|
80
80
|
if (mapManager.map && ready) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MapGeoJSONFeature, MapMouseEvent } from "@maptiler/sdk";
|
|
2
|
-
import { mapManager } from "../map.svelte";
|
|
2
|
+
import { mapManager } from "../map-manager.svelte.js";
|
|
3
3
|
|
|
4
4
|
export type ClickHandler = (features: MapGeoJSONFeature[], evt: MapMouseEvent & object) => void;
|
|
5
5
|
|
|
@@ -2,7 +2,7 @@ import type { Map as MaptilerMap } from "@maptiler/sdk";
|
|
|
2
2
|
import type { Feature, Geometry } from "geojson";
|
|
3
3
|
import type { MapIconManager } from "./map-icon-manager";
|
|
4
4
|
import { MapLayerIntegration } from "./map-layer-integration.svelte";
|
|
5
|
-
import { mapManager } from "../map.svelte";
|
|
5
|
+
import { mapManager } from "../map-manager.svelte.js";
|
|
6
6
|
|
|
7
7
|
export abstract class MapGeoJSONIntegration<
|
|
8
8
|
T = Record<string, unknown>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Map as MaptilerMap } from "@maptiler/sdk";
|
|
2
2
|
import { untrack } from "svelte";
|
|
3
|
-
import { mapManager } from "../map.svelte";
|
|
3
|
+
import { mapManager } from "../map-manager.svelte.js";
|
|
4
4
|
import { MapIntegration } from "./map-integration.svelte";
|
|
5
5
|
|
|
6
6
|
export abstract class MapLayerIntegration extends MapIntegration {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MapLayerEventType, Popup } from "@maptiler/sdk";
|
|
2
|
-
import { mapManager } from "../map.svelte";
|
|
2
|
+
import { mapManager } from "../map-manager.svelte.js";
|
|
3
3
|
import { XMap } from "@tstk/builtin-extensions";
|
|
4
4
|
|
|
5
5
|
export type TooltipBuilder = (evt: MapLayerEventType["mousemove"] & object) => Popup | void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MapStyleVariant, ReferenceMapStyle } from "@maptiler/sdk";
|
|
2
|
-
import { DefaultMapStyle, mapManager } from "./map.svelte";
|
|
2
|
+
import { DefaultMapStyle, mapManager } from "./map-manager.svelte.js";
|
|
3
3
|
import { integrationsManager } from "./integrations/integrations-manager";
|
|
4
4
|
import { variantForStyle } from "./basemap";
|
|
5
5
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { area } from "@turf/area";
|
|
7
7
|
import { convertArea } from "@turf/helpers";
|
|
8
8
|
import DataBox from "$lib/components/DataBox.svelte";
|
|
9
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
9
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
10
10
|
import type { MonitorClusterMapFeature, MonitorMapFeature } from "../types";
|
|
11
11
|
import { monitorsManager } from "../monitors.svelte";
|
|
12
12
|
import { getCurrentLevel } from "../monitor-utils";
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
} from "@maptiler/sdk";
|
|
8
8
|
import type { Point } from "geojson";
|
|
9
9
|
import type { SJVAirEntryLevel } from "@sjvair/sdk";
|
|
10
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
10
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
11
11
|
import { mountPopup } from "$lib/map/utils";
|
|
12
12
|
import { clickManager, type ClickHandler } from "$lib/map/integrations/click-manager";
|
|
13
13
|
import { TooltipManager } from "$lib/map/integrations/tooltip.svelte";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MonitorData, SJVAirEntryLevel } from "@sjvair/sdk";
|
|
2
2
|
import { asDataURI, circle, square, triangle } from "$lib/map/icons";
|
|
3
3
|
import { MapIconManager } from "$lib/map/integrations/map-icon-manager";
|
|
4
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
4
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
5
5
|
import type { MonitorsDataSource } from "./types";
|
|
6
6
|
|
|
7
7
|
const MONITOR_ICONS = { circle, square, triangle };
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import type { MonitorType, SJVAirEntryLevel } from "@sjvair/sdk";
|
|
7
7
|
import { untrack } from "svelte";
|
|
8
8
|
import type { Geometry } from "geojson";
|
|
9
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
9
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
10
10
|
import { clickManager, type ClickHandler } from "$lib/map/integrations/click-manager";
|
|
11
11
|
import { MapIconLayerIntegration } from "$lib/map/integrations/map-geojson-integration.svelte";
|
|
12
12
|
import { monitorsManager } from "./monitors.svelte";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { XMap } from "@tstk/builtin-extensions";
|
|
11
11
|
import { Interval } from "@tstk/utils";
|
|
12
12
|
import { untrack } from "svelte";
|
|
13
|
-
import type { MonitorsDataSource } from "./types";
|
|
13
|
+
import type { MonitorsDataSource, MonitorsPollutant } from "./types";
|
|
14
14
|
|
|
15
15
|
class MonitorsManager implements MonitorsDataSource {
|
|
16
16
|
autoUpdate: Interval = new Interval(async () => await this.update(), 2 * 60 * 1000);
|
|
@@ -27,6 +27,9 @@ class MonitorsManager implements MonitorsDataSource {
|
|
|
27
27
|
this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null
|
|
28
28
|
);
|
|
29
29
|
|
|
30
|
+
#initPromise: Promise<void> | null = null;
|
|
31
|
+
#requestedPollutant: MonitorsPollutant | null = null;
|
|
32
|
+
|
|
30
33
|
constructor() {
|
|
31
34
|
// Refetch monitors when the pollutant changes after the initial load, wherever the change
|
|
32
35
|
// comes from (Pollutant menu, URL sync, a host's own UI). Clearing first blanks the markers
|
|
@@ -48,25 +51,90 @@ class MonitorsManager implements MonitorsDataSource {
|
|
|
48
51
|
});
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Safe to call from several places: overlapping calls share one run, and a failed run can
|
|
56
|
+
* be retried. An explicit "pm25"/"o3" wins over a call without one, whichever came first;
|
|
57
|
+
* between two explicit values the most recent wins.
|
|
58
|
+
*/
|
|
59
|
+
init(urlPollutant?: string | number | boolean): Promise<void> {
|
|
60
|
+
if (this.initialized) return Promise.resolve();
|
|
61
|
+
|
|
62
|
+
if (urlPollutant === "pm25" || urlPollutant === "o3") {
|
|
63
|
+
this.#requestedPollutant = urlPollutant;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.#initPromise ??= this.#runInit().finally(() => {
|
|
67
|
+
this.#initPromise = null;
|
|
68
|
+
this.#requestedPollutant = null;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return this.#initPromise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async #runInit(): Promise<void> {
|
|
75
|
+
// The pollutant the latest readings were requested for.
|
|
76
|
+
let fetchedPollutant = this.#requestedPollutant;
|
|
77
|
+
|
|
78
|
+
const metaRequest = getMonitorsMeta();
|
|
79
|
+
const listAndMeta = Promise.all([getMonitorsList(), metaRequest]).then(([list, meta]) => {
|
|
80
|
+
this.list = list;
|
|
81
|
+
this.meta = meta;
|
|
82
|
+
return meta;
|
|
83
|
+
});
|
|
84
|
+
// The latest readings only need the pollutant, so they never wait for the list. An explicit
|
|
85
|
+
// pollutant may still arrive before meta does, in which case the default is never fetched.
|
|
86
|
+
const latestRequest = fetchedPollutant
|
|
87
|
+
? getMonitorsLatestMap(fetchedPollutant)
|
|
88
|
+
: metaRequest.then((meta) => {
|
|
89
|
+
const pollutant = this.#requestedPollutant ?? meta.default_pollutant;
|
|
90
|
+
fetchedPollutant = pollutant;
|
|
91
|
+
return getMonitorsLatestMap(pollutant);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// One Promise.all so a failure in either never leaves the other unhandled.
|
|
95
|
+
const [meta, firstLatest] = await Promise.all([listAndMeta, latestRequest]);
|
|
96
|
+
let latest = firstLatest;
|
|
53
97
|
|
|
54
|
-
|
|
98
|
+
// An explicit pollutant may have arrived while the requests above were in flight.
|
|
99
|
+
let wanted = this.#requestedPollutant ?? meta.default_pollutant;
|
|
100
|
+
while (wanted !== fetchedPollutant) {
|
|
101
|
+
latest = await getMonitorsLatestMap(wanted);
|
|
102
|
+
fetchedPollutant = wanted;
|
|
103
|
+
wanted = this.#requestedPollutant ?? meta.default_pollutant;
|
|
104
|
+
}
|
|
55
105
|
|
|
56
|
-
this.pollutant =
|
|
57
|
-
|
|
58
|
-
this.latest = await getMonitorsLatestMap(this.pollutant);
|
|
106
|
+
this.pollutant = fetchedPollutant;
|
|
107
|
+
this.latest = latest;
|
|
59
108
|
this.autoUpdate.start();
|
|
60
109
|
this.initialized = true;
|
|
61
110
|
}
|
|
62
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Restarts polling after something stopped it (MonitorMapLayout stops it on destroy),
|
|
114
|
+
* refreshing first since the data may be stale. A no-op before init or while polling.
|
|
115
|
+
*/
|
|
116
|
+
resumeAutoUpdate(): void {
|
|
117
|
+
if (!this.initialized || this.autoUpdate.isActive) return;
|
|
118
|
+
|
|
119
|
+
this.update().catch(console.error);
|
|
120
|
+
this.autoUpdate.start();
|
|
121
|
+
}
|
|
122
|
+
|
|
63
123
|
async update(): Promise<void> {
|
|
64
124
|
if (!this.initialized) return;
|
|
65
125
|
|
|
66
|
-
|
|
126
|
+
const pollutant = this.pollutant;
|
|
127
|
+
const [list, latest] = await Promise.all([
|
|
67
128
|
getMonitorsList(),
|
|
68
|
-
getMonitorsLatestMap(
|
|
129
|
+
getMonitorsLatestMap(pollutant || this.meta?.default_pollutant || "pm25")
|
|
69
130
|
]);
|
|
131
|
+
|
|
132
|
+
// The pollutant changed while this was in flight: these readings belong to the previous
|
|
133
|
+
// one, and the change has already started its own update.
|
|
134
|
+
if (this.pollutant !== pollutant) return;
|
|
135
|
+
|
|
136
|
+
this.list = list;
|
|
137
|
+
this.latest = latest;
|
|
70
138
|
}
|
|
71
139
|
}
|
|
72
140
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Marker } from "@maptiler/sdk";
|
|
4
4
|
import { MapPinIcon, SearchIcon } from "@lucide/svelte";
|
|
5
5
|
import { useMonitorMapRouter } from "../router-context";
|
|
6
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
6
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
7
7
|
import { debounce } from "$lib/utils";
|
|
8
8
|
import { monitorSearch, geocode, type SearchResult } from "./service";
|
|
9
9
|
import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
|
|
File without changes
|
|
File without changes
|
|
File without changes
|