@sjvair/monitor-map 4.0.3 → 4.2.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 +94 -14
- package/dist/lib/LazyMapLayout.svelte +53 -0
- package/dist/lib/LazyMapLayout.svelte.d.ts +7 -0
- package/dist/lib/LazyMonitorDetailPanel.svelte +33 -0
- package/dist/lib/LazyMonitorDetailPanel.svelte.d.ts +4 -0
- package/dist/lib/LoadError.svelte +19 -0
- package/dist/lib/LoadError.svelte.d.ts +7 -0
- package/dist/lib/LoadScreen.svelte +10 -1
- package/dist/lib/LoadScreen.svelte.d.ts +6 -16
- package/dist/lib/MonitorDetailPanel.svelte +108 -28
- package/dist/lib/MonitorMapLayout.svelte +21 -1
- 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/data-chart/DataChart.svelte +63 -13
- package/dist/lib/data-chart/DataChart.svelte.d.ts +10 -5
- package/dist/lib/data-chart/DateRange.d.ts +4 -0
- package/dist/lib/data-chart/DateRange.js +9 -1
- package/dist/lib/data-chart/chart-utils.d.ts +2 -1
- package/dist/lib/data-chart/chart-utils.js +19 -23
- package/dist/lib/data-chart/level-colors.d.ts +6 -0
- package/dist/lib/data-chart/level-colors.js +5 -0
- package/dist/lib/data-chart/requests.d.ts +5 -2
- package/dist/lib/data-chart/requests.js +2 -3
- package/dist/lib/data-chart/tooltip.d.ts +2 -1
- package/dist/lib/data-chart/tooltip.js +1 -9
- package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +1 -1
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +5 -7
- 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 +8 -3
- package/dist/lib/index.js +6 -1
- package/dist/lib/lazy-chunks.d.ts +2 -0
- package/dist/lib/lazy-chunks.js +6 -0
- package/dist/lib/lazy-routes.d.ts +12 -0
- package/dist/lib/lazy-routes.js +18 -0
- package/dist/lib/map/Map.svelte +1 -1
- package/dist/lib/map/MapShell.svelte +9 -3
- package/dist/lib/map/MapShell.svelte.d.ts +5 -0
- package/dist/lib/map/integrations/click-manager.js +1 -1
- package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +13 -0
- package/dist/lib/map/integrations/map-geojson-integration.svelte.js +31 -5
- 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.d.ts +10 -0
- package/dist/lib/map/map-style-state.svelte.js +57 -6
- package/dist/lib/monitors/components/MonitorClusterTooltip.svelte +1 -1
- package/dist/lib/monitors/monitor-details.svelte.d.ts +15 -0
- package/dist/lib/monitors/monitor-details.svelte.js +63 -0
- 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 +5 -7
- package/dist/lib/monitors/monitors.svelte.d.ts +22 -1
- package/dist/lib/monitors/monitors.svelte.js +75 -8
- package/dist/lib/monitors/panel-pollutant.d.ts +37 -0
- package/dist/lib/monitors/panel-pollutant.js +43 -0
- package/dist/lib/options/OptionsBar.svelte +8 -1
- package/dist/lib/options/OptionsMenu.svelte +10 -1
- package/dist/lib/search/Search.svelte +1 -1
- package/dist/lib/shared-load.d.ts +5 -0
- package/dist/lib/shared-load.js +14 -0
- package/package.json +13 -2
- package/src/lib/LazyMapLayout.svelte +53 -0
- package/src/lib/LazyMonitorDetailPanel.svelte +33 -0
- package/src/lib/LoadError.svelte +19 -0
- package/src/lib/LoadScreen.svelte +10 -1
- package/src/lib/MonitorDetailPanel.svelte +108 -28
- package/src/lib/MonitorMapLayout.svelte +21 -1
- package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +1 -1
- package/src/lib/core.ts +17 -0
- package/src/lib/data-chart/DataChart.svelte +63 -13
- package/src/lib/data-chart/DateRange.ts +11 -1
- package/src/lib/data-chart/chart-utils.ts +23 -26
- package/src/lib/data-chart/level-colors.ts +13 -0
- package/src/lib/data-chart/requests.ts +9 -5
- package/src/lib/data-chart/tooltip.ts +2 -10
- package/src/lib/ev-stations/ev-stations-cluster-renderer.ts +1 -1
- package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +5 -5
- 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 +8 -3
- package/src/lib/lazy-chunks.ts +7 -0
- package/src/lib/lazy-routes.ts +20 -0
- package/src/lib/map/Map.svelte +1 -1
- package/src/lib/map/MapShell.svelte +9 -3
- package/src/lib/map/integrations/click-manager.ts +1 -1
- package/src/lib/map/integrations/map-geojson-integration.svelte.ts +33 -4
- 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 +59 -7
- package/src/lib/monitors/components/MonitorClusterTooltip.svelte +1 -1
- package/src/lib/monitors/monitor-details.svelte.ts +66 -0
- 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 +5 -5
- package/src/lib/monitors/monitors.svelte.ts +93 -10
- package/src/lib/monitors/panel-pollutant.ts +76 -0
- package/src/lib/options/OptionsBar.svelte +8 -1
- package/src/lib/options/OptionsMenu.svelte +10 -1
- package/src/lib/search/Search.svelte +1 -1
- package/src/lib/shared-load.ts +15 -0
- /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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { LevelColors } from "./level-colors";
|
|
2
2
|
import { tooltipsPlugin, uPlotCursorConfig } from "./tooltip";
|
|
3
3
|
import { fromUnixTime, getUnixTime, isToday, isAfter } from "date-fns";
|
|
4
4
|
import type uPlot from "uplot";
|
|
@@ -6,15 +6,6 @@ import type { MonitorType } from "@sjvair/sdk";
|
|
|
6
6
|
|
|
7
7
|
const PM25_AVG_60_STROKE = "3572b0";
|
|
8
8
|
|
|
9
|
-
function getColors(): Array<[number, string]> {
|
|
10
|
-
const levels = monitorsManager.levels ?? [];
|
|
11
|
-
const colors = levels.map((level) => [level.range[0], level.color.replace(/^#/, "")]) as Array<
|
|
12
|
-
[number, string]
|
|
13
|
-
>;
|
|
14
|
-
colors.push([Infinity, "000000"]);
|
|
15
|
-
return colors;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
const baseXSeriesConfig = {};
|
|
19
10
|
|
|
20
11
|
const baseYSeriesConfig = {
|
|
@@ -23,26 +14,33 @@ const baseYSeriesConfig = {
|
|
|
23
14
|
width: 1
|
|
24
15
|
};
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
/** The level stops plus a final black stop, so values above the top level still get a color. */
|
|
18
|
+
function gradientStops(colors: LevelColors): LevelColors {
|
|
19
|
+
return [...colors, [Infinity, "000000"]];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pm25SeriesConfig = (stops: LevelColors) => ({
|
|
27
23
|
...baseYSeriesConfig,
|
|
28
24
|
label: "Real Time",
|
|
29
25
|
width: 2,
|
|
30
|
-
stroke: (u: uPlot) => scaleGradient(u, "y", 1,
|
|
31
|
-
};
|
|
26
|
+
stroke: (u: uPlot) => scaleGradient(u, "y", 1, stops, true)
|
|
27
|
+
});
|
|
32
28
|
|
|
33
|
-
const pm25Avg60SeriesConfig = {
|
|
29
|
+
const pm25Avg60SeriesConfig = (stops: LevelColors) => ({
|
|
34
30
|
...baseYSeriesConfig,
|
|
35
31
|
label: "60 Minute Average",
|
|
36
|
-
stroke: (u: uPlot) => autograd(u, PM25_AVG_60_STROKE)
|
|
37
|
-
};
|
|
32
|
+
stroke: (u: uPlot) => autograd(u, PM25_AVG_60_STROKE, stops)
|
|
33
|
+
});
|
|
38
34
|
|
|
39
35
|
export function getChartConfig(
|
|
40
36
|
sourceName: MonitorType,
|
|
41
37
|
maxDiff: number,
|
|
42
38
|
width: number,
|
|
43
|
-
height: number
|
|
39
|
+
height: number,
|
|
40
|
+
colors: LevelColors,
|
|
41
|
+
label: string
|
|
44
42
|
): uPlot.Options {
|
|
45
|
-
const series: Array<uPlot.Series> = getSeriesConfigs(sourceName);
|
|
43
|
+
const series: Array<uPlot.Series> = getSeriesConfigs(sourceName, gradientStops(colors));
|
|
46
44
|
const getStroke = (u: uPlot, sIdx: number): string => {
|
|
47
45
|
const stroke = series[sIdx].stroke! as (u: uPlot, sIdx: number) => string | CanvasGradient;
|
|
48
46
|
return stroke(u, sIdx) as string;
|
|
@@ -51,7 +49,7 @@ export function getChartConfig(
|
|
|
51
49
|
return {
|
|
52
50
|
width,
|
|
53
51
|
height,
|
|
54
|
-
cursor: uPlotCursorConfig.get(),
|
|
52
|
+
cursor: uPlotCursorConfig.get(colors),
|
|
55
53
|
// @ts-expect-error: select missing pseudo "required" options
|
|
56
54
|
select: {
|
|
57
55
|
show: false
|
|
@@ -94,7 +92,7 @@ export function getChartConfig(
|
|
|
94
92
|
}
|
|
95
93
|
},
|
|
96
94
|
{
|
|
97
|
-
label
|
|
95
|
+
label,
|
|
98
96
|
side: 3,
|
|
99
97
|
values: (_: uPlot, ticks: number[]) =>
|
|
100
98
|
ticks.map((v: number) => v.toFixed(maxDiff < 5 ? 1 : 0))
|
|
@@ -106,9 +104,8 @@ export function getChartConfig(
|
|
|
106
104
|
};
|
|
107
105
|
}
|
|
108
106
|
|
|
109
|
-
function getSeriesConfigs(monitorType: MonitorType) {
|
|
110
|
-
const singleSeriesConfig =
|
|
111
|
-
singleSeriesConfig.width = 2;
|
|
107
|
+
function getSeriesConfigs(monitorType: MonitorType, stops: LevelColors) {
|
|
108
|
+
const singleSeriesConfig = { ...pm25Avg60SeriesConfig(stops), width: 2 };
|
|
112
109
|
|
|
113
110
|
switch (monitorType) {
|
|
114
111
|
case "airnow":
|
|
@@ -120,15 +117,15 @@ function getSeriesConfigs(monitorType: MonitorType) {
|
|
|
120
117
|
|
|
121
118
|
case "airgradient":
|
|
122
119
|
case "purpleair":
|
|
123
|
-
return [baseXSeriesConfig, pm25SeriesConfig];
|
|
120
|
+
return [baseXSeriesConfig, pm25SeriesConfig(stops)];
|
|
124
121
|
}
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
function autograd(u: uPlot, fallback: string) {
|
|
124
|
+
function autograd(u: uPlot, fallback: string, stops: LevelColors) {
|
|
128
125
|
if (u.series.length > 2) {
|
|
129
126
|
return fallback;
|
|
130
127
|
}
|
|
131
|
-
return scaleGradient(u, "y", 1,
|
|
128
|
+
return scaleGradient(u, "y", 1, stops, true);
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
function scaleGradient(
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Color stops for a chart: each level's lower bound and its hex color without the `#`. */
|
|
2
|
+
export type LevelColors = Array<[number, string]>;
|
|
3
|
+
|
|
4
|
+
// `range` is loose on purpose: the SDK types it as a tuple with an open tail, and only its
|
|
5
|
+
// first member (the level's lower bound) is read.
|
|
6
|
+
export function levelColors(
|
|
7
|
+
levels: ReadonlyArray<{
|
|
8
|
+
range: readonly [number, ...unknown[]] | readonly number[];
|
|
9
|
+
color: string;
|
|
10
|
+
}>
|
|
11
|
+
): LevelColors {
|
|
12
|
+
return levels.map((level) => [level.range[0], level.color.replace(/^#/, "")]);
|
|
13
|
+
}
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { getMonitorEntries, type
|
|
1
|
+
import { getMonitorEntries, type MonitorDetails, type MonitorEntry } from "@sjvair/sdk";
|
|
2
2
|
import { getUnixTime, parseISO, differenceInMinutes, addMinutes } from "date-fns";
|
|
3
|
-
import {
|
|
3
|
+
import type { PanelPollutant } from "../monitors/panel-pollutant";
|
|
4
4
|
import type { DateRange } from "./DateRange";
|
|
5
5
|
|
|
6
|
+
/** The monitor fields the chart reads. */
|
|
7
|
+
export type ChartMonitor = Pick<MonitorDetails, "id" | "name" | "type" | "data_source">;
|
|
8
|
+
|
|
6
9
|
export async function fetchChartData(
|
|
7
|
-
m:
|
|
8
|
-
d: DateRange
|
|
10
|
+
m: ChartMonitor,
|
|
11
|
+
d: DateRange,
|
|
12
|
+
entryType: PanelPollutant
|
|
9
13
|
): Promise<uPlot.AlignedData> {
|
|
10
14
|
return await getMonitorEntries({
|
|
11
|
-
entryType
|
|
15
|
+
entryType,
|
|
12
16
|
monitorId: m.id,
|
|
13
17
|
timestampGte: d.start,
|
|
14
18
|
timestampLte: d.end
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import { format, fromUnixTime } from "date-fns";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
function getColors(): Array<[number, string]> {
|
|
5
|
-
return (monitorsManager.levels ?? []).map((level) => [
|
|
6
|
-
level.range[0],
|
|
7
|
-
level.color.replace(/^#/, "")
|
|
8
|
-
]) as Array<[number, string]>;
|
|
9
|
-
}
|
|
2
|
+
import type { LevelColors } from "./level-colors";
|
|
10
3
|
|
|
11
4
|
let cursLeft = -100;
|
|
12
5
|
let cursTop = -100;
|
|
@@ -16,7 +9,7 @@ export const uPlotCursorConfig = {
|
|
|
16
9
|
cursLeft = left;
|
|
17
10
|
cursTop = top;
|
|
18
11
|
},
|
|
19
|
-
get: (): uPlot.Cursor => ({
|
|
12
|
+
get: (colors: LevelColors): uPlot.Cursor => ({
|
|
20
13
|
left: cursLeft,
|
|
21
14
|
top: cursTop,
|
|
22
15
|
y: false,
|
|
@@ -24,7 +17,6 @@ export const uPlotCursorConfig = {
|
|
|
24
17
|
size: 7,
|
|
25
18
|
fill: (u: uPlot, sIdx: number) => {
|
|
26
19
|
const xIdx = u.cursor.idxs![sIdx];
|
|
27
|
-
const colors = getColors();
|
|
28
20
|
|
|
29
21
|
if (xIdx != null && u.series.length >= 2 && sIdx === 1) {
|
|
30
22
|
const val = u.data[sIdx][xIdx]!;
|
|
@@ -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";
|
|
@@ -188,8 +188,9 @@ class EvStationsMapIntegration
|
|
|
188
188
|
clickManager.unregister([this.referenceId]);
|
|
189
189
|
this.tooltipManager.disable();
|
|
190
190
|
this.renderer.remove();
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
// Clustering may be toggled again before the icons arrive; only the latest apply runs.
|
|
192
|
+
this.supersedePending();
|
|
193
|
+
this.whenIconsLoad(() => {
|
|
193
194
|
this.renderer.apply(this.handleStationClick);
|
|
194
195
|
this.tooltipManager.enable();
|
|
195
196
|
});
|
|
@@ -198,8 +199,7 @@ class EvStationsMapIntegration
|
|
|
198
199
|
this.tooltipManager.disable();
|
|
199
200
|
clickManager.unregister([this.referenceId]);
|
|
200
201
|
super.apply();
|
|
201
|
-
this.
|
|
202
|
-
if (mapManager.styleLoading) return;
|
|
202
|
+
this.whenIconsLoad(() => {
|
|
203
203
|
clickManager.register([this.referenceId], this.handleStationClick);
|
|
204
204
|
this.tooltipManager.enable();
|
|
205
205
|
});
|
|
@@ -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 {
|
|
@@ -34,15 +39,15 @@ export {
|
|
|
34
39
|
|
|
35
40
|
// State managers
|
|
36
41
|
export { monitorsManager } from "./monitors/monitors.svelte";
|
|
37
|
-
export type { MonitorsManager } from "./monitors/monitors.svelte";
|
|
42
|
+
export type { MonitorsManager, MonitorsInitOptions } from "./monitors/monitors.svelte";
|
|
38
43
|
export { collocationSitesManager } from "./collocation-sites/collocations.svelte";
|
|
39
44
|
export type { CollocationSitesManager } from "./collocation-sites/collocations.svelte";
|
|
40
45
|
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,7 @@
|
|
|
1
|
+
import { sharedLoad } from "./shared-load";
|
|
2
|
+
|
|
3
|
+
// The heavy chunks behind the lazy routes entry (`./lazy-routes.ts`). They are imported here,
|
|
4
|
+
// not in the wrapper components that await them: sv-router treats any function whose source
|
|
5
|
+
// contains `() => import(...)` as a lazy route, and a compiled component is such a function.
|
|
6
|
+
export const loadMonitorMapLayout = sharedLoad(() => import("./MonitorMapLayout.svelte"));
|
|
7
|
+
export const loadMonitorDetailPanel = sharedLoad(() => import("./MonitorDetailPanel.svelte"));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Routes } from "sv-router";
|
|
2
|
+
import LazyMapLayout from "./LazyMapLayout.svelte";
|
|
3
|
+
import LazyMonitorDetailPanel from "./LazyMonitorDetailPanel.svelte";
|
|
4
|
+
import MainMapView from "./MainMapView.svelte";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lazy routes entry (`@sjvair/monitor-map/routes`): the same route tree as the main entry's
|
|
8
|
+
* `monitorMapRoutes`, but the map layout and the monitor panel load on demand, so a host
|
|
9
|
+
* router can reference it at startup without loading the map.
|
|
10
|
+
*
|
|
11
|
+
* The components here are light wrappers that load the real ones themselves
|
|
12
|
+
* (`./lazy-chunks.ts`) and show the load screen meanwhile. They are deliberately not
|
|
13
|
+
* `() => import(...)` routes: sv-router renders nothing for a route until every lazy component
|
|
14
|
+
* on it has loaded, which left the outlet empty on a cold link into the map.
|
|
15
|
+
*/
|
|
16
|
+
export const monitorMapRoutes: Routes = {
|
|
17
|
+
layout: LazyMapLayout,
|
|
18
|
+
"/": MainMapView,
|
|
19
|
+
"/monitor/:id": LazyMonitorDetailPanel
|
|
20
|
+
};
|
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
|
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* is true.
|
|
12
12
|
*/
|
|
13
13
|
ready: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Rendered on the load screen, under the logo, for as long as the load screen is up.
|
|
16
|
+
* Render it only when the caller's initial load failed (e.g. a message and a retry).
|
|
17
|
+
*/
|
|
18
|
+
loadError?: Snippet;
|
|
14
19
|
/** True when the routed detail panel (`children`) should be visible */
|
|
15
20
|
panelOpen: boolean;
|
|
16
21
|
/**
|
|
@@ -53,12 +58,13 @@
|
|
|
53
58
|
import LoadScreen, { disable as disableLoadScreen } from "$lib/LoadScreen.svelte";
|
|
54
59
|
import Map from "$lib/map/Map.svelte";
|
|
55
60
|
import OptionsBar from "$lib/options/OptionsBar.svelte";
|
|
56
|
-
import { mapManager } from "$lib/map/map.svelte";
|
|
61
|
+
import { mapManager } from "$lib/map/map-manager.svelte.js";
|
|
57
62
|
import { useMonitorMapRouter } from "$lib/router-context";
|
|
58
63
|
|
|
59
64
|
let {
|
|
60
65
|
integrations,
|
|
61
66
|
ready,
|
|
67
|
+
loadError,
|
|
62
68
|
panelOpen,
|
|
63
69
|
knownRoutes = [],
|
|
64
70
|
menu,
|
|
@@ -74,7 +80,7 @@
|
|
|
74
80
|
|
|
75
81
|
$effect(() => {
|
|
76
82
|
// 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
|
|
83
|
+
// already fired (see initializeMap in map-manager.svelte.ts), so by the time this
|
|
78
84
|
// effect sees a non-null map, it's already safe to reveal — waiting for a
|
|
79
85
|
// further "idle" event here is unreliable since one may never fire again.
|
|
80
86
|
if (mapManager.map && ready) {
|
|
@@ -123,7 +129,7 @@
|
|
|
123
129
|
</script>
|
|
124
130
|
|
|
125
131
|
<div class="relative flex h-full w-full flex-col md:flex-row">
|
|
126
|
-
<LoadScreen />
|
|
132
|
+
<LoadScreen error={loadError} />
|
|
127
133
|
<div class="relative flex-1 overflow-hidden">
|
|
128
134
|
<Map {integrations} />
|
|
129
135
|
{@render overlays?.()}
|
|
@@ -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>
|
|
@@ -33,16 +33,45 @@ export abstract class MapIconLayerIntegration<
|
|
|
33
33
|
T = Record<string, unknown>
|
|
34
34
|
> extends MapGeoJSONIntegration<T> {
|
|
35
35
|
abstract icons: MapIconManager;
|
|
36
|
+
private generation = 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Drops every callback still waiting on icons. `apply()` and `remove()` call it, so a
|
|
40
|
+
* subclass overriding `apply()` without calling `super.apply()` first must call it itself.
|
|
41
|
+
*/
|
|
42
|
+
protected supersedePending(): void {
|
|
43
|
+
this.generation++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Runs `run` once the icons have loaded, unless an `apply()` or `remove()` has happened
|
|
48
|
+
* since (the state it was scheduled for is gone) or a basemap change is in flight (the
|
|
49
|
+
* refresh on "style.load" re-applies).
|
|
50
|
+
*/
|
|
51
|
+
protected whenIconsLoad(run: () => void): void {
|
|
52
|
+
const generation = this.generation;
|
|
53
|
+
this.icons
|
|
54
|
+
.loadIcons()
|
|
55
|
+
.then(() => {
|
|
56
|
+
if (generation !== this.generation || mapManager.styleLoading) return;
|
|
57
|
+
run();
|
|
58
|
+
})
|
|
59
|
+
.catch(console.error);
|
|
60
|
+
}
|
|
36
61
|
|
|
37
62
|
apply() {
|
|
38
63
|
if (!mapManager.map) return;
|
|
39
64
|
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
if (mapManager.styleLoading) return;
|
|
65
|
+
this.supersedePending();
|
|
66
|
+
this.whenIconsLoad(() => {
|
|
43
67
|
this.removeLayerAndSource();
|
|
44
68
|
mapManager.map?.addSource(this.referenceId, this.mapSource);
|
|
45
69
|
super.apply();
|
|
46
70
|
});
|
|
47
71
|
}
|
|
72
|
+
|
|
73
|
+
remove() {
|
|
74
|
+
this.supersedePending();
|
|
75
|
+
super.remove();
|
|
76
|
+
}
|
|
48
77
|
}
|
|
@@ -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
|
|
|
@@ -7,7 +7,12 @@ import { variantForStyle } from "./basemap";
|
|
|
7
7
|
class MapStyleState {
|
|
8
8
|
referenceStyle: ReferenceMapStyle = $state.raw(DefaultMapStyle.getReferenceStyle());
|
|
9
9
|
variant: MapStyleVariant = $state.raw(DefaultMapStyle);
|
|
10
|
-
|
|
10
|
+
// The variant last handed to setStyle (loaded or still loading); null once a load has failed
|
|
11
|
+
// with nothing left to fall back to, so that any later selection is tried again.
|
|
12
|
+
private applied: MapStyleVariant | null = DefaultMapStyle;
|
|
13
|
+
// The variant the map last finished loading, which a failed change falls back to.
|
|
14
|
+
private loaded: MapStyleVariant = DefaultMapStyle;
|
|
15
|
+
private stopWaiting: (() => void) | null = null;
|
|
11
16
|
|
|
12
17
|
selectReferenceStyle(style: ReferenceMapStyle): void {
|
|
13
18
|
this.referenceStyle = style;
|
|
@@ -25,17 +30,64 @@ class MapStyleState {
|
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
private apply(): void {
|
|
33
|
+
if (!mapManager.map || this.variant === this.applied) return;
|
|
34
|
+
this.load(this.variant);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private load(variant: MapStyleVariant): void {
|
|
28
38
|
const map = mapManager.map;
|
|
29
|
-
if (!map
|
|
30
|
-
|
|
39
|
+
if (!map) return;
|
|
40
|
+
// A newer change supersedes one still in flight.
|
|
41
|
+
this.stopWaiting?.();
|
|
42
|
+
this.applied = variant;
|
|
43
|
+
|
|
31
44
|
// setStyle wipes every custom source/layer; re-add enabled integrations once it loads.
|
|
32
|
-
|
|
45
|
+
const onLoad = () => {
|
|
46
|
+
this.stopWaiting?.();
|
|
47
|
+
this.loaded = variant;
|
|
33
48
|
mapManager.styleLoading = false;
|
|
34
49
|
integrationsManager.refresh();
|
|
35
|
-
}
|
|
50
|
+
};
|
|
51
|
+
// "error" also fires for tiles, sprites and glyphs; only the style document failing means
|
|
52
|
+
// the change failed.
|
|
53
|
+
const onError = (event: { error?: unknown }) => {
|
|
54
|
+
if (!isRequestFor(event.error, variant.getExpandedStyleURL())) return;
|
|
55
|
+
this.stopWaiting?.();
|
|
56
|
+
this.recover(variant);
|
|
57
|
+
};
|
|
58
|
+
this.stopWaiting = () => {
|
|
59
|
+
map.off("style.load", onLoad);
|
|
60
|
+
map.off("error", onError);
|
|
61
|
+
this.stopWaiting = null;
|
|
62
|
+
};
|
|
63
|
+
map.on("style.load", onLoad);
|
|
64
|
+
map.on("error", onError);
|
|
36
65
|
mapManager.styleLoading = true;
|
|
37
|
-
map.setStyle(
|
|
66
|
+
map.setStyle(variant);
|
|
38
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A style that fails to load leaves the map with no style at all, so go back to the last one
|
|
71
|
+
* that loaded and make the selection say so. When that one fails too (e.g. offline) there is
|
|
72
|
+
* nothing to draw on: `styleLoading` stays set, since integrations cannot add layers to a
|
|
73
|
+
* missing style, and the next selection starts a fresh attempt.
|
|
74
|
+
*/
|
|
75
|
+
private recover(failed: MapStyleVariant): void {
|
|
76
|
+
if (failed === this.loaded) {
|
|
77
|
+
this.applied = null;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.variant = this.loaded;
|
|
81
|
+
this.referenceStyle = this.loaded.getReferenceStyle();
|
|
82
|
+
this.load(this.loaded);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether a map "error" event's error is a failed request for `url`, ignoring the query. */
|
|
87
|
+
function isRequestFor(error: unknown, url: string): boolean {
|
|
88
|
+
const failed = (error as { url?: unknown } | null | undefined)?.url;
|
|
89
|
+
if (typeof failed !== "string") return false;
|
|
90
|
+
return failed.split("?")[0] === url.split("?")[0];
|
|
39
91
|
}
|
|
40
92
|
|
|
41
93
|
export const mapStyleState = new MapStyleState();
|
|
@@ -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";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { getMonitorDetails, type MonitorDetails } from "@sjvair/sdk";
|
|
2
|
+
|
|
3
|
+
export type MonitorDetailsStatus = "loading" | "ready" | "not-found" | "failed";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One monitor, fetched by id. Unlike `monitorsManager.latest`, this does not depend on the
|
|
7
|
+
* map's pollutant or on the monitor being active, so the detail panel can show any monitor.
|
|
8
|
+
*/
|
|
9
|
+
export class MonitorDetailsState {
|
|
10
|
+
monitor: MonitorDetails | null = $state.raw(null);
|
|
11
|
+
status: MonitorDetailsStatus = $state("loading");
|
|
12
|
+
|
|
13
|
+
#id: string | null = null;
|
|
14
|
+
// Bumped per request; a response for anything but the latest request is dropped.
|
|
15
|
+
#request = 0;
|
|
16
|
+
|
|
17
|
+
async load(id: string): Promise<void> {
|
|
18
|
+
this.#id = id;
|
|
19
|
+
const request = ++this.#request;
|
|
20
|
+
this.monitor = null;
|
|
21
|
+
this.status = "loading";
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const monitor = await getMonitorDetails(id);
|
|
25
|
+
if (request !== this.#request) return;
|
|
26
|
+
this.monitor = monitor;
|
|
27
|
+
this.status = "ready";
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (request !== this.#request) return;
|
|
30
|
+
if (isNotFound(error)) {
|
|
31
|
+
this.status = "not-found";
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.error(error);
|
|
35
|
+
this.status = "failed";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Re-fetches the monitor already shown. A failure keeps the data on screen. */
|
|
40
|
+
async refresh(): Promise<void> {
|
|
41
|
+
if (this.status !== "ready" || this.#id === null) return;
|
|
42
|
+
const request = ++this.#request;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const monitor = await getMonitorDetails(this.#id);
|
|
46
|
+
if (request !== this.#request) return;
|
|
47
|
+
this.monitor = monitor;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (request === this.#request) console.error(error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
retry(): Promise<void> {
|
|
54
|
+
return this.#id === null ? Promise.resolve() : this.load(this.#id);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The SDK rejects a failed request with `cause.result.status` set to the HTTP status, or,
|
|
60
|
+
* when it could not parse the error body, with the `Response` itself as the cause.
|
|
61
|
+
*/
|
|
62
|
+
function isNotFound(error: unknown): boolean {
|
|
63
|
+
const cause = (error as { cause?: { status?: unknown; result?: { status?: unknown } } } | null)
|
|
64
|
+
?.cause;
|
|
65
|
+
return cause?.result?.status === 404 || cause?.status === 404;
|
|
66
|
+
}
|
|
@@ -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 };
|