@sjvair/monitor-map 4.2.0 → 5.0.0-beta.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/LICENSE +21 -0
- package/README.md +61 -126
- package/dist/lib/MonitorDetailPanel.svelte +23 -20
- package/dist/lib/MonitorMapLayout.svelte +147 -77
- package/dist/lib/abort.d.ts +16 -0
- package/dist/lib/abort.js +51 -0
- package/dist/lib/components/DataBox.svelte +4 -1
- package/dist/lib/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
- package/dist/lib/config.svelte.d.ts +7 -0
- package/dist/lib/config.svelte.js +10 -0
- package/dist/lib/core.d.ts +2 -2
- package/dist/lib/core.js +2 -1
- package/dist/lib/data-chart/DataChart.svelte +54 -24
- package/dist/lib/data-chart/DateRange.d.ts +13 -4
- package/dist/lib/data-chart/DateRange.js +50 -14
- package/dist/lib/data-chart/chart-utils.js +5 -7
- package/dist/lib/data-chart/level-colors.d.ts +3 -4
- package/dist/lib/data-chart/level-colors.js +7 -4
- package/dist/lib/data-chart/pacific-time.d.ts +9 -0
- package/dist/lib/data-chart/pacific-time.js +25 -0
- package/dist/lib/data-chart/requests.d.ts +2 -1
- package/dist/lib/data-chart/requests.js +5 -2
- package/dist/lib/data-chart/tooltip.js +2 -2
- package/{src/lib/ev-stations/components/EvStationTooltip.svelte → dist/lib/ev-stations/EvPopup.svelte} +6 -5
- package/dist/lib/ev-stations/EvPopup.svelte.d.ts +7 -0
- package/dist/lib/ev-stations/ev-plugin.svelte.d.ts +14 -0
- package/dist/lib/ev-stations/ev-plugin.svelte.js +71 -0
- package/dist/lib/ev-stations/ev-station-icons.d.ts +1 -4
- package/dist/lib/ev-stations/ev-station-icons.js +1 -13
- package/dist/lib/ev-stations/ev-stations.d.ts +7 -0
- package/dist/lib/ev-stations/ev-stations.js +53 -0
- package/dist/lib/ev-stations/ev-store.svelte.d.ts +21 -0
- package/dist/lib/ev-stations/ev-store.svelte.js +83 -0
- package/dist/lib/ev-stations/station-popup.svelte.d.ts +14 -0
- package/dist/lib/ev-stations/station-popup.svelte.js +54 -0
- package/dist/lib/ev-stations/types.d.ts +0 -29
- package/dist/lib/ev-stations/zip-codes.d.ts +11 -0
- package/dist/lib/ev-stations/zip-codes.js +28 -16
- package/dist/lib/index.d.ts +8 -56
- package/dist/lib/index.js +7 -46
- package/dist/lib/legend/Legend.svelte +18 -19
- package/dist/lib/legend/Legend.svelte.d.ts +2 -2
- package/dist/lib/levels.d.ts +7 -0
- package/dist/lib/levels.js +19 -0
- package/dist/lib/map/MapShell.svelte +17 -24
- package/dist/lib/map/MapShell.svelte.d.ts +4 -4
- package/dist/lib/map/basemaps.d.ts +25 -0
- package/dist/lib/map/basemaps.js +66 -0
- package/dist/lib/map/live-map.svelte.d.ts +3 -0
- package/dist/lib/map/live-map.svelte.js +22 -0
- package/dist/lib/map/sync-plugins.svelte.d.ts +19 -0
- package/dist/lib/map/sync-plugins.svelte.js +52 -0
- package/dist/lib/monitors/components/MonitorSubscriptionManager.svelte +82 -82
- package/dist/lib/monitors/filtered-source.svelte.d.ts +9 -0
- package/dist/lib/monitors/filtered-source.svelte.js +28 -0
- package/dist/lib/monitors/monitor-details.svelte.js +11 -3
- package/dist/lib/monitors/monitors.svelte.d.ts +29 -26
- package/dist/lib/monitors/monitors.svelte.js +35 -126
- package/dist/lib/monitors/retry-load.d.ts +8 -0
- package/dist/lib/monitors/retry-load.js +11 -0
- package/dist/lib/monitors/subscriptions.d.ts +3 -0
- package/dist/lib/monitors/subscriptions.js +9 -0
- package/dist/lib/monitors/types.d.ts +1 -41
- package/dist/lib/options/map-option-defaults.d.ts +25 -36
- package/dist/lib/options/map-option-defaults.js +12 -30
- package/dist/lib/options/map-options.svelte.d.ts +13 -0
- package/dist/lib/options/map-options.svelte.js +26 -0
- package/dist/lib/options/menus/LayersMenu.svelte +8 -20
- package/dist/lib/options/menus/LayersMenu.svelte.d.ts +17 -2
- package/dist/lib/options/menus/MonitorsMenu.svelte +34 -18
- package/dist/lib/options/menus/OverlaysMenu.svelte +2 -2
- package/dist/lib/options/menus/PollutantMenu.svelte +4 -9
- package/dist/lib/options/menus/SettingsMenu.svelte +32 -25
- package/dist/lib/options/menus/SettingsMenu.svelte.d.ts +5 -16
- package/dist/lib/options/plugin-order.d.ts +8 -0
- package/dist/lib/options/plugin-order.js +36 -0
- package/dist/lib/options/pollutant-param.d.ts +7 -5
- package/dist/lib/options/pollutant-param.js +9 -5
- package/dist/lib/options/row-icons.d.ts +8 -0
- package/dist/lib/options/row-icons.js +15 -0
- package/dist/lib/options/rows/MarkerIcon.svelte +5 -5
- package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +1 -2
- package/dist/lib/search/Search.svelte +20 -33
- package/dist/lib/search/Search.svelte.d.ts +5 -1
- package/dist/lib/search/search-pin.svelte.d.ts +11 -0
- package/dist/lib/search/search-pin.svelte.js +28 -0
- package/dist/lib/search/service.d.ts +7 -2
- package/dist/lib/search/service.js +17 -10
- package/dist/lib/stores.svelte.d.ts +8 -0
- package/dist/lib/stores.svelte.js +9 -0
- package/dist/lib/wind/wind-plugin.d.ts +11 -0
- package/dist/lib/wind/wind-plugin.js +25 -0
- package/package.json +12 -10
- package/src/lib/MonitorDetailPanel.svelte +23 -20
- package/src/lib/MonitorMapLayout.svelte +147 -77
- package/src/lib/abort.ts +59 -0
- package/src/lib/components/DataBox.svelte +4 -1
- package/src/lib/config.svelte.ts +16 -0
- package/src/lib/core.ts +2 -2
- package/src/lib/data-chart/DataChart.svelte +54 -24
- package/src/lib/data-chart/DateRange.ts +58 -15
- package/src/lib/data-chart/chart-utils.ts +6 -7
- package/src/lib/data-chart/level-colors.ts +8 -9
- package/src/lib/data-chart/pacific-time.ts +31 -0
- package/src/lib/data-chart/requests.ts +14 -7
- package/src/lib/data-chart/tooltip.ts +2 -2
- package/{dist/lib/ev-stations/components/EvStationTooltip.svelte → src/lib/ev-stations/EvPopup.svelte} +6 -5
- package/src/lib/ev-stations/ev-plugin.svelte.ts +86 -0
- package/src/lib/ev-stations/ev-station-icons.ts +1 -17
- package/src/lib/ev-stations/ev-stations.ts +72 -0
- package/src/lib/ev-stations/ev-store.svelte.ts +117 -0
- package/src/lib/ev-stations/station-popup.svelte.ts +64 -0
- package/src/lib/ev-stations/types.ts +0 -34
- package/src/lib/ev-stations/zip-codes.ts +29 -16
- package/src/lib/index.ts +8 -99
- package/src/lib/legend/Legend.svelte +18 -19
- package/src/lib/levels.ts +24 -0
- package/src/lib/map/MapShell.svelte +17 -24
- package/src/lib/map/basemaps.ts +76 -0
- package/src/lib/map/live-map.svelte.ts +24 -0
- package/src/lib/map/sync-plugins.svelte.ts +69 -0
- package/src/lib/monitors/components/MonitorSubscriptionManager.svelte +82 -82
- package/src/lib/monitors/filtered-source.svelte.ts +38 -0
- package/src/lib/monitors/monitor-details.svelte.ts +12 -3
- package/src/lib/monitors/monitors.svelte.ts +62 -153
- package/src/lib/monitors/retry-load.ts +14 -0
- package/src/lib/monitors/subscriptions.ts +17 -0
- package/src/lib/monitors/types.ts +1 -47
- package/src/lib/options/map-option-defaults.ts +30 -53
- package/src/lib/options/map-options.svelte.ts +30 -0
- package/src/lib/options/menus/LayersMenu.svelte +8 -20
- package/src/lib/options/menus/MonitorsMenu.svelte +34 -18
- package/src/lib/options/menus/OverlaysMenu.svelte +2 -2
- package/src/lib/options/menus/PollutantMenu.svelte +4 -9
- package/src/lib/options/menus/SettingsMenu.svelte +32 -25
- package/src/lib/options/plugin-order.ts +38 -0
- package/src/lib/options/pollutant-param.ts +14 -9
- package/src/lib/options/row-icons.ts +17 -0
- package/src/lib/options/rows/MarkerIcon.svelte +5 -5
- package/src/lib/search/Search.svelte +20 -33
- package/src/lib/search/search-pin.svelte.ts +35 -0
- package/src/lib/search/service.ts +34 -15
- package/src/lib/stores.svelte.ts +15 -0
- package/src/lib/wind/wind-plugin.ts +33 -0
- package/dist/lib/collocation-sites/CollocationTooltip.svelte +0 -98
- package/dist/lib/collocation-sites/CollocationTooltip.svelte.d.ts +0 -7
- package/dist/lib/collocation-sites/collocations-icon-manager.svelte.d.ts +0 -7
- package/dist/lib/collocation-sites/collocations-icon-manager.svelte.js +0 -18
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +0 -21
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +0 -113
- package/dist/lib/collocation-sites/collocations.svelte.d.ts +0 -9
- package/dist/lib/collocation-sites/collocations.svelte.js +0 -17
- package/dist/lib/collocation-sites/types.d.ts +0 -8
- package/dist/lib/collocation-sites/types.js +0 -1
- package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
- package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte.d.ts +0 -7
- package/dist/lib/ev-stations/components/EvStationTooltip.svelte.d.ts +0 -7
- package/dist/lib/ev-stations/ev-stations-cluster-renderer.d.ts +0 -19
- package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +0 -131
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.d.ts +0 -31
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +0 -226
- package/dist/lib/ev-stations/ev-stations.svelte.d.ts +0 -9
- package/dist/lib/ev-stations/ev-stations.svelte.js +0 -55
- package/dist/lib/hms/hms-fire-icon-manager.d.ts +0 -13
- package/dist/lib/hms/hms-fire-icon-manager.js +0 -39
- package/dist/lib/hms/hms-fire-map-integration.svelte.d.ts +0 -23
- package/dist/lib/hms/hms-fire-map-integration.svelte.js +0 -94
- package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +0 -39
- package/dist/lib/hms/hms-smoke-map-integration.svelte.js +0 -87
- package/dist/lib/hms/hms.svelte.d.ts +0 -19
- package/dist/lib/hms/hms.svelte.js +0 -227
- package/dist/lib/legend/LegendBar.svelte +0 -45
- package/dist/lib/legend/LegendBar.svelte.d.ts +0 -7
- package/dist/lib/legend/legend-data.d.ts +0 -40
- package/dist/lib/legend/legend-data.js +0 -65
- package/dist/lib/map/Map.svelte +0 -13
- package/dist/lib/map/Map.svelte.d.ts +0 -8
- package/dist/lib/map/basemap.d.ts +0 -14
- package/dist/lib/map/basemap.js +0 -10
- package/dist/lib/map/icons.d.ts +0 -7
- package/dist/lib/map/icons.js +0 -33
- package/dist/lib/map/integrations/base-layer-seperator.d.ts +0 -9
- package/dist/lib/map/integrations/base-layer-seperator.js +0 -11
- package/dist/lib/map/integrations/click-manager.d.ts +0 -14
- package/dist/lib/map/integrations/click-manager.js +0 -52
- package/dist/lib/map/integrations/integrations-manager.d.ts +0 -8
- package/dist/lib/map/integrations/integrations-manager.js +0 -20
- package/dist/lib/map/integrations/map-display-option.svelte.d.ts +0 -7
- package/dist/lib/map/integrations/map-display-option.svelte.js +0 -10
- package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +0 -32
- package/dist/lib/map/integrations/map-geojson-integration.svelte.js +0 -60
- package/dist/lib/map/integrations/map-icon-manager.d.ts +0 -15
- package/dist/lib/map/integrations/map-icon-manager.js +0 -52
- package/dist/lib/map/integrations/map-integration.svelte.d.ts +0 -6
- package/dist/lib/map/integrations/map-integration.svelte.js +0 -2
- package/dist/lib/map/integrations/map-layer-integration.svelte.d.ts +0 -11
- package/dist/lib/map/integrations/map-layer-integration.svelte.js +0 -40
- package/dist/lib/map/integrations/tooltip.svelte.d.ts +0 -36
- package/dist/lib/map/integrations/tooltip.svelte.js +0 -131
- package/dist/lib/map/integrations/types.d.ts +0 -14
- package/dist/lib/map/integrations/types.js +0 -1
- package/dist/lib/map/map-manager.svelte.d.ts +0 -20
- package/dist/lib/map/map-manager.svelte.js +0 -63
- package/dist/lib/map/map-style-state.svelte.d.ts +0 -23
- package/dist/lib/map/map-style-state.svelte.js +0 -86
- package/dist/lib/map/utils.d.ts +0 -7
- package/dist/lib/map/utils.js +0 -36
- package/dist/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
- package/dist/lib/monitors/components/MonitorClusterTooltip.svelte.d.ts +0 -7
- package/dist/lib/monitors/components/MonitorTooltip.svelte +0 -61
- package/dist/lib/monitors/components/MonitorTooltip.svelte.d.ts +0 -7
- package/dist/lib/monitors/monitor-utils.d.ts +0 -4
- package/dist/lib/monitors/monitor-utils.js +0 -41
- package/dist/lib/monitors/monitors-cluster-renderer.d.ts +0 -33
- package/dist/lib/monitors/monitors-cluster-renderer.js +0 -221
- package/dist/lib/monitors/monitors-icon-manager.svelte.d.ts +0 -15
- package/dist/lib/monitors/monitors-icon-manager.svelte.js +0 -78
- package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +0 -43
- package/dist/lib/monitors/monitors-map-integration.svelte.js +0 -301
- package/dist/lib/options/reset-map-options.d.ts +0 -2
- package/dist/lib/options/reset-map-options.js +0 -24
- package/dist/lib/reactivity.svelte.d.ts +0 -0
- package/dist/lib/reactivity.svelte.js +0 -83
- package/dist/lib/wind/wind.svelte.d.ts +0 -10
- package/dist/lib/wind/wind.svelte.js +0 -13
- package/src/lib/collocation-sites/CollocationTooltip.svelte +0 -98
- package/src/lib/collocation-sites/collocations-icon-manager.svelte.ts +0 -21
- package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +0 -136
- package/src/lib/collocation-sites/collocations.svelte.ts +0 -24
- package/src/lib/collocation-sites/types.ts +0 -10
- package/src/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
- package/src/lib/ev-stations/ev-stations-cluster-renderer.ts +0 -162
- package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +0 -248
- package/src/lib/ev-stations/ev-stations.svelte.ts +0 -79
- package/src/lib/hms/hms-fire-icon-manager.ts +0 -48
- package/src/lib/hms/hms-fire-map-integration.svelte.ts +0 -109
- package/src/lib/hms/hms-smoke-map-integration.svelte.ts +0 -105
- package/src/lib/hms/hms.svelte.ts +0 -264
- package/src/lib/legend/LegendBar.svelte +0 -45
- package/src/lib/legend/legend-data.ts +0 -101
- package/src/lib/map/Map.svelte +0 -13
- package/src/lib/map/basemap.ts +0 -24
- package/src/lib/map/icons.ts +0 -42
- package/src/lib/map/integrations/base-layer-seperator.ts +0 -17
- package/src/lib/map/integrations/click-manager.ts +0 -64
- package/src/lib/map/integrations/integrations-manager.ts +0 -25
- package/src/lib/map/integrations/map-display-option.svelte.ts +0 -13
- package/src/lib/map/integrations/map-geojson-integration.svelte.ts +0 -77
- package/src/lib/map/integrations/map-icon-manager.ts +0 -67
- package/src/lib/map/integrations/map-integration.svelte.ts +0 -8
- package/src/lib/map/integrations/map-layer-integration.svelte.ts +0 -49
- package/src/lib/map/integrations/tooltip.svelte.ts +0 -153
- package/src/lib/map/integrations/types.ts +0 -24
- package/src/lib/map/map-manager.svelte.ts +0 -83
- package/src/lib/map/map-style-state.svelte.ts +0 -94
- package/src/lib/map/utils.ts +0 -53
- package/src/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
- package/src/lib/monitors/components/MonitorTooltip.svelte +0 -61
- package/src/lib/monitors/monitor-utils.ts +0 -57
- package/src/lib/monitors/monitors-cluster-renderer.ts +0 -285
- package/src/lib/monitors/monitors-icon-manager.svelte.ts +0 -98
- package/src/lib/monitors/monitors-map-integration.svelte.ts +0 -318
- package/src/lib/options/reset-map-options.ts +0 -28
- package/src/lib/reactivity.svelte.ts +0 -82
- package/src/lib/wind/wind.svelte.ts +0 -20
|
@@ -7,14 +7,20 @@
|
|
|
7
7
|
import uPlot from "uplot";
|
|
8
8
|
import "uplot/dist/uPlot.min.css";
|
|
9
9
|
import { getMonitorEntriesCSVUrl } from "@sjvair/sdk/monitors";
|
|
10
|
-
import { format, parseISO } from "date-fns";
|
|
11
10
|
import { RangeCalendar } from "$lib/components/ui/range-calendar";
|
|
12
11
|
import type { PanelPollutant } from "../monitors/panel-pollutant";
|
|
13
12
|
import { monitorsManager } from "../monitors/monitors.svelte";
|
|
14
13
|
import { DataChartManager } from "./data-chart.svelte";
|
|
15
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createDateRange,
|
|
16
|
+
dateRangeLabel as formatDateRange,
|
|
17
|
+
extendToNow,
|
|
18
|
+
fileDay,
|
|
19
|
+
includesToday
|
|
20
|
+
} from "./DateRange";
|
|
16
21
|
import { fetchChartData, type ChartMonitor } from "./requests";
|
|
17
22
|
import { getChartConfig } from "./chart-utils";
|
|
23
|
+
import { PACIFIC_TIME_ZONE } from "./pacific-time";
|
|
18
24
|
import { levelColors } from "./level-colors";
|
|
19
25
|
|
|
20
26
|
interface Props {
|
|
@@ -38,11 +44,7 @@
|
|
|
38
44
|
let dateRangeContainer: HTMLElement | undefined = $state();
|
|
39
45
|
let triggerRebuild: (() => void) | undefined = $state();
|
|
40
46
|
|
|
41
|
-
const dateRangeLabel = $derived(
|
|
42
|
-
format(parseISO(chartManager.dateRange.start), "MMM d") +
|
|
43
|
-
" – " +
|
|
44
|
-
format(parseISO(chartManager.dateRange.end), "MMM d, yyyy")
|
|
45
|
-
);
|
|
47
|
+
const dateRangeLabel = $derived(formatDateRange(chartManager.dateRange));
|
|
46
48
|
|
|
47
49
|
// A request with no entries still resolves to [[], []], so test the x axis, not the tuple.
|
|
48
50
|
const noChartData = $derived(!chartManager.chartData[0]?.length);
|
|
@@ -53,17 +55,18 @@
|
|
|
53
55
|
? "No Data Available For Selected Date Range"
|
|
54
56
|
: ""
|
|
55
57
|
);
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
const colors = $derived(
|
|
59
|
-
levelColors(monitorsManager.meta?.entryType(pollutant).asIter.levels ?? [])
|
|
60
|
-
);
|
|
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.
|
|
58
|
+
// Props are read through a $derived: the owner recomputes what it passes on every refresh,
|
|
59
|
+
// and a prop read directly would re-run an effect even when the value is unchanged.
|
|
65
60
|
const monitorId = $derived(monitor.id);
|
|
61
|
+
const monitorType = $derived(monitor.type);
|
|
66
62
|
const chartPollutant = $derived(pollutant);
|
|
63
|
+
|
|
64
|
+
// Level definitions and labels are shared metadata, not the map's current selection.
|
|
65
|
+
const entryType = $derived(monitorsManager.meta?.entryType(chartPollutant));
|
|
66
|
+
const pollutantLabel = $derived(entryType?.label ?? "");
|
|
67
|
+
const colors = $derived(levelColors(entryType?.scale ?? null));
|
|
68
|
+
|
|
69
|
+
// Reload for another monitor or pollutant, not for a new object describing the same one.
|
|
67
70
|
$effect(() => {
|
|
68
71
|
void monitorId;
|
|
69
72
|
void chartPollutant;
|
|
@@ -101,9 +104,15 @@
|
|
|
101
104
|
return () => document.removeEventListener("click", handleDocClick);
|
|
102
105
|
});
|
|
103
106
|
|
|
107
|
+
// Rebuild for new data or anything the chart draws with, and nothing else: `rebuild` runs
|
|
108
|
+
// untracked, so a refreshed copy of the same monitor does not recreate the chart.
|
|
104
109
|
$effect(() => {
|
|
105
|
-
void chartManager.chartData;
|
|
106
|
-
|
|
110
|
+
void chartManager.chartData;
|
|
111
|
+
void monitorType;
|
|
112
|
+
void colors;
|
|
113
|
+
void pollutantLabel;
|
|
114
|
+
const rebuild = triggerRebuild;
|
|
115
|
+
untrack(() => rebuild?.());
|
|
107
116
|
});
|
|
108
117
|
|
|
109
118
|
const chartAttachment: Attachment<HTMLElement> = (el) => {
|
|
@@ -121,7 +130,9 @@
|
|
|
121
130
|
instance?.destroy?.();
|
|
122
131
|
el.innerHTML = "";
|
|
123
132
|
const maxDiff = Math.max(...flatData) - Math.min(...flatData);
|
|
124
|
-
const opts = getChartConfig(
|
|
133
|
+
const opts = getChartConfig(monitorType, maxDiff, width, height, colors, pollutantLabel);
|
|
134
|
+
// Axis ticks in Pacific time, like the tooltip.
|
|
135
|
+
opts.tzDate = (ts) => uPlot.tzDate(new Date(ts * 1000), PACIFIC_TIME_ZONE);
|
|
125
136
|
instance = new uPlot(opts, chartManager.chartData, el);
|
|
126
137
|
};
|
|
127
138
|
|
|
@@ -143,17 +154,36 @@
|
|
|
143
154
|
};
|
|
144
155
|
|
|
145
156
|
let chartRequest = 0;
|
|
157
|
+
let chartController: AbortController | undefined;
|
|
158
|
+
|
|
159
|
+
// A closed panel cancels its in-flight fetch.
|
|
160
|
+
$effect(() => () => chartController?.abort());
|
|
146
161
|
|
|
147
162
|
// `silent` keeps the current chart on screen while a timed refresh loads.
|
|
148
163
|
async function loadChartData({ silent = false } = {}) {
|
|
149
164
|
const request = ++chartRequest;
|
|
150
|
-
|
|
151
|
-
const
|
|
165
|
+
chartController?.abort();
|
|
166
|
+
const controller = (chartController = new AbortController());
|
|
167
|
+
// A refresh that supersedes a load still in flight takes over its loading flag: the
|
|
168
|
+
// superseded load will never clear it.
|
|
169
|
+
const ownsLoading = !silent || chartManager.loading;
|
|
170
|
+
chartManager.loading = ownsLoading;
|
|
171
|
+
let data: Awaited<ReturnType<typeof fetchChartData>>;
|
|
172
|
+
try {
|
|
173
|
+
data = await fetchChartData(monitor, chartManager.dateRange, pollutant, {
|
|
174
|
+
signal: controller.signal
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// Superseded or closed: the newer call owns `loading` and the chart, so leave both.
|
|
178
|
+
if (controller.signal.aborted || request !== chartRequest) return;
|
|
179
|
+
console.error("Failed to load chart data: ", err);
|
|
180
|
+
data = [];
|
|
181
|
+
}
|
|
152
182
|
// A newer request (another range, monitor, pollutant or refresh) owns the chart now.
|
|
153
183
|
if (request !== chartRequest) return;
|
|
154
184
|
// A failed request resolves to [] (an empty range is [[], []]); a background refresh
|
|
155
185
|
// that failed keeps the chart already on screen.
|
|
156
|
-
if (
|
|
186
|
+
if (!ownsLoading && data.length === 0) return;
|
|
157
187
|
chartManager.chartData = data;
|
|
158
188
|
chartManager.loading = false;
|
|
159
189
|
}
|
|
@@ -186,8 +216,8 @@
|
|
|
186
216
|
if (!canvas) return;
|
|
187
217
|
const link = document.createElement("a");
|
|
188
218
|
const monitorName = monitor.name.split(" ").join("-");
|
|
189
|
-
const start =
|
|
190
|
-
const end =
|
|
219
|
+
const start = fileDay(chartManager.dateRange.start);
|
|
220
|
+
const end = fileDay(chartManager.dateRange.end);
|
|
191
221
|
link.download = `${monitorName}_${start}-${end}.png`;
|
|
192
222
|
link.href = canvas.toDataURL();
|
|
193
223
|
link.click();
|
|
@@ -1,32 +1,75 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addPacificDays, endOfPacificDay, startOfPacificDay } from "@sjvair/sdk/datetime/pacific";
|
|
2
2
|
import type { DateValue } from "@internationalized/date";
|
|
3
3
|
|
|
4
|
+
/** ISO instants. Days are Pacific days, whatever zone the device is in. */
|
|
4
5
|
export type DateRange = { start: string; end: string };
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
/** An instant on the given calendar day in Pacific time (noon UTC is 4–5 AM Pacific). */
|
|
8
|
+
function onPacificDay(day: DateValue): Date {
|
|
9
|
+
return new Date(Date.UTC(day.year, day.month - 1, day.day, 12));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function samePacificDay(a: Date, b: Date): boolean {
|
|
13
|
+
return startOfPacificDay(a).getTime() === startOfPacificDay(b).getTime();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The chart's range: with no days picked, the start of yesterday through now; otherwise the
|
|
18
|
+
* whole picked days, ending now when the end day is today.
|
|
19
|
+
*/
|
|
20
|
+
export function createDateRange(start?: DateValue, end?: DateValue, now = new Date()): DateRange {
|
|
7
21
|
if (!start || !end) {
|
|
8
22
|
return {
|
|
9
|
-
start:
|
|
10
|
-
end:
|
|
23
|
+
start: startOfPacificDay(addPacificDays(now, -1)).toISOString(),
|
|
24
|
+
end: now.toISOString()
|
|
11
25
|
};
|
|
12
26
|
}
|
|
13
27
|
|
|
14
|
-
const
|
|
15
|
-
let endDate = new Date(end.year, end.month - 1, end.day);
|
|
16
|
-
endDate = isToday(endDate) ? new Date() : endOfDay(endDate);
|
|
17
|
-
|
|
28
|
+
const endDay = onPacificDay(end);
|
|
18
29
|
return {
|
|
19
|
-
start:
|
|
20
|
-
end:
|
|
30
|
+
start: startOfPacificDay(onPacificDay(start)).toISOString(),
|
|
31
|
+
end: (samePacificDay(endDay, now) ? now : endOfPacificDay(endDay)).toISOString()
|
|
21
32
|
};
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
/** Whether the range ends on the current
|
|
25
|
-
export function includesToday(range: DateRange): boolean {
|
|
26
|
-
return
|
|
35
|
+
/** Whether the range ends on the current Pacific day, i.e. it can still gain entries. */
|
|
36
|
+
export function includesToday(range: DateRange, now = new Date()): boolean {
|
|
37
|
+
return samePacificDay(new Date(range.end), now);
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
/** The same range with its end moved to the present. */
|
|
30
|
-
export function extendToNow(range: DateRange): DateRange {
|
|
31
|
-
return { start: range.start, end:
|
|
41
|
+
export function extendToNow(range: DateRange, now = new Date()): DateRange {
|
|
42
|
+
return { start: range.start, end: now.toISOString() };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PACIFIC = "America/Los_Angeles";
|
|
46
|
+
const shortDay = new Intl.DateTimeFormat("en-US", {
|
|
47
|
+
timeZone: PACIFIC,
|
|
48
|
+
month: "short",
|
|
49
|
+
day: "numeric"
|
|
50
|
+
});
|
|
51
|
+
const shortDayYear = new Intl.DateTimeFormat("en-US", {
|
|
52
|
+
timeZone: PACIFIC,
|
|
53
|
+
month: "short",
|
|
54
|
+
day: "numeric",
|
|
55
|
+
year: "numeric"
|
|
56
|
+
});
|
|
57
|
+
const fileDayParts = new Intl.DateTimeFormat("en-US", {
|
|
58
|
+
timeZone: PACIFIC,
|
|
59
|
+
day: "2-digit",
|
|
60
|
+
month: "short",
|
|
61
|
+
year: "numeric"
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** The range's Pacific days for display, e.g. "Nov 3 – Nov 4, 2026". */
|
|
65
|
+
export function dateRangeLabel(range: DateRange): string {
|
|
66
|
+
return `${shortDay.format(new Date(range.start))} – ${shortDayYear.format(new Date(range.end))}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An instant's Pacific day for a file name, e.g. "04.Nov.2026". */
|
|
70
|
+
export function fileDay(iso: string): string {
|
|
71
|
+
const parts = Object.fromEntries(
|
|
72
|
+
fileDayParts.formatToParts(new Date(iso)).map((part) => [part.type, part.value])
|
|
73
|
+
);
|
|
74
|
+
return `${parts.day}.${parts.month}.${parts.year}`;
|
|
32
75
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LevelColors } from "./level-colors";
|
|
2
2
|
import { tooltipsPlugin, uPlotCursorConfig } from "./tooltip";
|
|
3
|
-
import {
|
|
3
|
+
import { chartRangeMax } from "./pacific-time";
|
|
4
4
|
import type uPlot from "uplot";
|
|
5
5
|
import type { MonitorType } from "@sjvair/sdk";
|
|
6
6
|
|
|
@@ -64,12 +64,7 @@ export function getChartConfig(
|
|
|
64
64
|
},
|
|
65
65
|
scales: {
|
|
66
66
|
x: {
|
|
67
|
-
range: (_: uPlot, min: number, max: number) =>
|
|
68
|
-
const maxDate = fromUnixTime(max);
|
|
69
|
-
const now = new Date();
|
|
70
|
-
max = isToday(maxDate) && isAfter(now, maxDate) ? getUnixTime(now) : max;
|
|
71
|
-
return [min, max];
|
|
72
|
-
}
|
|
67
|
+
range: (_: uPlot, min: number, max: number) => [min, chartRangeMax(max)]
|
|
73
68
|
},
|
|
74
69
|
y: {
|
|
75
70
|
range: {
|
|
@@ -118,6 +113,10 @@ function getSeriesConfigs(monitorType: MonitorType, stops: LevelColors) {
|
|
|
118
113
|
case "airgradient":
|
|
119
114
|
case "purpleair":
|
|
120
115
|
return [baseXSeriesConfig, pm25SeriesConfig(stops)];
|
|
116
|
+
|
|
117
|
+
// Types without a real-time series of their own (CIMIS weather stations, from SDK 5.0).
|
|
118
|
+
default:
|
|
119
|
+
return [baseXSeriesConfig, singleSeriesConfig];
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import type { Scale } from "@sjvair/map-sdk/data";
|
|
2
|
+
|
|
1
3
|
/** Color stops for a chart: each level's lower bound and its hex color without the `#`. */
|
|
2
4
|
export type LevelColors = Array<[number, string]>;
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}>
|
|
11
|
-
): LevelColors {
|
|
12
|
-
return levels.map((level) => [level.range[0], level.color.replace(/^#/, "")]);
|
|
6
|
+
/** The chart's color stops from a range scale; a level with no lower bound or color is skipped. */
|
|
7
|
+
export function levelColors(scale: Scale | null): LevelColors {
|
|
8
|
+
return (scale?.levels ?? []).flatMap((level): LevelColors => {
|
|
9
|
+
if (level.min === null || !level.color) return [];
|
|
10
|
+
return [[level.min, level.color.replace(/^#/, "")]];
|
|
11
|
+
});
|
|
13
12
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { startOfPacificDay } from "@sjvair/sdk/datetime/pacific";
|
|
2
|
+
|
|
3
|
+
/** The chart's zone: times and days are Pacific, whatever zone the device is in. */
|
|
4
|
+
export const PACIFIC_TIME_ZONE = "America/Los_Angeles";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The x-axis end: `now` when the data ends earlier on the current Pacific day (so the axis
|
|
8
|
+
* reaches the present), else the data's end. Both are Unix seconds.
|
|
9
|
+
*/
|
|
10
|
+
export function chartRangeMax(max: number, now: Date = new Date()): number {
|
|
11
|
+
const end = new Date(max * 1000);
|
|
12
|
+
const today = startOfPacificDay(end).getTime() === startOfPacificDay(now).getTime();
|
|
13
|
+
return today && now > end ? Math.floor(now.getTime() / 1000) : max;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
17
|
+
timeZone: PACIFIC_TIME_ZONE,
|
|
18
|
+
month: "short",
|
|
19
|
+
day: "numeric",
|
|
20
|
+
year: "numeric",
|
|
21
|
+
hour: "numeric",
|
|
22
|
+
minute: "2-digit"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** A Unix time as Pacific wall-clock time, e.g. "Nov 4, 2026 8:05 PM". */
|
|
26
|
+
export function formatPacificTime(unixSeconds: number): string {
|
|
27
|
+
const part = Object.fromEntries(
|
|
28
|
+
formatter.formatToParts(new Date(unixSeconds * 1000)).map((p) => [p.type, p.value])
|
|
29
|
+
);
|
|
30
|
+
return `${part.month} ${part.day}, ${part.year} ${part.hour}:${part.minute} ${part.dayPeriod}`;
|
|
31
|
+
}
|
|
@@ -2,6 +2,7 @@ import { getMonitorEntries, type MonitorDetails, type MonitorEntry } from "@sjva
|
|
|
2
2
|
import { getUnixTime, parseISO, differenceInMinutes, addMinutes } from "date-fns";
|
|
3
3
|
import type { PanelPollutant } from "../monitors/panel-pollutant";
|
|
4
4
|
import type { DateRange } from "./DateRange";
|
|
5
|
+
import type { SignalOptions } from "$lib/abort";
|
|
5
6
|
|
|
6
7
|
/** The monitor fields the chart reads. */
|
|
7
8
|
export type ChartMonitor = Pick<MonitorDetails, "id" | "name" | "type" | "data_source">;
|
|
@@ -9,14 +10,18 @@ export type ChartMonitor = Pick<MonitorDetails, "id" | "name" | "type" | "data_s
|
|
|
9
10
|
export async function fetchChartData(
|
|
10
11
|
m: ChartMonitor,
|
|
11
12
|
d: DateRange,
|
|
12
|
-
entryType: PanelPollutant
|
|
13
|
+
entryType: PanelPollutant,
|
|
14
|
+
options: SignalOptions = {}
|
|
13
15
|
): Promise<uPlot.AlignedData> {
|
|
14
|
-
return await getMonitorEntries(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
return await getMonitorEntries(
|
|
17
|
+
{
|
|
18
|
+
entryType,
|
|
19
|
+
monitorId: m.id,
|
|
20
|
+
timestampGte: d.start,
|
|
21
|
+
timestampLte: d.end
|
|
22
|
+
},
|
|
23
|
+
{ signal: options.signal }
|
|
24
|
+
)
|
|
20
25
|
.then((entries) => {
|
|
21
26
|
const xAxisData: Array<Date> = [];
|
|
22
27
|
const updateDuration = m.data_source.name === "PurpleAir" ? 2 : 60;
|
|
@@ -41,6 +46,8 @@ export async function fetchChartData(
|
|
|
41
46
|
return [xAxisData.map((d) => getUnixTime(d)), yAxisRecord] as uPlot.AlignedData;
|
|
42
47
|
})
|
|
43
48
|
.catch((err) => {
|
|
49
|
+
// A cancelled request is not a failure: let the caller see the reason, log nothing.
|
|
50
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
44
51
|
console.error("Failed to load chart data: ", err);
|
|
45
52
|
return [] as uPlot.AlignedData;
|
|
46
53
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatPacificTime } from "./pacific-time";
|
|
2
2
|
import type { LevelColors } from "./level-colors";
|
|
3
3
|
|
|
4
4
|
let cursLeft = -100;
|
|
@@ -153,7 +153,7 @@ export function tooltipsPlugin() {
|
|
|
153
153
|
let label = u.series[i].label as string;
|
|
154
154
|
label = label?.substring(label.indexOf("(") + 1, label.indexOf(")")) || label;
|
|
155
155
|
|
|
156
|
-
ttHeader.textContent =
|
|
156
|
+
ttHeader.textContent = formatPacificTime(xVal);
|
|
157
157
|
tt!.textContent = `${label}: ${yVal}`;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import CreditCard from "@lucide/svelte/icons/credit-card";
|
|
3
3
|
import Zap from "@lucide/svelte/icons/zap";
|
|
4
|
-
import type {
|
|
4
|
+
import type { EvStation } from "./types";
|
|
5
5
|
|
|
6
6
|
interface Props {
|
|
7
|
-
|
|
7
|
+
station: EvStation;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const {
|
|
10
|
+
const { station }: Props = $props();
|
|
11
11
|
|
|
12
|
+
// 4.x's station popup, from the station NREL returned (codes already turned into labels).
|
|
12
13
|
const computed = $derived.by(() => {
|
|
13
|
-
const p =
|
|
14
|
+
const p = station;
|
|
14
15
|
|
|
15
|
-
const connectors: string[] =
|
|
16
|
+
const connectors: string[] = (p.ev_connector_types ?? []).filter(Boolean);
|
|
16
17
|
|
|
17
18
|
const cards: string[] = p.cards_accepted
|
|
18
19
|
? p.cards_accepted.split(",").map((c) => c.trim())
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createRawSnippet } from "svelte";
|
|
2
|
+
import { pointsPlugin, type MapPlugin } from "@sjvair/map-sdk";
|
|
3
|
+
import type { DataSource } from "@sjvair/map-sdk/data";
|
|
4
|
+
import { EV_STATION_LEVELS, evStationIcon } from "./ev-station-icons";
|
|
5
|
+
import type { EvFeatureProperties, EvLevel } from "./ev-store.svelte.js";
|
|
6
|
+
|
|
7
|
+
export type EvPluginId = "ev-l2" | "ev-l3";
|
|
8
|
+
|
|
9
|
+
export const EV_PLUGIN_IDS: Record<EvLevel, EvPluginId> = { lvl2: "ev-l2", lvl3: "ev-l3" };
|
|
10
|
+
|
|
11
|
+
const LEVEL_LABELS: Record<EvLevel, string> = { lvl2: "Level 2", lvl3: "Level 3" };
|
|
12
|
+
|
|
13
|
+
/** The marker size, as in 4.x. */
|
|
14
|
+
const ICON_SIZE = 32;
|
|
15
|
+
|
|
16
|
+
/** "rgb(r, g, b)" at 0.6 alpha, the cluster bubbles' fill in 4.x. */
|
|
17
|
+
function clusterFill(rgb: string): string {
|
|
18
|
+
return rgb.replace("rgb(", "rgba(").replace(")", ", 0.6)");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 4.x's cluster tooltip: "Level 2 Chargers" over "N stations". */
|
|
22
|
+
function clusterTooltip(level: EvLevel, count: number) {
|
|
23
|
+
return createRawSnippet(() => ({
|
|
24
|
+
render: () =>
|
|
25
|
+
`<div class="flex flex-col gap-1 p-1"><p class="font-semibold">${LEVEL_LABELS[level]} Chargers</p><p class="text-sm">${count} stations</p></div>`
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One level of EV chargers, as 4.x drew them: the level's charger icon and, clustered, bubbles in
|
|
31
|
+
* the level's color (radius 15/20/25 by count, with a white count) that zoom in on click. Stations
|
|
32
|
+
* are listed for the feature list; a click on one is a `featureclick` with a `feature` ref whose
|
|
33
|
+
* id is the station id (the layout opens the popup).
|
|
34
|
+
*/
|
|
35
|
+
export function evPlugin(
|
|
36
|
+
level: EvLevel,
|
|
37
|
+
source: DataSource<EvFeatureProperties>,
|
|
38
|
+
options: { clustered: boolean }
|
|
39
|
+
): MapPlugin {
|
|
40
|
+
const { bg, border } = EV_STATION_LEVELS[level];
|
|
41
|
+
const icon = evStationIcon(bg, border);
|
|
42
|
+
return pointsPlugin<EvFeatureProperties>({
|
|
43
|
+
id: EV_PLUGIN_IDS[level],
|
|
44
|
+
source,
|
|
45
|
+
layerLabel: `${LEVEL_LABELS[level]} Chargers`,
|
|
46
|
+
selectable: false,
|
|
47
|
+
icon: { category: () => level, draw: () => icon, size: ICON_SIZE },
|
|
48
|
+
detail: () => `${LEVEL_LABELS[level]} charger`,
|
|
49
|
+
...(options.clustered && {
|
|
50
|
+
cluster: {
|
|
51
|
+
radius: 40,
|
|
52
|
+
maxZoom: 9,
|
|
53
|
+
layers: ({ sourceId }) => [
|
|
54
|
+
{
|
|
55
|
+
local: "cluster-circle",
|
|
56
|
+
spec: {
|
|
57
|
+
type: "circle",
|
|
58
|
+
source: sourceId,
|
|
59
|
+
filter: ["has", "point_count"],
|
|
60
|
+
paint: {
|
|
61
|
+
"circle-color": clusterFill(bg),
|
|
62
|
+
"circle-radius": ["step", ["get", "point_count"], 15, 10, 20, 25, 25]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
local: "cluster-label",
|
|
68
|
+
spec: {
|
|
69
|
+
type: "symbol",
|
|
70
|
+
source: sourceId,
|
|
71
|
+
filter: ["has", "point_count"],
|
|
72
|
+
layout: {
|
|
73
|
+
"text-field": ["get", "point_count_abbreviated"],
|
|
74
|
+
"text-size": 12,
|
|
75
|
+
"text-allow-overlap": true,
|
|
76
|
+
"text-ignore-placement": true
|
|
77
|
+
},
|
|
78
|
+
paint: { "text-color": "#ffffff" }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
tooltip: (cluster) => clusterTooltip(level, cluster.pointCount)
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { MapIconManager } from "$lib/map/integrations/map-icon-manager";
|
|
2
|
-
import { asDataURI } from "$lib/map/icons";
|
|
3
|
-
|
|
4
1
|
const EV_STATION_PATH =
|
|
5
2
|
"m340-200 100-160h-60v-120L280-320h60v120ZM240-560h240v-200H240v200Zm0 360h240v-280H240v280Zm-80 80v-640q0-33 23.5-56.5T240-840h240q33 0 56.5 23.5T560-760v280h50q29 0 49.5 20.5T680-410v185q0 17 14 31t31 14q18 0 31.5-14t13.5-31v-375h-10q-17 0-28.5-11.5T720-640v-80h20v-60h40v60h40v-60h40v60h20v80q0 17-11.5 28.5T840-600h-10v375q0 42-30.5 73.5T725-120q-43 0-74-31.5T620-225v-185q0-5-2.5-7.5T610-420h-50v300H160Zm320-80H240h240Z";
|
|
6
3
|
|
|
@@ -11,6 +8,7 @@ export const EV_STATION_LEVELS = {
|
|
|
11
8
|
|
|
12
9
|
export type EvStationLevel = keyof typeof EV_STATION_LEVELS;
|
|
13
10
|
|
|
11
|
+
/** A charger marker: a circle in the level's colors with the white charger glyph, 32px. */
|
|
14
12
|
export function evStationIcon(bg: string, border: string): string {
|
|
15
13
|
return `<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
|
16
14
|
<circle cx="16" cy="16" r="14" fill="${bg}" stroke="${border}" stroke-width="2"/>
|
|
@@ -19,17 +17,3 @@ export function evStationIcon(bg: string, border: string): string {
|
|
|
19
17
|
</svg>
|
|
20
18
|
</svg>`;
|
|
21
19
|
}
|
|
22
|
-
|
|
23
|
-
export class EvStationIconManager extends MapIconManager {
|
|
24
|
-
constructor() {
|
|
25
|
-
super();
|
|
26
|
-
for (const [level, colors] of Object.entries(EV_STATION_LEVELS) as Array<
|
|
27
|
-
[EvStationLevel, (typeof EV_STATION_LEVELS)[EvStationLevel]]
|
|
28
|
-
>) {
|
|
29
|
-
const id = `ev-station-${level}`;
|
|
30
|
-
const icon = new Image(32, 32);
|
|
31
|
-
icon.src = asDataURI(evStationIcon(colors.bg, colors.border));
|
|
32
|
-
this.register(id, icon);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accessDetailCodeTypes,
|
|
3
|
+
creditCardTypes,
|
|
4
|
+
evConnectorTypes,
|
|
5
|
+
evNetworkTypes,
|
|
6
|
+
facilityTypes
|
|
7
|
+
} from "./data-maps";
|
|
8
|
+
import { monitorMapConfig } from "../config.svelte.js";
|
|
9
|
+
import type { EvStation } from "./types";
|
|
10
|
+
import { EV_ZIP_CODES } from "./zip-codes";
|
|
11
|
+
|
|
12
|
+
/** Which NREL charging level each map level asks for. */
|
|
13
|
+
const CHARGING_LEVEL = { lvl2: "2", lvl3: "dc_fast" } as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetches the public electric chargers of one level in the Valley's ZIPs from NREL (key from
|
|
17
|
+
* `monitorMapConfig().nrelKey`), with codes turned into labels. Station 226154 is dropped: its
|
|
18
|
+
* coordinates are wrong.
|
|
19
|
+
*/
|
|
20
|
+
export async function fetchEvStations(
|
|
21
|
+
level: "lvl2" | "lvl3",
|
|
22
|
+
signal?: AbortSignal
|
|
23
|
+
): Promise<Array<EvStation>> {
|
|
24
|
+
if (signal?.aborted) throw signal.reason;
|
|
25
|
+
const url = new URL(`https://developer.nlr.gov/api/alt-fuel-stations/v1.json`);
|
|
26
|
+
url.searchParams.set("fuel_type", "ELEC");
|
|
27
|
+
url.searchParams.set("state", "CA");
|
|
28
|
+
url.searchParams.set("status", "E");
|
|
29
|
+
url.searchParams.set("access", "public");
|
|
30
|
+
url.searchParams.set("ev_charging_level", CHARGING_LEVEL[level]);
|
|
31
|
+
url.searchParams.set("zip", EV_ZIP_CODES.join(","));
|
|
32
|
+
url.searchParams.set("api_key", monitorMapConfig().nrelKey);
|
|
33
|
+
|
|
34
|
+
const res = await fetch(url, { signal });
|
|
35
|
+
if (!res.ok) throw new Error(`EV chargers: NREL answered ${res.status}`);
|
|
36
|
+
const data = await res.json();
|
|
37
|
+
const fuelStations: Array<EvStation> = data.fuel_stations;
|
|
38
|
+
if (!Array.isArray(fuelStations)) throw new Error("EV chargers: no fuel_stations in the answer");
|
|
39
|
+
return (
|
|
40
|
+
fuelStations
|
|
41
|
+
.map((station) => {
|
|
42
|
+
if (station.access_detail_code) {
|
|
43
|
+
station.access_detail_code = accessDetailCodeTypes.get(station.access_detail_code);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (station.cards_accepted) {
|
|
47
|
+
station.cards_accepted = station.cards_accepted
|
|
48
|
+
.split(" ")
|
|
49
|
+
.map((c: string) => creditCardTypes.get(c))
|
|
50
|
+
.join(", ");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (station.ev_connector_types && station.ev_connector_types.length) {
|
|
54
|
+
station.ev_connector_types = station.ev_connector_types.map((t: string) =>
|
|
55
|
+
evConnectorTypes.get(t)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (station.ev_network) {
|
|
60
|
+
station.ev_network = evNetworkTypes.get(station.ev_network);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (station.facility_type) {
|
|
64
|
+
station.facility_type = facilityTypes.get(station.facility_type);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return station;
|
|
68
|
+
})
|
|
69
|
+
// remove stations with invalid coordinates
|
|
70
|
+
.filter((station) => station.id !== 226154)
|
|
71
|
+
);
|
|
72
|
+
}
|