@sjvair/monitor-map 3.3.0 → 3.4.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 +140 -31
- package/dist/lib/MonitorMapLayout.svelte +7 -2
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +1 -1
- package/dist/lib/index.d.ts +2 -3
- package/dist/lib/index.js +1 -1
- package/dist/lib/map/MapShell.svelte +5 -1
- package/dist/lib/map/map.svelte.js +1 -1
- package/dist/lib/monitors/monitors-icon-manager.svelte.d.ts +3 -1
- package/dist/lib/monitors/monitors-icon-manager.svelte.js +5 -4
- package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +4 -3
- package/dist/lib/monitors/monitors-map-integration.svelte.js +11 -7
- package/dist/lib/monitors/monitors.svelte.d.ts +2 -1
- package/dist/lib/monitors/monitors.svelte.js +4 -4
- package/dist/lib/monitors/types.d.ts +17 -1
- package/package.json +2 -2
- package/src/lib/MonitorMapLayout.svelte +7 -2
- package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +1 -1
- package/src/lib/index.ts +7 -3
- package/src/lib/map/MapShell.svelte +5 -1
- package/src/lib/map/map.svelte.ts +1 -1
- package/src/lib/monitors/monitors-icon-manager.svelte.ts +4 -4
- package/src/lib/monitors/monitors-map-integration.svelte.ts +10 -9
- package/src/lib/monitors/monitors.svelte.ts +6 -5
- package/src/lib/monitors/types.ts +19 -1
package/README.md
CHANGED
|
@@ -1,58 +1,167 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sjvair/monitor-map
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SJVAir's interactive air quality monitor map, built with Svelte 5, Vite, and MapTiler
|
|
4
|
+
SDK. It ships two ways:
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
- **A code-split widget** (`npm run build`), embedded via a `<div id="SJVAirMonitorMap">`
|
|
7
|
+
in a Django template on sjvair.com.
|
|
8
|
+
- **A component library** (`npm run build:lib`, published to npm as
|
|
9
|
+
`@sjvair/monitor-map`), imported by the `v3-mobile` Capacitor app and by other SJVAir
|
|
10
|
+
projects (e.g. `data-dashboard`) that want to embed the map or reuse its map-display
|
|
11
|
+
logic against their own data.
|
|
6
12
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
If you're seeing this, you've probably already done this step. Congrats!
|
|
13
|
+
## Installation
|
|
10
14
|
|
|
11
15
|
```bash
|
|
12
|
-
|
|
13
|
-
npx sv create
|
|
14
|
-
|
|
15
|
-
# create a new project in my-app
|
|
16
|
-
npx sv create my-app
|
|
16
|
+
npm install @sjvair/monitor-map
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Usage
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### Full-page map, via `sv-router`
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
The default integration path mounts a complete map (all integrations: monitors,
|
|
24
|
+
collocation sites, wind, HMS smoke/fire, EV stations) as a route in a host app that
|
|
25
|
+
uses `sv-router`:
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
```ts
|
|
28
|
+
// router.ts
|
|
29
|
+
import { createRouter } from "sv-router";
|
|
30
|
+
import { monitorMapRoutes } from "@sjvair/monitor-map";
|
|
31
|
+
|
|
32
|
+
export const { route, navigate, p, isActive, Router } = createRouter({
|
|
33
|
+
"/my-map": monitorMapRoutes
|
|
34
|
+
// ...the host app's own routes
|
|
35
|
+
});
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
```svelte
|
|
39
|
+
<!-- app root -->
|
|
40
|
+
<script>
|
|
41
|
+
import { provideMonitorMapRouter } from "@sjvair/monitor-map";
|
|
42
|
+
import { route, navigate, p, isActive, Router } from "./router";
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
provideMonitorMapRouter({ route, navigate, p, isActive, basePath: "/my-map" });
|
|
45
|
+
</script>
|
|
33
46
|
|
|
34
|
-
|
|
47
|
+
<Router />
|
|
48
|
+
```
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
50
|
+
This is what the sjvair.com widget and `v3-mobile` use today, via `MonitorMapLayout`
|
|
51
|
+
(the thin wrapper `monitorMapRoutes` points to).
|
|
52
|
+
|
|
53
|
+
### Embedding `MapShell` directly, with a reduced integration set
|
|
54
|
+
|
|
55
|
+
`MonitorMapLayout` is an opinionated wrapper around the lower-level `MapShell`
|
|
56
|
+
component: it wires up every integration, every manager, and the full display-options
|
|
57
|
+
menu. A host that only wants some of that — say, just the monitors integration, with no
|
|
58
|
+
EV stations/wind/HMS — should compose `MapShell` directly instead:
|
|
59
|
+
|
|
60
|
+
```svelte
|
|
61
|
+
<script>
|
|
62
|
+
import { MapShell, monitorsMapIntegration, monitorsManager, Menu } from "@sjvair/monitor-map";
|
|
63
|
+
|
|
64
|
+
monitorsManager.init();
|
|
65
|
+
</script>
|
|
66
|
+
|
|
67
|
+
<MapShell
|
|
68
|
+
integrations={[monitorsMapIntegration]}
|
|
69
|
+
ready={monitorsManager.initialized}
|
|
70
|
+
panelOpen={false}
|
|
71
|
+
routerEscapeHatch={false}
|
|
72
|
+
>
|
|
73
|
+
{#snippet menu()}
|
|
74
|
+
<Menu>
|
|
75
|
+
<!-- display options for the integrations you included -->
|
|
76
|
+
</Menu>
|
|
77
|
+
{/snippet}
|
|
78
|
+
</MapShell>
|
|
38
79
|
```
|
|
39
80
|
|
|
40
|
-
|
|
81
|
+
See `src/lib/MonitorMapLayout.svelte` for the reference example of wiring a manager, an
|
|
82
|
+
integration list, and menu/overlay content into `MapShell`.
|
|
41
83
|
|
|
42
|
-
|
|
43
|
-
|
|
84
|
+
**`routerEscapeHatch`**: `MapShell` defaults to `routerEscapeHatch={true}`, which
|
|
85
|
+
intercepts clicks and forces full-page navigation for links outside the map's own
|
|
86
|
+
routes — correct for a full-page map, wrong for a map embedded alongside a host app's
|
|
87
|
+
own navigation. A host with its own routing (like `data-dashboard`) **must** pass
|
|
88
|
+
`routerEscapeHatch={false}`, which also skips `useMonitorMapRouter()` entirely, so no
|
|
89
|
+
`provideMonitorMapRouter(...)` ancestor is required. Pass `basePath` if you need the
|
|
90
|
+
escape-hatch's "known route" prefix without going through `provideMonitorMapRouter`.
|
|
91
|
+
|
|
92
|
+
Mount at most one `MapShell` per app instance — its underlying map/load-screen/
|
|
93
|
+
integration-registration state is module-level singleton state shared across the whole
|
|
94
|
+
page.
|
|
95
|
+
|
|
96
|
+
## Extending
|
|
97
|
+
|
|
98
|
+
### Adding a new map feature
|
|
99
|
+
|
|
100
|
+
Map features are a plugin system built on a few abstract base classes, exported from the
|
|
101
|
+
package root:
|
|
102
|
+
|
|
103
|
+
- `MapIntegration` — base apply/remove lifecycle
|
|
104
|
+
- `MapLayerIntegration` — adds MapTiler layer management
|
|
105
|
+
- `MapGeoJSONIntegration` — adds GeoJSON data-source management, plus a
|
|
106
|
+
`MapIconManager` for sprite icons (this is `src/lib/map/integrations/
|
|
107
|
+
map-geojson-integration.svelte.ts`'s `MapIconLayerIntegration`, exported under this
|
|
108
|
+
name)
|
|
109
|
+
|
|
110
|
+
Extend one of these rather than manipulating the MapTiler map instance directly. Look
|
|
111
|
+
at `src/lib/monitors/monitors-map-integration.svelte.ts` or `src/lib/wind/wind.svelte.ts`
|
|
112
|
+
as reference implementations.
|
|
113
|
+
|
|
114
|
+
### Reusing the monitors map display with different data (`MonitorsDataSource`)
|
|
115
|
+
|
|
116
|
+
`MonitorsMapIntegration` (and the icon-generation base class behind
|
|
117
|
+
`MonitorsIconManager`/`CollocationIconManager`) don't hardcode the live `monitorsManager`
|
|
118
|
+
singleton — they accept a `MonitorsDataSource` via their constructor:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
export interface MonitorsDataSource {
|
|
122
|
+
readonly meta: MonitorsMeta | null;
|
|
123
|
+
readonly pollutant: "pm25" | "o3" | null;
|
|
124
|
+
readonly latest: XMap<string, MonitorLatestType<"pm25" | "o3">> | null;
|
|
125
|
+
readonly levels: Array<SJVAirEntryLevel> | null;
|
|
126
|
+
}
|
|
44
127
|
```
|
|
45
128
|
|
|
46
|
-
|
|
129
|
+
`monitorsManager` (auto-polling, always-current readings) is the default when no data
|
|
130
|
+
source is given, so existing consumers are unaffected. A host app that wants the same
|
|
131
|
+
clustering/icon-rendering/filtering/tooltip logic driven by different data — for
|
|
132
|
+
example, `data-dashboard`'s Monitors tab, which colors monitors by the **average of
|
|
133
|
+
historical summary data over a selected date range** rather than a live reading —
|
|
134
|
+
implements its own `MonitorsDataSource` (no polling, populated from
|
|
135
|
+
`getMonitorSummariesDaily`/etc.) and constructs its own instance:
|
|
47
136
|
|
|
48
|
-
|
|
137
|
+
```ts
|
|
138
|
+
import { MonitorsMapIntegration, type MonitorsDataSource } from "@sjvair/monitor-map";
|
|
49
139
|
|
|
50
|
-
|
|
140
|
+
const myDataSource: MonitorsDataSource = /* ... */;
|
|
141
|
+
const monitorsMapIntegration = new MonitorsMapIntegration(myDataSource);
|
|
142
|
+
```
|
|
51
143
|
|
|
52
|
-
|
|
144
|
+
This reuses all of the existing map-rendering work instead of forking it — see
|
|
145
|
+
`CLAUDE.md`'s "Monitor Data Flow" section for more detail.
|
|
53
146
|
|
|
54
|
-
|
|
147
|
+
## Development
|
|
55
148
|
|
|
56
149
|
```bash
|
|
57
|
-
npm
|
|
150
|
+
npm run dev # Start development server
|
|
151
|
+
npm run build # Widget build (vite build, code-split ES modules)
|
|
152
|
+
npm run build:lib # Library build (svelte-package), what gets published to npm
|
|
153
|
+
npm run preview # Preview widget build
|
|
154
|
+
npm run check # Type-check Svelte components
|
|
155
|
+
npm run check:watch # Type-check in watch mode
|
|
156
|
+
npm run lint # Prettier check + ESLint
|
|
157
|
+
npm run format # Auto-format with Prettier
|
|
58
158
|
```
|
|
159
|
+
|
|
160
|
+
No test framework is currently configured.
|
|
161
|
+
|
|
162
|
+
## Publishing
|
|
163
|
+
|
|
164
|
+
Publishing to npm (`@sjvair/monitor-map`) happens via GitHub Actions
|
|
165
|
+
(`.github/workflows/release-package.yml`) whenever a GitHub Release is published (or the
|
|
166
|
+
workflow is triggered manually). Bump `version` in `package.json` before cutting the
|
|
167
|
+
release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import { onDestroy } from "svelte";
|
|
3
|
+
import { onDestroy, untrack } from "svelte";
|
|
4
4
|
import MapShell from "./map/MapShell.svelte";
|
|
5
5
|
import MonitorsDisplayOptions from "./monitors/components/MonitorsDisplayOptions.svelte";
|
|
6
6
|
import MapLayersDisplayOptions from "./components/MapLayersDisplayOptions.svelte";
|
|
@@ -54,10 +54,15 @@
|
|
|
54
54
|
// applies a later "?pollutant=" change (e.g. navigating in from elsewhere) to an already-running manager.
|
|
55
55
|
$effect(() => {
|
|
56
56
|
const urlPollutant = route.search.pollutant;
|
|
57
|
+
// Read monitorsManager.pollutant via untrack: this effect must only react to the URL
|
|
58
|
+
// changing (e.g. navigating in from elsewhere), not to monitorsManager.pollutant itself.
|
|
59
|
+
// Tracking it here would make a UI-driven pollutant change (e.g. the display-options
|
|
60
|
+
// toggle) re-trigger this effect before the manager->URL effect below can sync the URL,
|
|
61
|
+
// so this effect would see the still-stale URL and immediately revert the user's change.
|
|
57
62
|
if (
|
|
58
63
|
monitorsManager.initialized &&
|
|
59
64
|
(urlPollutant === "pm25" || urlPollutant === "o3") &&
|
|
60
|
-
monitorsManager.pollutant !== urlPollutant
|
|
65
|
+
untrack(() => monitorsManager.pollutant) !== urlPollutant
|
|
61
66
|
) {
|
|
62
67
|
monitorsManager.pollutant = urlPollutant;
|
|
63
68
|
}
|
|
@@ -17,7 +17,7 @@ function collocationTooltip(evt) {
|
|
|
17
17
|
class CollocationSitesMapIntegration extends MapIconLayerIntegration {
|
|
18
18
|
referenceId = "collocation-sites";
|
|
19
19
|
enabled = $state(false);
|
|
20
|
-
icons = new CollocationIconManager();
|
|
20
|
+
icons = new CollocationIconManager(monitorsManager);
|
|
21
21
|
tooltipManager = new TooltipManager();
|
|
22
22
|
features = $derived.by(() => {
|
|
23
23
|
if (!monitorsManager.meta ||
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export { hmsManager } from "./hms/hms.svelte";
|
|
|
36
36
|
export type { HMSManager } from "./hms/hms.svelte";
|
|
37
37
|
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
|
|
38
38
|
export type { MapManager } from "./map/map.svelte";
|
|
39
|
-
export { monitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
39
|
+
export { monitorsMapIntegration, MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
40
40
|
export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
|
|
41
41
|
export { evStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
|
|
42
42
|
export { windMapIntegration } from "./wind/wind.svelte";
|
|
@@ -58,12 +58,11 @@ export { MapIntegration } from "./map/integrations/map-integration.svelte";
|
|
|
58
58
|
export { MapLayerIntegration } from "./map/integrations/map-layer-integration.svelte";
|
|
59
59
|
export { MapIconLayerIntegration as MapGeoJSONIntegration } from "./map/integrations/map-geojson-integration.svelte";
|
|
60
60
|
export type { SomeMapIntegration } from "./map/integrations/types";
|
|
61
|
-
export type { MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
62
61
|
export type { WindMapIntegration } from "./wind/wind.svelte";
|
|
63
62
|
export type { EvStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
|
|
64
63
|
export type { HMSFireMapIntegration } from "./hms/hms-fire-map-integration.svelte";
|
|
65
64
|
export type { HMSSmokeMapIntegration } from "./hms/hms-smoke-map-integration.svelte";
|
|
66
65
|
export type { HMSFireGroup } from "./hms/hms.svelte";
|
|
67
|
-
export type { MonitorMapFeature, MonitorMarkerProperties, MonitorClusterMapFeature, MonitorClusterMarkerProperties } from "./monitors/types";
|
|
66
|
+
export type { MonitorMapFeature, MonitorMarkerProperties, MonitorClusterMapFeature, MonitorClusterMarkerProperties, MonitorsDataSource, MonitorsPollutant } from "./monitors/types";
|
|
68
67
|
export type { CollocationSiteMapFeature, CollocationSiteMarkerProperties } from "./collocation-sites/types";
|
|
69
68
|
export type { EvStation, EvStationMarkerProperties, EvStationMapFeature, EvStationClusterMapFeature } from "./ev-stations/types";
|
package/dist/lib/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export { evStationsManager } from "./ev-stations/ev-stations.svelte";
|
|
|
33
33
|
export { hmsManager } from "./hms/hms.svelte";
|
|
34
34
|
export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
|
|
35
35
|
// Map integrations
|
|
36
|
-
export { monitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
36
|
+
export { monitorsMapIntegration, MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
37
37
|
export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
|
|
38
38
|
export { evStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
|
|
39
39
|
export { windMapIntegration } from "./wind/wind.svelte";
|
|
@@ -70,8 +70,12 @@
|
|
|
70
70
|
const TRANSITION_MS = 300;
|
|
71
71
|
|
|
72
72
|
$effect(() => {
|
|
73
|
+
// mapManager.map is only ever assigned once its own initial "load" has
|
|
74
|
+
// already fired (see initializeMap in map.svelte.ts), so by the time this
|
|
75
|
+
// effect sees a non-null map, it's already safe to reveal — waiting for a
|
|
76
|
+
// further "idle" event here is unreliable since one may never fire again.
|
|
73
77
|
if (mapManager.map && ready) {
|
|
74
|
-
|
|
78
|
+
disableLoadScreen();
|
|
75
79
|
}
|
|
76
80
|
});
|
|
77
81
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { MonitorData, SJVAirEntryLevel } from "@sjvair/sdk";
|
|
2
2
|
import { MapIconManager } from "../map/integrations/map-icon-manager";
|
|
3
|
+
import type { MonitorsDataSource } from "./types";
|
|
3
4
|
export declare function getIconId<T extends MonitorData>(monitor: T, level: SJVAirEntryLevel): string;
|
|
4
5
|
export declare abstract class MonitorShapeIconManager extends MapIconManager {
|
|
6
|
+
private dataSource;
|
|
5
7
|
protected abstract get idPrefix(): string;
|
|
6
8
|
protected abstract transformSvg(svg: string): string;
|
|
7
9
|
protected get iconSize(): number;
|
|
8
|
-
constructor();
|
|
10
|
+
constructor(dataSource: Pick<MonitorsDataSource, "levels">);
|
|
9
11
|
}
|
|
10
12
|
export declare class MonitorsIconManager extends MonitorShapeIconManager {
|
|
11
13
|
protected get idPrefix(): string;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { asDataURI, circle, square, triangle } from "../map/icons";
|
|
2
2
|
import { MapIconManager } from "../map/integrations/map-icon-manager";
|
|
3
3
|
import { mapManager } from "../map/map.svelte";
|
|
4
|
-
import { monitorsManager } from "./monitors.svelte";
|
|
5
4
|
const MONITOR_ICONS = { circle, square, triangle };
|
|
6
5
|
const MONITOR_ICON_BORDER_WIDTH = 2;
|
|
7
6
|
const MONITOR_ICON_DEFAULT_COLOR = "#969696"; // light gray
|
|
@@ -26,16 +25,18 @@ export function getIconId(monitor, level) {
|
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
export class MonitorShapeIconManager extends MapIconManager {
|
|
28
|
+
dataSource;
|
|
29
29
|
get iconSize() {
|
|
30
30
|
return 24;
|
|
31
31
|
}
|
|
32
|
-
constructor() {
|
|
32
|
+
constructor(dataSource) {
|
|
33
33
|
super();
|
|
34
|
+
this.dataSource = dataSource;
|
|
34
35
|
$effect.root(() => {
|
|
35
36
|
$effect(() => {
|
|
36
|
-
if (
|
|
37
|
+
if (this.dataSource.levels) {
|
|
37
38
|
const iconLevels = [
|
|
38
|
-
...
|
|
39
|
+
...this.dataSource.levels,
|
|
39
40
|
{ name: "default", color: MONITOR_ICON_DEFAULT_COLOR },
|
|
40
41
|
{ name: "display", color: MONITOR_ICON_DISPLAY_COLOR }
|
|
41
42
|
];
|
|
@@ -4,8 +4,9 @@ import { MapIconLayerIntegration } from "../map/integrations/map-geojson-integra
|
|
|
4
4
|
import { MonitorsIconManager } from "./monitors-icon-manager.svelte";
|
|
5
5
|
import { TooltipManager } from "../map/integrations/tooltip.svelte";
|
|
6
6
|
import { MapDisplayOption } from "../map/integrations/map-display-option.svelte";
|
|
7
|
-
import type { MonitorMapFeature, MonitorMarkerProperties } from "./types";
|
|
7
|
+
import type { MonitorMapFeature, MonitorMarkerProperties, MonitorsDataSource } from "./types";
|
|
8
8
|
declare class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProperties> {
|
|
9
|
+
private dataSource;
|
|
9
10
|
referenceId: string;
|
|
10
11
|
enabled: boolean;
|
|
11
12
|
clustered: boolean;
|
|
@@ -33,10 +34,10 @@ declare class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMark
|
|
|
33
34
|
clusterIconThresholds: Array<SJVAirEntryLevel>;
|
|
34
35
|
get mapLayer(): Parameters<MaptilerMap["addLayer"]>[0];
|
|
35
36
|
get mapSource(): Parameters<MaptilerMap["addSource"]>[1];
|
|
36
|
-
constructor();
|
|
37
|
+
constructor(dataSource?: MonitorsDataSource);
|
|
37
38
|
apply(): void;
|
|
38
39
|
remove(): void;
|
|
39
40
|
private applySelectedState;
|
|
40
41
|
}
|
|
41
42
|
export declare const monitorsMapIntegration: MonitorsMapIntegration;
|
|
42
|
-
export
|
|
43
|
+
export { MonitorsMapIntegration };
|
|
@@ -20,10 +20,11 @@ const filters = {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
23
|
+
dataSource;
|
|
23
24
|
referenceId = "monitors";
|
|
24
25
|
enabled = $state(true);
|
|
25
26
|
clustered = $state(true);
|
|
26
|
-
icons
|
|
27
|
+
icons;
|
|
27
28
|
tooltipManager = new TooltipManager();
|
|
28
29
|
renderer = new MonitorsClusterRenderer(this);
|
|
29
30
|
displayOptions = $derived.by(() => ({
|
|
@@ -58,11 +59,11 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
58
59
|
});
|
|
59
60
|
};
|
|
60
61
|
features = $derived.by(() => {
|
|
61
|
-
if (!
|
|
62
|
+
if (!this.dataSource.meta || !this.dataSource.latest || !this.dataSource.pollutant) {
|
|
62
63
|
return [];
|
|
63
64
|
}
|
|
64
|
-
const levels =
|
|
65
|
-
return Array.from(
|
|
65
|
+
const levels = this.dataSource.meta.entryType(this.dataSource.pollutant).asIter.levels;
|
|
66
|
+
return Array.from(this.dataSource.latest.values().map((m) => {
|
|
66
67
|
const feature = {
|
|
67
68
|
type: "Feature",
|
|
68
69
|
properties: {
|
|
@@ -145,9 +146,9 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
145
146
|
return byType;
|
|
146
147
|
});
|
|
147
148
|
clusterIconThresholds = $derived.by(() => {
|
|
148
|
-
if (!
|
|
149
|
+
if (!this.dataSource.meta || !this.dataSource.pollutant)
|
|
149
150
|
return [];
|
|
150
|
-
return
|
|
151
|
+
return this.dataSource.meta.entryType(this.dataSource.pollutant).asIter.levels ?? [];
|
|
151
152
|
});
|
|
152
153
|
get mapLayer() {
|
|
153
154
|
return {
|
|
@@ -175,8 +176,10 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
175
176
|
}
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
|
-
constructor() {
|
|
179
|
+
constructor(dataSource = monitorsManager) {
|
|
179
180
|
super();
|
|
181
|
+
this.dataSource = dataSource;
|
|
182
|
+
this.icons = new MonitorsIconManager(dataSource);
|
|
180
183
|
$effect.root(() => {
|
|
181
184
|
// Push filter changes to the active layer(s) imperatively
|
|
182
185
|
$effect(() => {
|
|
@@ -284,3 +287,4 @@ class MonitorsMapIntegration extends MapIconLayerIntegration {
|
|
|
284
287
|
}
|
|
285
288
|
}
|
|
286
289
|
export const monitorsMapIntegration = new MonitorsMapIntegration();
|
|
290
|
+
export { MonitorsMapIntegration };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type MonitorData, type MonitorLatestType, type MonitorsMeta, type SJVAirEntryLevel } from "@sjvair/sdk";
|
|
2
2
|
import { XMap } from "@tstk/builtin-extensions";
|
|
3
3
|
import { Interval } from "@tstk/utils";
|
|
4
|
-
|
|
4
|
+
import type { MonitorsDataSource } from "./types";
|
|
5
|
+
declare class MonitorsManager implements MonitorsDataSource {
|
|
5
6
|
autoUpdate: Interval;
|
|
6
7
|
initialized: boolean;
|
|
7
8
|
latest: XMap<string, MonitorLatestType<"pm25" | "o3">> | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getMonitors,
|
|
1
|
+
import { getMonitors, getMonitorsList, getMonitorsMeta } from "@sjvair/sdk";
|
|
2
2
|
import { XMap } from "@tstk/builtin-extensions";
|
|
3
3
|
import { Interval } from "@tstk/utils";
|
|
4
4
|
class MonitorsManager {
|
|
@@ -12,7 +12,7 @@ class MonitorsManager {
|
|
|
12
12
|
async init(urlPollutant) {
|
|
13
13
|
if (this.initialized)
|
|
14
14
|
return;
|
|
15
|
-
[this.list, this.meta] = await Promise.all([
|
|
15
|
+
[this.list, this.meta] = await Promise.all([getMonitorsList(), getMonitorsMeta()]);
|
|
16
16
|
this.pollutant =
|
|
17
17
|
urlPollutant === "pm25" || urlPollutant === "o3" ? urlPollutant : this.meta.default_pollutant;
|
|
18
18
|
this.latest = await getMonitorsLatestMap(this.pollutant);
|
|
@@ -23,13 +23,13 @@ class MonitorsManager {
|
|
|
23
23
|
if (!this.initialized)
|
|
24
24
|
return;
|
|
25
25
|
[this.list, this.latest] = await Promise.all([
|
|
26
|
-
|
|
26
|
+
getMonitorsList(),
|
|
27
27
|
getMonitorsLatestMap(this.pollutant || this.meta?.default_pollutant || "pm25")
|
|
28
28
|
]);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
async function getMonitorsLatestMap(pollutant) {
|
|
32
|
-
const monitors = await
|
|
32
|
+
const monitors = await getMonitors(pollutant);
|
|
33
33
|
const latest = new XMap();
|
|
34
34
|
for (const monitor of monitors) {
|
|
35
35
|
latest.set(monitor.id, monitor);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Feature, Geometry } from "geojson";
|
|
2
|
-
import type { MonitorType } from "@sjvair/sdk";
|
|
2
|
+
import type { MonitorLatestType, MonitorsMeta, MonitorType, SJVAirEntryLevel } from "@sjvair/sdk";
|
|
3
3
|
import type { MapGeoJSONFeature } from "@maptiler/sdk";
|
|
4
|
+
import type { XMap } from "@tstk/builtin-extensions";
|
|
4
5
|
export type MonitorMapFeature = Feature<Geometry, MonitorMarkerProperties>;
|
|
5
6
|
export interface MonitorClusterMapFeature extends MapGeoJSONFeature {
|
|
6
7
|
properties: MonitorClusterMarkerProperties;
|
|
@@ -24,3 +25,18 @@ export interface MonitorClusterMarkerProperties {
|
|
|
24
25
|
sumValues: number;
|
|
25
26
|
countValues: number;
|
|
26
27
|
}
|
|
28
|
+
/** The pollutants monitorsMapIntegration/monitorsIconManager can render. */
|
|
29
|
+
export type MonitorsPollutant = "pm25" | "o3";
|
|
30
|
+
/**
|
|
31
|
+
* The reactive data a monitors map display needs: which monitors exist, their
|
|
32
|
+
* per-monitor "latest" value for the active pollutant, and the entry-level metadata
|
|
33
|
+
* used to color icons. `monitorsManager` (live, auto-polling) is the default
|
|
34
|
+
* implementation; a host app can supply its own (e.g. one backed by historical
|
|
35
|
+
* summaries instead of live readings) to reuse the map rendering with different data.
|
|
36
|
+
*/
|
|
37
|
+
export interface MonitorsDataSource {
|
|
38
|
+
readonly meta: MonitorsMeta | null;
|
|
39
|
+
readonly pollutant: MonitorsPollutant | null;
|
|
40
|
+
readonly latest: XMap<string, MonitorLatestType<MonitorsPollutant>> | null;
|
|
41
|
+
readonly levels: Array<SJVAirEntryLevel> | null;
|
|
42
|
+
}
|
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": "3.
|
|
4
|
+
"version": "3.4.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/SJVAir/monitor-map"
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"@lucide/svelte": "^1.28.0",
|
|
72
72
|
"@maptiler/sdk": "^4.1.0",
|
|
73
73
|
"@maptiler/weather": "^3.1.1",
|
|
74
|
-
"@sjvair/sdk": "npm:@jsr/sjvair__sdk@^
|
|
74
|
+
"@sjvair/sdk": "npm:@jsr/sjvair__sdk@^4.0.0",
|
|
75
75
|
"@tstk/builtin-extensions": "npm:@jsr/tstk__builtin-extensions@^1.0.8",
|
|
76
76
|
"@tstk/utils": "npm:@jsr/tstk__utils@^1.1.2",
|
|
77
77
|
"@turf/area": "^7.4.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import { onDestroy } from "svelte";
|
|
3
|
+
import { onDestroy, untrack } from "svelte";
|
|
4
4
|
import MapShell from "$lib/map/MapShell.svelte";
|
|
5
5
|
import MonitorsDisplayOptions from "$lib/monitors/components/MonitorsDisplayOptions.svelte";
|
|
6
6
|
import MapLayersDisplayOptions from "$lib/components/MapLayersDisplayOptions.svelte";
|
|
@@ -54,10 +54,15 @@
|
|
|
54
54
|
// applies a later "?pollutant=" change (e.g. navigating in from elsewhere) to an already-running manager.
|
|
55
55
|
$effect(() => {
|
|
56
56
|
const urlPollutant = route.search.pollutant;
|
|
57
|
+
// Read monitorsManager.pollutant via untrack: this effect must only react to the URL
|
|
58
|
+
// changing (e.g. navigating in from elsewhere), not to monitorsManager.pollutant itself.
|
|
59
|
+
// Tracking it here would make a UI-driven pollutant change (e.g. the display-options
|
|
60
|
+
// toggle) re-trigger this effect before the manager->URL effect below can sync the URL,
|
|
61
|
+
// so this effect would see the still-stale URL and immediately revert the user's change.
|
|
57
62
|
if (
|
|
58
63
|
monitorsManager.initialized &&
|
|
59
64
|
(urlPollutant === "pm25" || urlPollutant === "o3") &&
|
|
60
|
-
monitorsManager.pollutant !== urlPollutant
|
|
65
|
+
untrack(() => monitorsManager.pollutant) !== urlPollutant
|
|
61
66
|
) {
|
|
62
67
|
monitorsManager.pollutant = urlPollutant;
|
|
63
68
|
}
|
|
@@ -22,7 +22,7 @@ class CollocationSitesMapIntegration extends MapIconLayerIntegration<Collocation
|
|
|
22
22
|
referenceId: string = "collocation-sites";
|
|
23
23
|
enabled: boolean = $state(false);
|
|
24
24
|
|
|
25
|
-
icons: CollocationIconManager = new CollocationIconManager();
|
|
25
|
+
icons: CollocationIconManager = new CollocationIconManager(monitorsManager);
|
|
26
26
|
tooltipManager: TooltipManager = new TooltipManager();
|
|
27
27
|
|
|
28
28
|
features: Array<CollocationSiteMapFeature> = $derived.by(() => {
|
package/src/lib/index.ts
CHANGED
|
@@ -45,7 +45,10 @@ export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
|
|
|
45
45
|
export type { MapManager } from "./map/map.svelte";
|
|
46
46
|
|
|
47
47
|
// Map integrations
|
|
48
|
-
export {
|
|
48
|
+
export {
|
|
49
|
+
monitorsMapIntegration,
|
|
50
|
+
MonitorsMapIntegration
|
|
51
|
+
} from "./monitors/monitors-map-integration.svelte";
|
|
49
52
|
export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
|
|
50
53
|
export { evStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
|
|
51
54
|
export { windMapIntegration } from "./wind/wind.svelte";
|
|
@@ -78,7 +81,6 @@ export { MapIconLayerIntegration as MapGeoJSONIntegration } from "./map/integrat
|
|
|
78
81
|
|
|
79
82
|
// Types
|
|
80
83
|
export type { SomeMapIntegration } from "./map/integrations/types";
|
|
81
|
-
export type { MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
82
84
|
export type { WindMapIntegration } from "./wind/wind.svelte";
|
|
83
85
|
export type { EvStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
|
|
84
86
|
export type { HMSFireMapIntegration } from "./hms/hms-fire-map-integration.svelte";
|
|
@@ -88,7 +90,9 @@ export type {
|
|
|
88
90
|
MonitorMapFeature,
|
|
89
91
|
MonitorMarkerProperties,
|
|
90
92
|
MonitorClusterMapFeature,
|
|
91
|
-
MonitorClusterMarkerProperties
|
|
93
|
+
MonitorClusterMarkerProperties,
|
|
94
|
+
MonitorsDataSource,
|
|
95
|
+
MonitorsPollutant
|
|
92
96
|
} from "./monitors/types";
|
|
93
97
|
export type {
|
|
94
98
|
CollocationSiteMapFeature,
|
|
@@ -70,8 +70,12 @@
|
|
|
70
70
|
const TRANSITION_MS = 300;
|
|
71
71
|
|
|
72
72
|
$effect(() => {
|
|
73
|
+
// mapManager.map is only ever assigned once its own initial "load" has
|
|
74
|
+
// already fired (see initializeMap in map.svelte.ts), so by the time this
|
|
75
|
+
// effect sees a non-null map, it's already safe to reveal — waiting for a
|
|
76
|
+
// further "idle" event here is unreliable since one may never fire again.
|
|
73
77
|
if (mapManager.map && ready) {
|
|
74
|
-
|
|
78
|
+
disableLoadScreen();
|
|
75
79
|
}
|
|
76
80
|
});
|
|
77
81
|
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { mapManager } from "$lib/map/map.svelte";
|
|
5
|
-
import {
|
|
5
|
+
import type { MonitorsDataSource } from "./types";
|
|
6
6
|
|
|
7
7
|
const MONITOR_ICONS = { circle, square, triangle };
|
|
8
8
|
const MONITOR_ICON_BORDER_WIDTH = 2;
|
|
@@ -42,14 +42,14 @@ export abstract class MonitorShapeIconManager extends MapIconManager {
|
|
|
42
42
|
return 24;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
constructor() {
|
|
45
|
+
constructor(private dataSource: Pick<MonitorsDataSource, "levels">) {
|
|
46
46
|
super();
|
|
47
47
|
|
|
48
48
|
$effect.root(() => {
|
|
49
49
|
$effect(() => {
|
|
50
|
-
if (
|
|
50
|
+
if (this.dataSource.levels) {
|
|
51
51
|
const iconLevels = [
|
|
52
|
-
...
|
|
52
|
+
...this.dataSource.levels,
|
|
53
53
|
{ name: "default", color: MONITOR_ICON_DEFAULT_COLOR },
|
|
54
54
|
{ name: "display", color: MONITOR_ICON_DISPLAY_COLOR }
|
|
55
55
|
];
|
|
@@ -15,7 +15,7 @@ import { TooltipManager } from "$lib/map/integrations/tooltip.svelte";
|
|
|
15
15
|
import { MapDisplayOption } from "$lib/map/integrations/map-display-option.svelte";
|
|
16
16
|
import { getCurrentLevel, getOrder } from "./monitor-utils";
|
|
17
17
|
import { MonitorsClusterRenderer, monitorTooltip } from "./monitors-cluster-renderer";
|
|
18
|
-
import type { MonitorMapFeature, MonitorMarkerProperties } from "./types";
|
|
18
|
+
import type { MonitorMapFeature, MonitorMarkerProperties, MonitorsDataSource } from "./types";
|
|
19
19
|
|
|
20
20
|
const filters = {
|
|
21
21
|
monitor(deviceType: MonitorType): ExpressionSpecification {
|
|
@@ -34,7 +34,7 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
34
34
|
enabled: boolean = $state(true);
|
|
35
35
|
clustered: boolean = $state(true);
|
|
36
36
|
|
|
37
|
-
icons: MonitorsIconManager
|
|
37
|
+
icons: MonitorsIconManager;
|
|
38
38
|
tooltipManager: TooltipManager = new TooltipManager();
|
|
39
39
|
private renderer: MonitorsClusterRenderer = new MonitorsClusterRenderer(this);
|
|
40
40
|
|
|
@@ -72,13 +72,13 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
features: Array<MonitorMapFeature> = $derived.by(() => {
|
|
75
|
-
if (!
|
|
75
|
+
if (!this.dataSource.meta || !this.dataSource.latest || !this.dataSource.pollutant) {
|
|
76
76
|
return [];
|
|
77
77
|
}
|
|
78
|
-
const levels =
|
|
78
|
+
const levels = this.dataSource.meta.entryType(this.dataSource.pollutant).asIter.levels;
|
|
79
79
|
|
|
80
80
|
return Array.from(
|
|
81
|
-
|
|
81
|
+
this.dataSource.latest.values().map((m) => {
|
|
82
82
|
const feature: MonitorMapFeature = {
|
|
83
83
|
type: "Feature",
|
|
84
84
|
properties: {
|
|
@@ -163,8 +163,8 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
clusterIconThresholds: Array<SJVAirEntryLevel> = $derived.by(() => {
|
|
166
|
-
if (!
|
|
167
|
-
return
|
|
166
|
+
if (!this.dataSource.meta || !this.dataSource.pollutant) return [];
|
|
167
|
+
return this.dataSource.meta.entryType(this.dataSource.pollutant).asIter.levels ?? [];
|
|
168
168
|
});
|
|
169
169
|
|
|
170
170
|
get mapLayer(): Parameters<MaptilerMap["addLayer"]>[0] {
|
|
@@ -195,8 +195,9 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
constructor() {
|
|
198
|
+
constructor(private dataSource: MonitorsDataSource = monitorsManager) {
|
|
199
199
|
super();
|
|
200
|
+
this.icons = new MonitorsIconManager(dataSource);
|
|
200
201
|
|
|
201
202
|
$effect.root(() => {
|
|
202
203
|
// Push filter changes to the active layer(s) imperatively
|
|
@@ -304,4 +305,4 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
|
|
|
304
305
|
}
|
|
305
306
|
|
|
306
307
|
export const monitorsMapIntegration = new MonitorsMapIntegration();
|
|
307
|
-
export
|
|
308
|
+
export { MonitorsMapIntegration };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getMonitors,
|
|
3
|
-
|
|
3
|
+
getMonitorsList,
|
|
4
4
|
getMonitorsMeta,
|
|
5
5
|
type MonitorData,
|
|
6
6
|
type MonitorLatestType,
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
} from "@sjvair/sdk";
|
|
10
10
|
import { XMap } from "@tstk/builtin-extensions";
|
|
11
11
|
import { Interval } from "@tstk/utils";
|
|
12
|
+
import type { MonitorsDataSource } from "./types";
|
|
12
13
|
|
|
13
|
-
class MonitorsManager {
|
|
14
|
+
class MonitorsManager implements MonitorsDataSource {
|
|
14
15
|
autoUpdate: Interval = new Interval(async () => await this.update(), 2 * 60 * 1000);
|
|
15
16
|
initialized: boolean = $state(false);
|
|
16
17
|
|
|
@@ -28,7 +29,7 @@ class MonitorsManager {
|
|
|
28
29
|
async init(urlPollutant?: string | number | boolean): Promise<void> {
|
|
29
30
|
if (this.initialized) return;
|
|
30
31
|
|
|
31
|
-
[this.list, this.meta] = await Promise.all([
|
|
32
|
+
[this.list, this.meta] = await Promise.all([getMonitorsList(), getMonitorsMeta()]);
|
|
32
33
|
|
|
33
34
|
this.pollutant =
|
|
34
35
|
urlPollutant === "pm25" || urlPollutant === "o3" ? urlPollutant : this.meta.default_pollutant;
|
|
@@ -41,7 +42,7 @@ class MonitorsManager {
|
|
|
41
42
|
if (!this.initialized) return;
|
|
42
43
|
|
|
43
44
|
[this.list, this.latest] = await Promise.all([
|
|
44
|
-
|
|
45
|
+
getMonitorsList(),
|
|
45
46
|
getMonitorsLatestMap(this.pollutant || this.meta?.default_pollutant || "pm25")
|
|
46
47
|
]);
|
|
47
48
|
}
|
|
@@ -50,7 +51,7 @@ class MonitorsManager {
|
|
|
50
51
|
async function getMonitorsLatestMap(
|
|
51
52
|
pollutant: "pm25" | "o3"
|
|
52
53
|
): Promise<XMap<string, MonitorLatestType<"pm25" | "o3">>> {
|
|
53
|
-
const monitors = await
|
|
54
|
+
const monitors = await getMonitors(pollutant);
|
|
54
55
|
const latest = new XMap<string, MonitorLatestType<"pm25" | "o3">>();
|
|
55
56
|
|
|
56
57
|
for (const monitor of monitors) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Feature, Geometry } from "geojson";
|
|
2
|
-
import type { MonitorType } from "@sjvair/sdk";
|
|
2
|
+
import type { MonitorLatestType, MonitorsMeta, MonitorType, SJVAirEntryLevel } from "@sjvair/sdk";
|
|
3
3
|
import type { MapGeoJSONFeature } from "@maptiler/sdk";
|
|
4
|
+
import type { XMap } from "@tstk/builtin-extensions";
|
|
4
5
|
|
|
5
6
|
export type MonitorMapFeature = Feature<Geometry, MonitorMarkerProperties>;
|
|
6
7
|
|
|
@@ -28,3 +29,20 @@ export interface MonitorClusterMarkerProperties {
|
|
|
28
29
|
sumValues: number;
|
|
29
30
|
countValues: number;
|
|
30
31
|
}
|
|
32
|
+
|
|
33
|
+
/** The pollutants monitorsMapIntegration/monitorsIconManager can render. */
|
|
34
|
+
export type MonitorsPollutant = "pm25" | "o3";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The reactive data a monitors map display needs: which monitors exist, their
|
|
38
|
+
* per-monitor "latest" value for the active pollutant, and the entry-level metadata
|
|
39
|
+
* used to color icons. `monitorsManager` (live, auto-polling) is the default
|
|
40
|
+
* implementation; a host app can supply its own (e.g. one backed by historical
|
|
41
|
+
* summaries instead of live readings) to reuse the map rendering with different data.
|
|
42
|
+
*/
|
|
43
|
+
export interface MonitorsDataSource {
|
|
44
|
+
readonly meta: MonitorsMeta | null;
|
|
45
|
+
readonly pollutant: MonitorsPollutant | null;
|
|
46
|
+
readonly latest: XMap<string, MonitorLatestType<MonitorsPollutant>> | null;
|
|
47
|
+
readonly levels: Array<SJVAirEntryLevel> | null;
|
|
48
|
+
}
|