@sjvair/monitor-map 3.3.1 → 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 CHANGED
@@ -1,58 +1,167 @@
1
- # create-svelte
1
+ # @sjvair/monitor-map
2
2
 
3
- Everything you need to build a Svelte library, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
3
+ SJVAir's interactive air quality monitor map, built with Svelte 5, Vite, and MapTiler
4
+ SDK. It ships two ways:
4
5
 
5
- Read more about creating a library [in the docs](https://svelte.dev/docs/kit/packaging).
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
- ## Creating a project
8
-
9
- If you're seeing this, you've probably already done this step. Congrats!
13
+ ## Installation
10
14
 
11
15
  ```bash
12
- # create a new project in the current directory
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
- ## Developing
19
+ ## Usage
20
20
 
21
- Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
21
+ ### Full-page map, via `sv-router`
22
22
 
23
- ```bash
24
- npm run dev
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
- # or start the server and open the app in a new browser tab
27
- npm run dev -- --open
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
- Everything inside `src/lib` is part of your library, everything inside `src/routes` can be used as a showcase or preview app.
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
- ## Building
44
+ provideMonitorMapRouter({ route, navigate, p, isActive, basePath: "/my-map" });
45
+ </script>
33
46
 
34
- To build your library:
47
+ <Router />
48
+ ```
35
49
 
36
- ```bash
37
- npm run package
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
- To create a production version of your showcase app:
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
- ```bash
43
- npm run build
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
- You can preview the production build with `npm run preview`.
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
- > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
137
+ ```ts
138
+ import { MonitorsMapIntegration, type MonitorsDataSource } from "@sjvair/monitor-map";
49
139
 
50
- ## Publishing
140
+ const myDataSource: MonitorsDataSource = /* ... */;
141
+ const monitorsMapIntegration = new MonitorsMapIntegration(myDataSource);
142
+ ```
51
143
 
52
- Go into the `package.json` and give your package the desired name through the `"name"` option. Also consider adding a `"license"` field and point it to a `LICENSE` file which you can create from a template (one popular option is the [MIT license](https://opensource.org/license/mit/)).
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
- To publish your library to [npm](https://www.npmjs.com):
147
+ ## Development
55
148
 
56
149
  ```bash
57
- npm publish
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.
@@ -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 ||
@@ -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";
@@ -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 (monitorsManager.levels) {
37
+ if (this.dataSource.levels) {
37
38
  const iconLevels = [
38
- ...monitorsManager.levels,
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 type { MonitorsMapIntegration };
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 = new MonitorsIconManager();
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 (!monitorsManager.meta || !monitorsManager.latest || !monitorsManager.pollutant) {
62
+ if (!this.dataSource.meta || !this.dataSource.latest || !this.dataSource.pollutant) {
62
63
  return [];
63
64
  }
64
- const levels = monitorsManager.meta.entryType(monitorsManager.pollutant).asIter.levels;
65
- return Array.from(monitorsManager.latest.values().map((m) => {
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 (!monitorsManager.meta || !monitorsManager.pollutant)
149
+ if (!this.dataSource.meta || !this.dataSource.pollutant)
149
150
  return [];
150
- return monitorsManager.meta.entryType(monitorsManager.pollutant).asIter.levels ?? [];
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
- declare class MonitorsManager {
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, getMonitorsLatest, getMonitorsMeta } from "@sjvair/sdk";
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([getMonitors(), getMonitorsMeta()]);
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
- getMonitors(),
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 getMonitorsLatest(pollutant);
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.3.1",
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@^3.2.0",
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",
@@ -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 { monitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
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,
@@ -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 { monitorsManager } from "./monitors.svelte";
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 (monitorsManager.levels) {
50
+ if (this.dataSource.levels) {
51
51
  const iconLevels = [
52
- ...monitorsManager.levels,
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 = new 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 (!monitorsManager.meta || !monitorsManager.latest || !monitorsManager.pollutant) {
75
+ if (!this.dataSource.meta || !this.dataSource.latest || !this.dataSource.pollutant) {
76
76
  return [];
77
77
  }
78
- const levels = monitorsManager.meta.entryType(monitorsManager.pollutant).asIter.levels;
78
+ const levels = this.dataSource.meta.entryType(this.dataSource.pollutant).asIter.levels;
79
79
 
80
80
  return Array.from(
81
- monitorsManager.latest.values().map((m) => {
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 (!monitorsManager.meta || !monitorsManager.pollutant) return [];
167
- return monitorsManager.meta.entryType(monitorsManager.pollutant).asIter.levels ?? [];
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 type { MonitorsMapIntegration };
308
+ export { MonitorsMapIntegration };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getMonitors,
3
- getMonitorsLatest,
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([getMonitors(), getMonitorsMeta()]);
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
- getMonitors(),
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 getMonitorsLatest(pollutant);
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
+ }