@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.
Files changed (106) hide show
  1. package/README.md +94 -14
  2. package/dist/lib/LazyMapLayout.svelte +53 -0
  3. package/dist/lib/LazyMapLayout.svelte.d.ts +7 -0
  4. package/dist/lib/LazyMonitorDetailPanel.svelte +33 -0
  5. package/dist/lib/LazyMonitorDetailPanel.svelte.d.ts +4 -0
  6. package/dist/lib/LoadError.svelte +19 -0
  7. package/dist/lib/LoadError.svelte.d.ts +7 -0
  8. package/dist/lib/LoadScreen.svelte +10 -1
  9. package/dist/lib/LoadScreen.svelte.d.ts +6 -16
  10. package/dist/lib/MonitorDetailPanel.svelte +108 -28
  11. package/dist/lib/MonitorMapLayout.svelte +21 -1
  12. package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +1 -1
  13. package/dist/lib/core.d.ts +12 -0
  14. package/dist/lib/core.js +10 -0
  15. package/dist/lib/data-chart/DataChart.svelte +63 -13
  16. package/dist/lib/data-chart/DataChart.svelte.d.ts +10 -5
  17. package/dist/lib/data-chart/DateRange.d.ts +4 -0
  18. package/dist/lib/data-chart/DateRange.js +9 -1
  19. package/dist/lib/data-chart/chart-utils.d.ts +2 -1
  20. package/dist/lib/data-chart/chart-utils.js +19 -23
  21. package/dist/lib/data-chart/level-colors.d.ts +6 -0
  22. package/dist/lib/data-chart/level-colors.js +5 -0
  23. package/dist/lib/data-chart/requests.d.ts +5 -2
  24. package/dist/lib/data-chart/requests.js +2 -3
  25. package/dist/lib/data-chart/tooltip.d.ts +2 -1
  26. package/dist/lib/data-chart/tooltip.js +1 -9
  27. package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +1 -1
  28. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +5 -7
  29. package/dist/lib/hms/hms-fire-map-integration.svelte.js +1 -1
  30. package/dist/lib/hms/hms-smoke-map-integration.svelte.js +1 -1
  31. package/dist/lib/index.d.ts +8 -3
  32. package/dist/lib/index.js +6 -1
  33. package/dist/lib/lazy-chunks.d.ts +2 -0
  34. package/dist/lib/lazy-chunks.js +6 -0
  35. package/dist/lib/lazy-routes.d.ts +12 -0
  36. package/dist/lib/lazy-routes.js +18 -0
  37. package/dist/lib/map/Map.svelte +1 -1
  38. package/dist/lib/map/MapShell.svelte +9 -3
  39. package/dist/lib/map/MapShell.svelte.d.ts +5 -0
  40. package/dist/lib/map/integrations/click-manager.js +1 -1
  41. package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +13 -0
  42. package/dist/lib/map/integrations/map-geojson-integration.svelte.js +31 -5
  43. package/dist/lib/map/integrations/map-icon-manager.js +1 -1
  44. package/dist/lib/map/integrations/map-layer-integration.svelte.js +1 -1
  45. package/dist/lib/map/integrations/tooltip.svelte.js +1 -1
  46. package/dist/lib/map/map-style-state.svelte.d.ts +10 -0
  47. package/dist/lib/map/map-style-state.svelte.js +57 -6
  48. package/dist/lib/monitors/components/MonitorClusterTooltip.svelte +1 -1
  49. package/dist/lib/monitors/monitor-details.svelte.d.ts +15 -0
  50. package/dist/lib/monitors/monitor-details.svelte.js +63 -0
  51. package/dist/lib/monitors/monitors-cluster-renderer.js +1 -1
  52. package/dist/lib/monitors/monitors-icon-manager.svelte.js +1 -1
  53. package/dist/lib/monitors/monitors-map-integration.svelte.js +5 -7
  54. package/dist/lib/monitors/monitors.svelte.d.ts +22 -1
  55. package/dist/lib/monitors/monitors.svelte.js +75 -8
  56. package/dist/lib/monitors/panel-pollutant.d.ts +37 -0
  57. package/dist/lib/monitors/panel-pollutant.js +43 -0
  58. package/dist/lib/options/OptionsBar.svelte +8 -1
  59. package/dist/lib/options/OptionsMenu.svelte +10 -1
  60. package/dist/lib/search/Search.svelte +1 -1
  61. package/dist/lib/shared-load.d.ts +5 -0
  62. package/dist/lib/shared-load.js +14 -0
  63. package/package.json +13 -2
  64. package/src/lib/LazyMapLayout.svelte +53 -0
  65. package/src/lib/LazyMonitorDetailPanel.svelte +33 -0
  66. package/src/lib/LoadError.svelte +19 -0
  67. package/src/lib/LoadScreen.svelte +10 -1
  68. package/src/lib/MonitorDetailPanel.svelte +108 -28
  69. package/src/lib/MonitorMapLayout.svelte +21 -1
  70. package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +1 -1
  71. package/src/lib/core.ts +17 -0
  72. package/src/lib/data-chart/DataChart.svelte +63 -13
  73. package/src/lib/data-chart/DateRange.ts +11 -1
  74. package/src/lib/data-chart/chart-utils.ts +23 -26
  75. package/src/lib/data-chart/level-colors.ts +13 -0
  76. package/src/lib/data-chart/requests.ts +9 -5
  77. package/src/lib/data-chart/tooltip.ts +2 -10
  78. package/src/lib/ev-stations/ev-stations-cluster-renderer.ts +1 -1
  79. package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +5 -5
  80. package/src/lib/hms/hms-fire-map-integration.svelte.ts +1 -1
  81. package/src/lib/hms/hms-smoke-map-integration.svelte.ts +1 -1
  82. package/src/lib/index.ts +8 -3
  83. package/src/lib/lazy-chunks.ts +7 -0
  84. package/src/lib/lazy-routes.ts +20 -0
  85. package/src/lib/map/Map.svelte +1 -1
  86. package/src/lib/map/MapShell.svelte +9 -3
  87. package/src/lib/map/integrations/click-manager.ts +1 -1
  88. package/src/lib/map/integrations/map-geojson-integration.svelte.ts +33 -4
  89. package/src/lib/map/integrations/map-icon-manager.ts +1 -1
  90. package/src/lib/map/integrations/map-layer-integration.svelte.ts +1 -1
  91. package/src/lib/map/integrations/tooltip.svelte.ts +1 -1
  92. package/src/lib/map/map-style-state.svelte.ts +59 -7
  93. package/src/lib/monitors/components/MonitorClusterTooltip.svelte +1 -1
  94. package/src/lib/monitors/monitor-details.svelte.ts +66 -0
  95. package/src/lib/monitors/monitors-cluster-renderer.ts +1 -1
  96. package/src/lib/monitors/monitors-icon-manager.svelte.ts +1 -1
  97. package/src/lib/monitors/monitors-map-integration.svelte.ts +5 -5
  98. package/src/lib/monitors/monitors.svelte.ts +93 -10
  99. package/src/lib/monitors/panel-pollutant.ts +76 -0
  100. package/src/lib/options/OptionsBar.svelte +8 -1
  101. package/src/lib/options/OptionsMenu.svelte +10 -1
  102. package/src/lib/search/Search.svelte +1 -1
  103. package/src/lib/shared-load.ts +15 -0
  104. /package/dist/lib/map/{map.svelte.d.ts → map-manager.svelte.d.ts} +0 -0
  105. /package/dist/lib/map/{map.svelte.js → map-manager.svelte.js} +0 -0
  106. /package/src/lib/map/{map.svelte.ts → map-manager.svelte.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
+ import { untrack } from "svelte";
2
3
  import type { Attachment } from "svelte/attachments";
3
- import type { MonitorLatestType } from "@sjvair/sdk";
4
4
  import type { DateRange as CalendarRange } from "bits-ui";
5
5
  import { parseAbsolute } from "@internationalized/date";
6
6
  import { Maximize, SquareX } from "@lucide/svelte";
@@ -9,13 +9,23 @@
9
9
  import { getMonitorEntriesCSVUrl } from "@sjvair/sdk/monitors";
10
10
  import { format, parseISO } from "date-fns";
11
11
  import { RangeCalendar } from "../components/ui/range-calendar";
12
+ import type { PanelPollutant } from "../monitors/panel-pollutant";
13
+ import { monitorsManager } from "../monitors/monitors.svelte";
12
14
  import { DataChartManager } from "./data-chart.svelte";
13
- import { createDateRange } from "./DateRange";
14
- import { fetchChartData } from "./requests";
15
+ import { createDateRange, extendToNow, includesToday } from "./DateRange";
16
+ import { fetchChartData, type ChartMonitor } from "./requests";
15
17
  import { getChartConfig } from "./chart-utils";
16
- import { monitorsManager } from "../monitors/monitors.svelte";
18
+ import { levelColors } from "./level-colors";
19
+
20
+ interface Props {
21
+ monitor: ChartMonitor;
22
+ /** The pollutant to chart. Independent of the pollutant the map is coloring by. */
23
+ pollutant: PanelPollutant;
24
+ /** Bumped by the owner whenever fresh data may exist; see the effect below. */
25
+ refreshTick?: number;
26
+ }
17
27
 
18
- let { monitor }: { monitor: MonitorLatestType<"pm25" | "o3"> } = $props();
28
+ let { monitor, pollutant, refreshTick = 0 }: Props = $props();
19
29
 
20
30
  const chartManager = new DataChartManager();
21
31
  let expanded = $state(false);
@@ -34,7 +44,8 @@
34
44
  format(parseISO(chartManager.dateRange.end), "MMM d, yyyy")
35
45
  );
36
46
 
37
- const noChartData = $derived(!chartManager.chartData.length);
47
+ // A request with no entries still resolves to [[], []], so test the x axis, not the tuple.
48
+ const noChartData = $derived(!chartManager.chartData[0]?.length);
38
49
  const message = $derived(
39
50
  chartManager.loading
40
51
  ? "Loading Data"
@@ -42,10 +53,38 @@
42
53
  ? "No Data Available For Selected Date Range"
43
54
  : ""
44
55
  );
45
- const pollutantLabel = $derived(monitorsManager.pollutant === "pm25" ? "PM 2.5" : "Ozone");
56
+ const pollutantLabel = $derived(pollutant === "pm25" ? "PM 2.5" : "Ozone");
57
+ // Level definitions are shared metadata, not the map's current selection.
58
+ const colors = $derived(
59
+ levelColors(monitorsManager.meta?.entryType(pollutant).asIter.levels ?? [])
60
+ );
46
61
 
62
+ // Reload for another monitor or pollutant, not for a new object describing the same one.
63
+ // Both go through a $derived: the owner recomputes what it passes on every refresh, and a
64
+ // prop read directly would re-run this effect even when the value is unchanged.
65
+ const monitorId = $derived(monitor.id);
66
+ const chartPollutant = $derived(pollutant);
47
67
  $effect(() => {
48
- if (monitor) loadChartData();
68
+ void monitorId;
69
+ void chartPollutant;
70
+ untrack(() => loadChartData());
71
+ });
72
+
73
+ // Whether the chosen range ended on the day it was chosen, so it should follow the present.
74
+ // Remembered, not re-tested per tick: a panel left open past midnight keeps following.
75
+ let live = includesToday(chartManager.dateRange);
76
+ let seenTick = untrack(() => refreshTick);
77
+
78
+ // A range that ended in the past cannot gain entries, so only a live one reloads.
79
+ $effect(() => {
80
+ const tick = refreshTick;
81
+ if (tick === seenTick) return;
82
+ seenTick = tick;
83
+ if (!live) return;
84
+ untrack(() => {
85
+ chartManager.dateRange = extendToNow(chartManager.dateRange);
86
+ loadChartData({ silent: true });
87
+ });
49
88
  });
50
89
 
51
90
  $effect(() => {
@@ -82,7 +121,7 @@
82
121
  instance?.destroy?.();
83
122
  el.innerHTML = "";
84
123
  const maxDiff = Math.max(...flatData) - Math.min(...flatData);
85
- const opts = getChartConfig(monitor.type, maxDiff, width, height);
124
+ const opts = getChartConfig(monitor.type, maxDiff, width, height, colors, pollutantLabel);
86
125
  instance = new uPlot(opts, chartManager.chartData, el);
87
126
  };
88
127
 
@@ -103,9 +142,19 @@
103
142
  };
104
143
  };
105
144
 
106
- async function loadChartData() {
107
- chartManager.loading = true;
108
- chartManager.chartData = await fetchChartData(monitor, chartManager.dateRange);
145
+ let chartRequest = 0;
146
+
147
+ // `silent` keeps the current chart on screen while a timed refresh loads.
148
+ async function loadChartData({ silent = false } = {}) {
149
+ const request = ++chartRequest;
150
+ if (!silent) chartManager.loading = true;
151
+ const data = await fetchChartData(monitor, chartManager.dateRange, pollutant);
152
+ // A newer request (another range, monitor, pollutant or refresh) owns the chart now.
153
+ if (request !== chartRequest) return;
154
+ // A failed request resolves to [] (an empty range is [[], []]); a background refresh
155
+ // that failed keeps the chart already on screen.
156
+ if (silent && data.length === 0) return;
157
+ chartManager.chartData = data;
109
158
  chartManager.loading = false;
110
159
  }
111
160
 
@@ -113,6 +162,7 @@
113
162
  if (calendarValue?.start && calendarValue?.end) {
114
163
  dataRange = calendarValue;
115
164
  chartManager.dateRange = createDateRange(dataRange.start, dataRange.end);
165
+ live = includesToday(chartManager.dateRange);
116
166
  calendarOpen = false;
117
167
  loadChartData();
118
168
  }
@@ -121,7 +171,7 @@
121
171
  function downloadCSV() {
122
172
  const url = getMonitorEntriesCSVUrl({
123
173
  monitorId: monitor.id,
124
- entryType: monitorsManager.pollutant ?? "pm25",
174
+ entryType: pollutant,
125
175
  timestampGte: chartManager.dateRange.start,
126
176
  timestampLte: chartManager.dateRange.end
127
177
  });
@@ -1,8 +1,13 @@
1
- import type { MonitorLatestType } from "@sjvair/sdk";
2
1
  import "uplot/dist/uPlot.min.css";
3
- type $$ComponentProps = {
4
- monitor: MonitorLatestType<"pm25" | "o3">;
5
- };
6
- declare const DataChart: import("svelte").Component<$$ComponentProps, {}, "">;
2
+ import type { PanelPollutant } from "../monitors/panel-pollutant";
3
+ import { type ChartMonitor } from "./requests";
4
+ interface Props {
5
+ monitor: ChartMonitor;
6
+ /** The pollutant to chart. Independent of the pollutant the map is coloring by. */
7
+ pollutant: PanelPollutant;
8
+ /** Bumped by the owner whenever fresh data may exist; see the effect below. */
9
+ refreshTick?: number;
10
+ }
11
+ declare const DataChart: import("svelte").Component<Props, {}, "">;
7
12
  type DataChart = ReturnType<typeof DataChart>;
8
13
  export default DataChart;
@@ -4,3 +4,7 @@ export type DateRange = {
4
4
  end: string;
5
5
  };
6
6
  export declare function createDateRange(start?: DateValue, end?: DateValue): DateRange;
7
+ /** Whether the range ends on the current calendar day, i.e. it can still gain entries. */
8
+ export declare function includesToday(range: DateRange): boolean;
9
+ /** The same range with its end moved to the present. */
10
+ export declare function extendToNow(range: DateRange): DateRange;
@@ -1,4 +1,4 @@
1
- import { subDays, startOfDay, endOfDay, isToday } from "date-fns";
1
+ import { subDays, startOfDay, endOfDay, isToday, parseISO } from "date-fns";
2
2
  export function createDateRange(start, end) {
3
3
  if (!start || !end) {
4
4
  return {
@@ -14,3 +14,11 @@ export function createDateRange(start, end) {
14
14
  end: endDate.toISOString()
15
15
  };
16
16
  }
17
+ /** Whether the range ends on the current calendar day, i.e. it can still gain entries. */
18
+ export function includesToday(range) {
19
+ return isToday(parseISO(range.end));
20
+ }
21
+ /** The same range with its end moved to the present. */
22
+ export function extendToNow(range) {
23
+ return { start: range.start, end: new Date().toISOString() };
24
+ }
@@ -1,3 +1,4 @@
1
+ import type { LevelColors } from "./level-colors";
1
2
  import type uPlot from "uplot";
2
3
  import type { MonitorType } from "@sjvair/sdk";
3
- export declare function getChartConfig(sourceName: MonitorType, maxDiff: number, width: number, height: number): uPlot.Options;
4
+ export declare function getChartConfig(sourceName: MonitorType, maxDiff: number, width: number, height: number, colors: LevelColors, label: string): uPlot.Options;
@@ -1,32 +1,29 @@
1
- import { monitorsManager } from "../monitors/monitors.svelte";
2
1
  import { tooltipsPlugin, uPlotCursorConfig } from "./tooltip";
3
2
  import { fromUnixTime, getUnixTime, isToday, isAfter } from "date-fns";
4
3
  const PM25_AVG_60_STROKE = "3572b0";
5
- function getColors() {
6
- const levels = monitorsManager.levels ?? [];
7
- const colors = levels.map((level) => [level.range[0], level.color.replace(/^#/, "")]);
8
- colors.push([Infinity, "000000"]);
9
- return colors;
10
- }
11
4
  const baseXSeriesConfig = {};
12
5
  const baseYSeriesConfig = {
13
6
  show: true,
14
7
  spanGaps: false,
15
8
  width: 1
16
9
  };
17
- const pm25SeriesConfig = {
10
+ /** The level stops plus a final black stop, so values above the top level still get a color. */
11
+ function gradientStops(colors) {
12
+ return [...colors, [Infinity, "000000"]];
13
+ }
14
+ const pm25SeriesConfig = (stops) => ({
18
15
  ...baseYSeriesConfig,
19
16
  label: "Real Time",
20
17
  width: 2,
21
- stroke: (u) => scaleGradient(u, "y", 1, getColors(), true)
22
- };
23
- const pm25Avg60SeriesConfig = {
18
+ stroke: (u) => scaleGradient(u, "y", 1, stops, true)
19
+ });
20
+ const pm25Avg60SeriesConfig = (stops) => ({
24
21
  ...baseYSeriesConfig,
25
22
  label: "60 Minute Average",
26
- stroke: (u) => autograd(u, PM25_AVG_60_STROKE)
27
- };
28
- export function getChartConfig(sourceName, maxDiff, width, height) {
29
- const series = getSeriesConfigs(sourceName);
23
+ stroke: (u) => autograd(u, PM25_AVG_60_STROKE, stops)
24
+ });
25
+ export function getChartConfig(sourceName, maxDiff, width, height, colors, label) {
26
+ const series = getSeriesConfigs(sourceName, gradientStops(colors));
30
27
  const getStroke = (u, sIdx) => {
31
28
  const stroke = series[sIdx].stroke;
32
29
  return stroke(u, sIdx);
@@ -34,7 +31,7 @@ export function getChartConfig(sourceName, maxDiff, width, height) {
34
31
  return {
35
32
  width,
36
33
  height,
37
- cursor: uPlotCursorConfig.get(),
34
+ cursor: uPlotCursorConfig.get(colors),
38
35
  // @ts-expect-error: select missing pseudo "required" options
39
36
  select: {
40
37
  show: false
@@ -77,7 +74,7 @@ export function getChartConfig(sourceName, maxDiff, width, height) {
77
74
  }
78
75
  },
79
76
  {
80
- label: monitorsManager.pollutant === "pm25" ? "PM 2.5" : "Ozone",
77
+ label,
81
78
  side: 3,
82
79
  values: (_, ticks) => ticks.map((v) => v.toFixed(maxDiff < 5 ? 1 : 0))
83
80
  }
@@ -87,9 +84,8 @@ export function getChartConfig(sourceName, maxDiff, width, height) {
87
84
  }
88
85
  };
89
86
  }
90
- function getSeriesConfigs(monitorType) {
91
- const singleSeriesConfig = Object.assign({}, pm25Avg60SeriesConfig);
92
- singleSeriesConfig.width = 2;
87
+ function getSeriesConfigs(monitorType, stops) {
88
+ const singleSeriesConfig = { ...pm25Avg60SeriesConfig(stops), width: 2 };
93
89
  switch (monitorType) {
94
90
  case "airnow":
95
91
  case "aqview":
@@ -99,14 +95,14 @@ function getSeriesConfigs(monitorType) {
99
95
  return [baseXSeriesConfig, singleSeriesConfig];
100
96
  case "airgradient":
101
97
  case "purpleair":
102
- return [baseXSeriesConfig, pm25SeriesConfig];
98
+ return [baseXSeriesConfig, pm25SeriesConfig(stops)];
103
99
  }
104
100
  }
105
- function autograd(u, fallback) {
101
+ function autograd(u, fallback, stops) {
106
102
  if (u.series.length > 2) {
107
103
  return fallback;
108
104
  }
109
- return scaleGradient(u, "y", 1, getColors(), true);
105
+ return scaleGradient(u, "y", 1, stops, true);
110
106
  }
111
107
  function scaleGradient(u, scaleKey, ori, scaleStops, discrete = false) {
112
108
  const scale = u.scales[scaleKey];
@@ -0,0 +1,6 @@
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
+ export declare function levelColors(levels: ReadonlyArray<{
4
+ range: readonly [number, ...unknown[]] | readonly number[];
5
+ color: string;
6
+ }>): LevelColors;
@@ -0,0 +1,5 @@
1
+ // `range` is loose on purpose: the SDK types it as a tuple with an open tail, and only its
2
+ // first member (the level's lower bound) is read.
3
+ export function levelColors(levels) {
4
+ return levels.map((level) => [level.range[0], level.color.replace(/^#/, "")]);
5
+ }
@@ -1,3 +1,6 @@
1
- import { type MonitorLatestType } from "@sjvair/sdk";
1
+ import { type MonitorDetails } from "@sjvair/sdk";
2
+ import type { PanelPollutant } from "../monitors/panel-pollutant";
2
3
  import type { DateRange } from "./DateRange";
3
- export declare function fetchChartData(m: MonitorLatestType<"pm25" | "o3">, d: DateRange): Promise<uPlot.AlignedData>;
4
+ /** The monitor fields the chart reads. */
5
+ export type ChartMonitor = Pick<MonitorDetails, "id" | "name" | "type" | "data_source">;
6
+ export declare function fetchChartData(m: ChartMonitor, d: DateRange, entryType: PanelPollutant): Promise<uPlot.AlignedData>;
@@ -1,9 +1,8 @@
1
1
  import { getMonitorEntries } from "@sjvair/sdk";
2
2
  import { getUnixTime, parseISO, differenceInMinutes, addMinutes } from "date-fns";
3
- import { monitorsManager } from "../monitors/monitors.svelte";
4
- export async function fetchChartData(m, d) {
3
+ export async function fetchChartData(m, d, entryType) {
5
4
  return await getMonitorEntries({
6
- entryType: monitorsManager.pollutant ?? "pm25",
5
+ entryType,
7
6
  monitorId: m.id,
8
7
  timestampGte: d.start,
9
8
  timestampLte: d.end
@@ -1,6 +1,7 @@
1
+ import type { LevelColors } from "./level-colors";
1
2
  export declare const uPlotCursorConfig: {
2
3
  set: (left: number, top: number) => void;
3
- get: () => uPlot.Cursor;
4
+ get: (colors: LevelColors) => uPlot.Cursor;
4
5
  };
5
6
  export declare function tooltipsPlugin(): {
6
7
  hooks: {
@@ -1,11 +1,4 @@
1
1
  import { format, fromUnixTime } from "date-fns";
2
- import { monitorsManager } from "../monitors/monitors.svelte";
3
- function getColors() {
4
- return (monitorsManager.levels ?? []).map((level) => [
5
- level.range[0],
6
- level.color.replace(/^#/, "")
7
- ]);
8
- }
9
2
  let cursLeft = -100;
10
3
  let cursTop = -100;
11
4
  export const uPlotCursorConfig = {
@@ -13,7 +6,7 @@ export const uPlotCursorConfig = {
13
6
  cursLeft = left;
14
7
  cursTop = top;
15
8
  },
16
- get: () => ({
9
+ get: (colors) => ({
17
10
  left: cursLeft,
18
11
  top: cursTop,
19
12
  y: false,
@@ -21,7 +14,6 @@ export const uPlotCursorConfig = {
21
14
  size: 7,
22
15
  fill: (u, sIdx) => {
23
16
  const xIdx = u.cursor.idxs[sIdx];
24
- const colors = getColors();
25
17
  if (xIdx != null && u.series.length >= 2 && sIdx === 1) {
26
18
  const val = u.data[sIdx][xIdx];
27
19
  for (let i = colors.length - 1; i >= 0; i--) {
@@ -1,4 +1,4 @@
1
- import { mapManager } from "../map/map.svelte";
1
+ import { mapManager } from "../map/map-manager.svelte.js";
2
2
  import { mountPopup } from "../map/utils";
3
3
  import { clickManager } from "../map/integrations/click-manager";
4
4
  import { EV_STATION_LEVELS } from "./ev-station-icons";
@@ -1,5 +1,5 @@
1
1
  import { untrack } from "svelte";
2
- import { mapManager } from "../map/map.svelte";
2
+ import { mapManager } from "../map/map-manager.svelte.js";
3
3
  import { clickManager } from "../map/integrations/click-manager";
4
4
  import { MapIconLayerIntegration } from "../map/integrations/map-geojson-integration.svelte";
5
5
  import { MapDisplayOption } from "../map/integrations/map-display-option.svelte";
@@ -169,9 +169,9 @@ class EvStationsMapIntegration extends MapIconLayerIntegration {
169
169
  clickManager.unregister([this.referenceId]);
170
170
  this.tooltipManager.disable();
171
171
  this.renderer.remove();
172
- this.icons.loadIcons().then(() => {
173
- if (mapManager.styleLoading)
174
- return;
172
+ // Clustering may be toggled again before the icons arrive; only the latest apply runs.
173
+ this.supersedePending();
174
+ this.whenIconsLoad(() => {
175
175
  this.renderer.apply(this.handleStationClick);
176
176
  this.tooltipManager.enable();
177
177
  });
@@ -181,9 +181,7 @@ class EvStationsMapIntegration extends MapIconLayerIntegration {
181
181
  this.tooltipManager.disable();
182
182
  clickManager.unregister([this.referenceId]);
183
183
  super.apply();
184
- this.icons.loadIcons().then(() => {
185
- if (mapManager.styleLoading)
186
- return;
184
+ this.whenIconsLoad(() => {
187
185
  clickManager.register([this.referenceId], this.handleStationClick);
188
186
  this.tooltipManager.enable();
189
187
  });
@@ -1,4 +1,4 @@
1
- import { mapManager } from "../map/map.svelte";
1
+ import { mapManager } from "../map/map-manager.svelte.js";
2
2
  import { MapIconLayerIntegration } from "../map/integrations/map-geojson-integration.svelte";
3
3
  import { hmsManager } from "./hms.svelte";
4
4
  import { HMSFireIconManager, getTierIconId } from "./hms-fire-icon-manager";
@@ -1,4 +1,4 @@
1
- import { mapManager } from "../map/map.svelte";
1
+ import { mapManager } from "../map/map-manager.svelte.js";
2
2
  import { MapGeoJSONIntegration } from "../map/integrations/map-geojson-integration.svelte";
3
3
  import { hmsManager } from "./hms.svelte";
4
4
  /** Smoke fill styles, shared by the map layer and the legend so they can't drift apart. */
@@ -23,19 +23,24 @@
23
23
  *
24
24
  * <Router />
25
25
  * ```
26
+ *
27
+ * A host that must stay light at startup imports from the subpath entries instead:
28
+ * `provideMonitorMapRouter`/`monitorsManager` from "@sjvair/monitor-map/core" (no map), and
29
+ * `monitorMapRoutes` from "@sjvair/monitor-map/routes" (same tree, lazy layout and routes).
30
+ * `monitorMapRoutes` from this entry stays eager.
26
31
  */
27
32
  export { monitorMapRoutes } from "./routes";
28
33
  export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter, type MonitorMapRouterApi } from "./router-context";
29
34
  export { monitorsManager } from "./monitors/monitors.svelte";
30
- export type { MonitorsManager } from "./monitors/monitors.svelte";
35
+ export type { MonitorsManager, MonitorsInitOptions } from "./monitors/monitors.svelte";
31
36
  export { collocationSitesManager } from "./collocation-sites/collocations.svelte";
32
37
  export type { CollocationSitesManager } from "./collocation-sites/collocations.svelte";
33
38
  export { evStationsManager } from "./ev-stations/ev-stations.svelte";
34
39
  export type { EvStationsManager } from "./ev-stations/ev-stations.svelte";
35
40
  export { hmsManager } from "./hms/hms.svelte";
36
41
  export type { HMSManager } from "./hms/hms.svelte";
37
- export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
38
- export type { MapManager } from "./map/map.svelte";
42
+ export { mapManager, DefaultMapStyle, initializeMap } from "./map/map-manager.svelte.js";
43
+ export type { MapManager } from "./map/map-manager.svelte.js";
39
44
  export { monitorsMapIntegration, MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
40
45
  export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
41
46
  export { evStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
package/dist/lib/index.js CHANGED
@@ -23,6 +23,11 @@
23
23
  *
24
24
  * <Router />
25
25
  * ```
26
+ *
27
+ * A host that must stay light at startup imports from the subpath entries instead:
28
+ * `provideMonitorMapRouter`/`monitorsManager` from "@sjvair/monitor-map/core" (no map), and
29
+ * `monitorMapRoutes` from "@sjvair/monitor-map/routes" (same tree, lazy layout and routes).
30
+ * `monitorMapRoutes` from this entry stays eager.
26
31
  */
27
32
  export { monitorMapRoutes } from "./routes";
28
33
  export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter } from "./router-context";
@@ -31,7 +36,7 @@ export { monitorsManager } from "./monitors/monitors.svelte";
31
36
  export { collocationSitesManager } from "./collocation-sites/collocations.svelte";
32
37
  export { evStationsManager } from "./ev-stations/ev-stations.svelte";
33
38
  export { hmsManager } from "./hms/hms.svelte";
34
- export { mapManager, DefaultMapStyle, initializeMap } from "./map/map.svelte";
39
+ export { mapManager, DefaultMapStyle, initializeMap } from "./map/map-manager.svelte.js";
35
40
  // Map integrations
36
41
  export { monitorsMapIntegration, MonitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
37
42
  export { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
@@ -0,0 +1,2 @@
1
+ export declare const loadMonitorMapLayout: () => Promise<typeof import("./MonitorMapLayout.svelte")>;
2
+ export declare const loadMonitorDetailPanel: () => Promise<typeof import("./MonitorDetailPanel.svelte")>;
@@ -0,0 +1,6 @@
1
+ import { sharedLoad } from "./shared-load";
2
+ // The heavy chunks behind the lazy routes entry (`./lazy-routes.ts`). They are imported here,
3
+ // not in the wrapper components that await them: sv-router treats any function whose source
4
+ // contains `() => import(...)` as a lazy route, and a compiled component is such a function.
5
+ export const loadMonitorMapLayout = sharedLoad(() => import("./MonitorMapLayout.svelte"));
6
+ export const loadMonitorDetailPanel = sharedLoad(() => import("./MonitorDetailPanel.svelte"));
@@ -0,0 +1,12 @@
1
+ import type { Routes } from "sv-router";
2
+ /**
3
+ * Lazy routes entry (`@sjvair/monitor-map/routes`): the same route tree as the main entry's
4
+ * `monitorMapRoutes`, but the map layout and the monitor panel load on demand, so a host
5
+ * router can reference it at startup without loading the map.
6
+ *
7
+ * The components here are light wrappers that load the real ones themselves
8
+ * (`./lazy-chunks.ts`) and show the load screen meanwhile. They are deliberately not
9
+ * `() => import(...)` routes: sv-router renders nothing for a route until every lazy component
10
+ * on it has loaded, which left the outlet empty on a cold link into the map.
11
+ */
12
+ export declare const monitorMapRoutes: Routes;
@@ -0,0 +1,18 @@
1
+ import LazyMapLayout from "./LazyMapLayout.svelte";
2
+ import LazyMonitorDetailPanel from "./LazyMonitorDetailPanel.svelte";
3
+ import MainMapView from "./MainMapView.svelte";
4
+ /**
5
+ * Lazy routes entry (`@sjvair/monitor-map/routes`): the same route tree as the main entry's
6
+ * `monitorMapRoutes`, but the map layout and the monitor panel load on demand, so a host
7
+ * router can reference it at startup without loading the map.
8
+ *
9
+ * The components here are light wrappers that load the real ones themselves
10
+ * (`./lazy-chunks.ts`) and show the load screen meanwhile. They are deliberately not
11
+ * `() => import(...)` routes: sv-router renders nothing for a route until every lazy component
12
+ * on it has loaded, which left the outlet empty on a cold link into the map.
13
+ */
14
+ export const monitorMapRoutes = {
15
+ layout: LazyMapLayout,
16
+ "/": MainMapView,
17
+ "/monitor/:id": LazyMonitorDetailPanel
18
+ };
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import "@maptiler/sdk/dist/maptiler-sdk.css";
3
- import { initializeMap } from "./map.svelte";
3
+ import { initializeMap } from "./map-manager.svelte.js";
4
4
  import { integrationsManager } from "./integrations/integrations-manager";
5
5
  import type { SomeMapIntegration } from "./integrations/types";
6
6
 
@@ -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 "../LoadScreen.svelte";
54
59
  import Map from "./Map.svelte";
55
60
  import OptionsBar from "../options/OptionsBar.svelte";
56
- import { mapManager } from "./map.svelte";
61
+ import { mapManager } from "./map-manager.svelte.js";
57
62
  import { useMonitorMapRouter } from "../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?.()}
@@ -9,6 +9,11 @@ export interface MapShellProps {
9
9
  * is true.
10
10
  */
11
11
  ready: boolean;
12
+ /**
13
+ * Rendered on the load screen, under the logo, for as long as the load screen is up.
14
+ * Render it only when the caller's initial load failed (e.g. a message and a retry).
15
+ */
16
+ loadError?: Snippet;
12
17
  /** True when the routed detail panel (`children`) should be visible */
13
18
  panelOpen: boolean;
14
19
  /**
@@ -1,4 +1,4 @@
1
- import { mapManager } from "../map.svelte";
1
+ import { mapManager } from "../map-manager.svelte.js";
2
2
  class ClickManager {
3
3
  registrations = [];
4
4
  isListening = false;
@@ -15,5 +15,18 @@ export declare abstract class MapGeoJSONIntegration<T = Record<string, unknown>>
15
15
  }
16
16
  export declare abstract class MapIconLayerIntegration<T = Record<string, unknown>> extends MapGeoJSONIntegration<T> {
17
17
  abstract icons: MapIconManager;
18
+ private generation;
19
+ /**
20
+ * Drops every callback still waiting on icons. `apply()` and `remove()` call it, so a
21
+ * subclass overriding `apply()` without calling `super.apply()` first must call it itself.
22
+ */
23
+ protected supersedePending(): void;
24
+ /**
25
+ * Runs `run` once the icons have loaded, unless an `apply()` or `remove()` has happened
26
+ * since (the state it was scheduled for is gone) or a basemap change is in flight (the
27
+ * refresh on "style.load" re-applies).
28
+ */
29
+ protected whenIconsLoad(run: () => void): void;
18
30
  apply(): void;
31
+ remove(): void;
19
32
  }