@sjvair/monitor-map 4.3.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 +59 -138
- package/dist/lib/MonitorDetailPanel.svelte +23 -20
- package/dist/lib/MonitorMapLayout.svelte +147 -77
- 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 +40 -25
- 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/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 +66 -61
- 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/monitors.svelte.d.ts +25 -35
- package/dist/lib/monitors/monitors.svelte.js +35 -218
- package/dist/lib/monitors/retry-load.d.ts +8 -0
- package/dist/lib/monitors/retry-load.js +11 -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/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 +40 -25
- 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/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 +66 -61
- package/src/lib/monitors/filtered-source.svelte.ts +38 -0
- package/src/lib/monitors/monitors.svelte.ts +58 -261
- package/src/lib/monitors/retry-load.ts +14 -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 -10
- package/dist/lib/collocation-sites/collocations.svelte.js +0 -21
- 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 -10
- package/dist/lib/ev-stations/ev-stations.svelte.js +0 -57
- 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 -20
- package/dist/lib/hms/hms.svelte.js +0 -237
- 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 -27
- 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 -84
- 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 -270
- 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
|
@@ -1,38 +1,44 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from "svelte";
|
|
3
|
-
import { onDestroy, untrack } from "svelte";
|
|
3
|
+
import { onDestroy, onMount, untrack } from "svelte";
|
|
4
4
|
import { searchParams } from "sv-router";
|
|
5
|
+
import {
|
|
6
|
+
collocationSitesPlugin,
|
|
7
|
+
hmsFirePlugin,
|
|
8
|
+
hmsSmokePlugin,
|
|
9
|
+
monitorsPlugin
|
|
10
|
+
} from "@sjvair/map-sdk";
|
|
11
|
+
import { windPlugin } from "./wind/wind-plugin.js";
|
|
12
|
+
import { evPlugin } from "./ev-stations/ev-plugin.svelte.js";
|
|
13
|
+
import { createEvStore } from "./ev-stations/ev-store.svelte.js";
|
|
14
|
+
import { stationPopups } from "./ev-stations/station-popup.svelte.js";
|
|
5
15
|
import MapShell from "./map/MapShell.svelte";
|
|
16
|
+
import { createLiveMap } from "./map/live-map.svelte.js";
|
|
17
|
+
import { syncPlugins } from "./map/sync-plugins.svelte.js";
|
|
18
|
+
import LoadError from "./LoadError.svelte";
|
|
19
|
+
import { retryMonitorsLoad } from "./monitors/retry-load.js";
|
|
20
|
+
import { monitorsManager } from "./monitors/monitors.svelte.js";
|
|
21
|
+
import { filteredMonitors } from "./monitors/filtered-source.svelte.js";
|
|
22
|
+
import Legend from "./legend/Legend.svelte";
|
|
23
|
+
import {
|
|
24
|
+
collocationSitesStore,
|
|
25
|
+
hmsFireStore,
|
|
26
|
+
hmsSmokeStore,
|
|
27
|
+
monitorsStore
|
|
28
|
+
} from "./stores.svelte.js";
|
|
6
29
|
import OptionsGroup from "./options/OptionsGroup.svelte";
|
|
7
30
|
import PollutantMenu from "./options/menus/PollutantMenu.svelte";
|
|
8
31
|
import MonitorsMenu from "./options/menus/MonitorsMenu.svelte";
|
|
9
32
|
import LayersMenu from "./options/menus/LayersMenu.svelte";
|
|
10
33
|
import OverlaysMenu from "./options/menus/OverlaysMenu.svelte";
|
|
11
34
|
import SettingsMenu from "./options/menus/SettingsMenu.svelte";
|
|
35
|
+
import { mapOptions } from "./options/map-options.svelte.js";
|
|
36
|
+
import { wantedPlugins } from "./options/plugin-order";
|
|
12
37
|
import {
|
|
13
38
|
applyPollutantSelection,
|
|
14
39
|
currentSelection,
|
|
15
40
|
parsePollutantParam
|
|
16
41
|
} from "./options/pollutant-param";
|
|
17
|
-
import LoadError from "./LoadError.svelte";
|
|
18
|
-
import Legend from "./legend/Legend.svelte";
|
|
19
|
-
import {
|
|
20
|
-
fireSmokeLegendSections,
|
|
21
|
-
pollutantLegendSection,
|
|
22
|
-
type LegendSection
|
|
23
|
-
} from "./legend/legend-data";
|
|
24
|
-
import { monitorsManager } from "./monitors/monitors.svelte";
|
|
25
|
-
import { monitorsMapIntegration } from "./monitors/monitors-map-integration.svelte";
|
|
26
|
-
import { windMapIntegration } from "./wind/wind.svelte";
|
|
27
|
-
import { baseLayerSeperator } from "./map/integrations/base-layer-seperator";
|
|
28
|
-
import type { SomeMapIntegration } from "./map/integrations/types";
|
|
29
|
-
import { collocationSitesManager } from "./collocation-sites/collocations.svelte";
|
|
30
|
-
import { collocationSitesMapIntegration } from "./collocation-sites/collocations-map-integration.svelte";
|
|
31
|
-
import { evStationsMapIntegration } from "./ev-stations/ev-stations-map-integration.svelte";
|
|
32
|
-
import { hmsManager } from "./hms/hms.svelte";
|
|
33
|
-
import { hmsFireMapIntegration } from "./hms/hms-fire-map-integration.svelte";
|
|
34
|
-
import { FIRE_LEGEND_CATEGORIES } from "./hms/hms-fire-icon-manager";
|
|
35
|
-
import { hmsSmokeMapIntegration, SMOKE_LEGEND } from "./hms/hms-smoke-map-integration.svelte";
|
|
36
42
|
import Search from "./search/Search.svelte";
|
|
37
43
|
import { useMonitorMapRouter } from "./router-context";
|
|
38
44
|
|
|
@@ -44,51 +50,105 @@
|
|
|
44
50
|
|
|
45
51
|
const { route, navigate, basePath } = useMonitorMapRouter();
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
collocationSitesMapIntegration,
|
|
50
|
-
windMapIntegration,
|
|
51
|
-
hmsSmokeMapIntegration,
|
|
52
|
-
hmsFireMapIntegration,
|
|
53
|
-
monitorsMapIntegration,
|
|
54
|
-
evStationsMapIntegration
|
|
55
|
-
];
|
|
53
|
+
// The map's state lives as long as this layout; MapView attaches the GL map when it mounts.
|
|
54
|
+
const map = createLiveMap();
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// "?pollutant=none" must disable monitors before the effects below first run, so the
|
|
60
|
-
// state->URL effect never sees enabled monitors and overwrites the param.
|
|
56
|
+
// "?pollutant=none" must turn monitors off before the effects below first run, so the
|
|
57
|
+
// state->URL effect never sees monitors on and overwrites the param.
|
|
61
58
|
if (parsePollutantParam(route.search.pollutant) === "none") {
|
|
62
|
-
|
|
59
|
+
mapOptions.monitorsOn = false;
|
|
63
60
|
}
|
|
64
61
|
|
|
65
62
|
// The load screen stays up until the monitors load, so a failure has to be shown on it.
|
|
66
|
-
|
|
63
|
+
const loadFailed = $derived(monitorsStore.dataSource.status.state === "error");
|
|
67
64
|
|
|
68
65
|
function loadMonitors() {
|
|
69
|
-
loadFailed = false;
|
|
70
66
|
// A rejected init() can be retried: a later call starts a fresh run.
|
|
71
|
-
monitorsManager.init(route.search.pollutant).catch((error) =>
|
|
72
|
-
console.error(error);
|
|
73
|
-
loadFailed = true;
|
|
74
|
-
});
|
|
67
|
+
monitorsManager.init(route.search.pollutant).catch((error) => console.error(error));
|
|
75
68
|
}
|
|
76
69
|
|
|
77
70
|
loadMonitors();
|
|
78
|
-
// A previous instance of this layout stopped polling in onDestroy; init() is a no-op by now.
|
|
79
|
-
monitorsManager.resumeAutoUpdate();
|
|
80
|
-
collocationSitesManager.init();
|
|
81
|
-
hmsManager.init();
|
|
82
|
-
monitorsMapIntegration.onMonitorClick = (id: string) => {
|
|
83
|
-
navigate(`${basePath}/monitor/:id`, { params: { id } }).catch(console.error);
|
|
84
|
-
};
|
|
85
71
|
|
|
86
|
-
|
|
72
|
+
const panelOpen = $derived(route.pathname.startsWith(`${basePath}/monitor/`));
|
|
73
|
+
const selectedId = $derived(panelOpen ? route.params.id : undefined);
|
|
74
|
+
|
|
75
|
+
// The monitors the Monitors menu's filters let through; clusters are built from these.
|
|
76
|
+
const monitorsSource = filteredMonitors(monitorsStore.dataSource, mapOptions);
|
|
77
|
+
|
|
78
|
+
// EV chargers: each level is fetched the first time it is wanted. A failure shows in the legend
|
|
79
|
+
// with a retry.
|
|
80
|
+
const evStore = createEvStore();
|
|
81
|
+
const evPopups = stationPopups(map);
|
|
82
|
+
$effect(() => {
|
|
83
|
+
const wanted = wantedPlugins(mapOptions, monitorsManager.pollutant);
|
|
84
|
+
for (const [key, level] of [
|
|
85
|
+
["ev-l2", "lvl2"],
|
|
86
|
+
["ev-l3", "lvl3"]
|
|
87
|
+
] as const) {
|
|
88
|
+
if (wanted.has(key)) untrack(() => evStore.load(level)).catch(console.error);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// The options decide which plugins are on the map, in DRAW_ORDER. `clustered` is fixed when a
|
|
93
|
+
// plugin is built, so a change rebuilds it.
|
|
94
|
+
syncPlugins(
|
|
95
|
+
map,
|
|
96
|
+
() => wantedPlugins(mapOptions, monitorsManager.pollutant),
|
|
97
|
+
{
|
|
98
|
+
monitors: () => ({
|
|
99
|
+
plugin: monitorsPlugin({
|
|
100
|
+
id: "monitors",
|
|
101
|
+
source: monitorsSource,
|
|
102
|
+
layerLabel: "Monitors",
|
|
103
|
+
selectable: true,
|
|
104
|
+
selectedStyle: "enlarge",
|
|
105
|
+
clustered: mapOptions.clustered
|
|
106
|
+
}),
|
|
107
|
+
// A removed plugin forgets its mode and selection, so each add passes them again.
|
|
108
|
+
options: {
|
|
109
|
+
selectionMode: "single",
|
|
110
|
+
selection: selectedId ? { kind: "monitors", ids: [selectedId] } : null
|
|
111
|
+
}
|
|
112
|
+
}),
|
|
113
|
+
"ev-l2": () =>
|
|
114
|
+
evPlugin("lvl2", evStore.dataSource("lvl2"), { clustered: mapOptions.clustered }),
|
|
115
|
+
"ev-l3": () =>
|
|
116
|
+
evPlugin("lvl3", evStore.dataSource("lvl3"), { clustered: mapOptions.clustered })
|
|
117
|
+
},
|
|
118
|
+
() => mapOptions.clustered
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Wind, Fire & Smoke and collocation sites take no build-time option, so they sit in their own call
|
|
122
|
+
// without `deps`: a Marker Clusters change rebuilds only the monitors above. DRAW_ORDER still
|
|
123
|
+
// holds across the two calls because `syncPlugins` reads `map.plugins` (every plugin on the
|
|
124
|
+
// map, whichever call added it) to find the plugin to add before.
|
|
125
|
+
syncPlugins(map, () => wantedPlugins(mapOptions, monitorsManager.pollutant), {
|
|
126
|
+
wind: () => windPlugin(),
|
|
127
|
+
"hms-smoke": () =>
|
|
128
|
+
hmsSmokePlugin({
|
|
129
|
+
id: "hms-smoke",
|
|
130
|
+
source: hmsSmokeStore.dataSource,
|
|
131
|
+
layerLabel: "Smoke"
|
|
132
|
+
}),
|
|
133
|
+
"hms-fire": () =>
|
|
134
|
+
hmsFirePlugin({
|
|
135
|
+
id: "hms-fire",
|
|
136
|
+
source: hmsFireStore.groups,
|
|
137
|
+
hotspots: hmsFireStore.hotspots,
|
|
138
|
+
layerLabel: "Fires"
|
|
139
|
+
}),
|
|
140
|
+
"collocation-sites": () =>
|
|
141
|
+
collocationSitesPlugin({
|
|
142
|
+
id: "collocation-sites",
|
|
143
|
+
source: collocationSitesStore.dataSource,
|
|
144
|
+
layerLabel: "Collocation sites"
|
|
145
|
+
})
|
|
146
|
+
});
|
|
87
147
|
|
|
88
148
|
// Keep the Pollutant menu's selection (pm25 / o3 / none) and the "pollutant" URL param in
|
|
89
|
-
// sync, both directions. init() only seeds the pollutant on the
|
|
149
|
+
// sync, both directions. init() only seeds the pollutant on the store's first-ever
|
|
90
150
|
// initialization; this effect applies a later "?pollutant=" change (e.g. navigating in from
|
|
91
|
-
// elsewhere) to an already-running map.
|
|
151
|
+
// elsewhere, or back/forward) to an already-running map.
|
|
92
152
|
$effect(() => {
|
|
93
153
|
const selection = parsePollutantParam(route.search.pollutant);
|
|
94
154
|
if (!monitorsManager.initialized || !selection) return;
|
|
@@ -96,47 +156,54 @@
|
|
|
96
156
|
// state here would make a UI-driven change re-trigger this effect before the effect below
|
|
97
157
|
// syncs the URL, so it would see the stale URL and revert the user's change.
|
|
98
158
|
untrack(() => {
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
) {
|
|
102
|
-
applyPollutantSelection(selection, pollutantTargets);
|
|
159
|
+
if (currentSelection(monitorsManager.pollutant, mapOptions.monitorsOn) !== selection) {
|
|
160
|
+
applyPollutantSelection(selection, { manager: monitorsManager, options: mapOptions });
|
|
103
161
|
}
|
|
104
162
|
});
|
|
105
163
|
});
|
|
106
164
|
|
|
107
165
|
// ...and the reverse: reflect UI-driven changes back to the URL.
|
|
108
166
|
$effect(() => {
|
|
109
|
-
const selection = currentSelection(monitorsManager.pollutant,
|
|
167
|
+
const selection = currentSelection(monitorsManager.pollutant, mapOptions.monitorsOn);
|
|
110
168
|
if (selection && searchParams.get("pollutant") !== selection) {
|
|
111
169
|
searchParams.set("pollutant", selection);
|
|
112
170
|
}
|
|
113
171
|
});
|
|
114
172
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
173
|
+
onMount(() => {
|
|
174
|
+
// A failed load leaves its layer empty (map-sdk's legend shows the status); log it once.
|
|
175
|
+
for (const store of [hmsSmokeStore, hmsFireStore, collocationSitesStore]) {
|
|
176
|
+
store.init().catch(console.error);
|
|
177
|
+
}
|
|
178
|
+
// A previous instance of this layout paused polling in onDestroy; init() is a no-op by now.
|
|
179
|
+
monitorsManager.resumeAutoUpdate();
|
|
180
|
+
return map.on("featureclick", ({ pluginId, ref, lngLat }) => {
|
|
181
|
+
// A station clicked on the map or picked from the list: its popup.
|
|
182
|
+
if ((pluginId === "ev-l2" || pluginId === "ev-l3") && ref.kind === "feature") {
|
|
183
|
+
const station = evStore.station(ref.id);
|
|
184
|
+
if (station) evPopups.open(pluginId, station, lngLat);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (pluginId !== "monitors" || ref.kind !== "monitor") return;
|
|
188
|
+
navigate(`${basePath}/monitor/:id`, { params: { id: ref.id } }).catch(console.error);
|
|
189
|
+
map.easeTo({ center: lngLat, minZoom: 12 });
|
|
190
|
+
});
|
|
119
191
|
});
|
|
120
192
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (pollutant) sections.push(pollutant);
|
|
126
|
-
}
|
|
127
|
-
if (hmsFireMapIntegration.enabled && hmsSmokeMapIntegration.enabled) {
|
|
128
|
-
sections.push(...fireSmokeLegendSections(FIRE_LEGEND_CATEGORIES, SMOKE_LEGEND));
|
|
129
|
-
}
|
|
130
|
-
return sections;
|
|
193
|
+
// The selected monitor follows the route: the open panel's monitor, none once it closes.
|
|
194
|
+
$effect(() => {
|
|
195
|
+
const id = selectedId;
|
|
196
|
+
map.setSelection("monitors", id ? { kind: "monitors", ids: [id] } : null);
|
|
131
197
|
});
|
|
132
198
|
|
|
133
199
|
onDestroy(() => {
|
|
134
|
-
monitorsManager.
|
|
200
|
+
monitorsManager.pauseAutoUpdate();
|
|
201
|
+
map.destroy();
|
|
135
202
|
});
|
|
136
203
|
</script>
|
|
137
204
|
|
|
138
205
|
<MapShell
|
|
139
|
-
{
|
|
206
|
+
{map}
|
|
140
207
|
ready={monitorsManager.initialized}
|
|
141
208
|
{panelOpen}
|
|
142
209
|
knownRoutes={[`${basePath}/monitor/`]}
|
|
@@ -144,7 +211,10 @@
|
|
|
144
211
|
>
|
|
145
212
|
{#snippet loadError()}
|
|
146
213
|
{#if loadFailed}
|
|
147
|
-
<LoadError
|
|
214
|
+
<LoadError
|
|
215
|
+
message="Couldn't load air monitors."
|
|
216
|
+
onretry={() => retryMonitorsLoad(monitorsStore, loadMonitors)}
|
|
217
|
+
/>
|
|
148
218
|
{/if}
|
|
149
219
|
{/snippet}
|
|
150
220
|
{#snippet menu()}
|
|
@@ -155,16 +225,16 @@
|
|
|
155
225
|
<OptionsGroup>
|
|
156
226
|
<LayersMenu />
|
|
157
227
|
<OverlaysMenu />
|
|
158
|
-
<SettingsMenu />
|
|
228
|
+
<SettingsMenu {map} />
|
|
159
229
|
</OptionsGroup>
|
|
160
230
|
{/snippet}
|
|
161
|
-
{#snippet search()}
|
|
162
|
-
<Search />
|
|
163
|
-
{/snippet}
|
|
164
231
|
{#snippet overlays()}
|
|
165
232
|
<div class="pointer-events-none absolute bottom-4 left-4 z-10">
|
|
166
|
-
<Legend
|
|
233
|
+
<Legend {map} />
|
|
167
234
|
</div>
|
|
168
235
|
{/snippet}
|
|
236
|
+
{#snippet search()}
|
|
237
|
+
<Search {map} />
|
|
238
|
+
{/snippet}
|
|
169
239
|
{@render children()}
|
|
170
240
|
</MapShell>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { darken, readableColor } from "color2k";
|
|
3
|
+
import { NEUTRAL_LEVEL_COLOR } from "../levels";
|
|
3
4
|
|
|
4
5
|
interface DataBoxProps {
|
|
5
6
|
color: string;
|
|
@@ -8,7 +9,9 @@
|
|
|
8
9
|
value: string;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
const { color, header, subheading, value }: DataBoxProps = $props();
|
|
12
|
+
const { color: givenColor, header, subheading, value }: DataBoxProps = $props();
|
|
13
|
+
// An empty color (a level without one) would make color2k throw.
|
|
14
|
+
const color = $derived(givenColor || NEUTRAL_LEVEL_COLOR);
|
|
12
15
|
const borderColor = $derived(darken(color, 0.1));
|
|
13
16
|
const textColor = $derived(readableColor(color));
|
|
14
17
|
</script>
|
|
@@ -16,6 +16,6 @@ type $$ComponentProps = WithoutChildrenOrChild<RangeCalendarPrimitive.RootProps>
|
|
|
16
16
|
outsideMonth: boolean;
|
|
17
17
|
}]>;
|
|
18
18
|
};
|
|
19
|
-
declare const RangeCalendar: import("svelte").Component<$$ComponentProps, {}, "
|
|
19
|
+
declare const RangeCalendar: import("svelte").Component<$$ComponentProps, {}, "ref" | "value" | "placeholder">;
|
|
20
20
|
type RangeCalendar = ReturnType<typeof RangeCalendar>;
|
|
21
21
|
export default RangeCalendar;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface MonitorMapConfig {
|
|
2
|
+
maptilerKey: string;
|
|
3
|
+
nrelKey: string;
|
|
4
|
+
}
|
|
5
|
+
/** Hosts call this once at startup, before the map or the EV layer is used. */
|
|
6
|
+
export declare function configureMonitorMap(config: MonitorMapConfig): void;
|
|
7
|
+
export declare function monitorMapConfig(): MonitorMapConfig;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
let current = null;
|
|
2
|
+
/** Hosts call this once at startup, before the map or the EV layer is used. */
|
|
3
|
+
export function configureMonitorMap(config) {
|
|
4
|
+
current = { ...config };
|
|
5
|
+
}
|
|
6
|
+
export function monitorMapConfig() {
|
|
7
|
+
if (!current)
|
|
8
|
+
throw new Error("monitor-map: call configureMonitorMap() first");
|
|
9
|
+
return current;
|
|
10
|
+
}
|
package/dist/lib/core.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Light entry (`@sjvair/monitor-map/core`): what a host app needs at startup, without the map.
|
|
3
3
|
*
|
|
4
|
-
* Nothing imported here may reach @maptiler/sdk, @maptiler/weather or uplot at runtime;
|
|
4
|
+
* Nothing imported here may reach @maptiler/sdk, maplibre, @maptiler/weather or uplot at runtime;
|
|
5
5
|
* `npm run test:bundle` enforces it. Everything is re-exported from the same modules the
|
|
6
6
|
* main entry uses, so `monitorsManager` and the router context stay single instances
|
|
7
7
|
* whichever entries a host mixes.
|
|
8
8
|
*/
|
|
9
|
+
export { configureMonitorMap, monitorMapConfig, type MonitorMapConfig } from "./config.svelte.js";
|
|
9
10
|
export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter, type MonitorMapRouterApi } from "./router-context";
|
|
10
11
|
export { monitorsManager } from "./monitors/monitors.svelte.js";
|
|
11
12
|
export type { MonitorsManager, MonitorsInitOptions } from "./monitors/monitors.svelte.js";
|
|
12
|
-
export type { MonitorsDataSource, MonitorsPollutant } from "./monitors/types";
|
package/dist/lib/core.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Light entry (`@sjvair/monitor-map/core`): what a host app needs at startup, without the map.
|
|
3
3
|
*
|
|
4
|
-
* Nothing imported here may reach @maptiler/sdk, @maptiler/weather or uplot at runtime;
|
|
4
|
+
* Nothing imported here may reach @maptiler/sdk, maplibre, @maptiler/weather or uplot at runtime;
|
|
5
5
|
* `npm run test:bundle` enforces it. Everything is re-exported from the same modules the
|
|
6
6
|
* main entry uses, so `monitorsManager` and the router context stay single instances
|
|
7
7
|
* whichever entries a host mixes.
|
|
8
8
|
*/
|
|
9
|
+
export { configureMonitorMap, monitorMapConfig } from "./config.svelte.js";
|
|
9
10
|
export { ROUTER_KEY, provideMonitorMapRouter, useMonitorMapRouter } from "./router-context";
|
|
10
11
|
export { monitorsManager } from "./monitors/monitors.svelte.js";
|
|
@@ -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 "../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
|
|
|
@@ -153,7 +164,10 @@
|
|
|
153
164
|
const request = ++chartRequest;
|
|
154
165
|
chartController?.abort();
|
|
155
166
|
const controller = (chartController = new AbortController());
|
|
156
|
-
|
|
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;
|
|
157
171
|
let data: Awaited<ReturnType<typeof fetchChartData>>;
|
|
158
172
|
try {
|
|
159
173
|
data = await fetchChartData(monitor, chartManager.dateRange, pollutant, {
|
|
@@ -161,14 +175,15 @@
|
|
|
161
175
|
});
|
|
162
176
|
} catch (err) {
|
|
163
177
|
// Superseded or closed: the newer call owns `loading` and the chart, so leave both.
|
|
164
|
-
if (controller.signal.aborted) return;
|
|
165
|
-
|
|
178
|
+
if (controller.signal.aborted || request !== chartRequest) return;
|
|
179
|
+
console.error("Failed to load chart data: ", err);
|
|
180
|
+
data = [];
|
|
166
181
|
}
|
|
167
182
|
// A newer request (another range, monitor, pollutant or refresh) owns the chart now.
|
|
168
183
|
if (request !== chartRequest) return;
|
|
169
184
|
// A failed request resolves to [] (an empty range is [[], []]); a background refresh
|
|
170
185
|
// that failed keeps the chart already on screen.
|
|
171
|
-
if (
|
|
186
|
+
if (!ownsLoading && data.length === 0) return;
|
|
172
187
|
chartManager.chartData = data;
|
|
173
188
|
chartManager.loading = false;
|
|
174
189
|
}
|
|
@@ -201,8 +216,8 @@
|
|
|
201
216
|
if (!canvas) return;
|
|
202
217
|
const link = document.createElement("a");
|
|
203
218
|
const monitorName = monitor.name.split(" ").join("-");
|
|
204
|
-
const start =
|
|
205
|
-
const end =
|
|
219
|
+
const start = fileDay(chartManager.dateRange.start);
|
|
220
|
+
const end = fileDay(chartManager.dateRange.end);
|
|
206
221
|
link.download = `${monitorName}_${start}-${end}.png`;
|
|
207
222
|
link.href = canvas.toDataURL();
|
|
208
223
|
link.click();
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { DateValue } from "@internationalized/date";
|
|
2
|
+
/** ISO instants. Days are Pacific days, whatever zone the device is in. */
|
|
2
3
|
export type DateRange = {
|
|
3
4
|
start: string;
|
|
4
5
|
end: string;
|
|
5
6
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The chart's range: with no days picked, the start of yesterday through now; otherwise the
|
|
9
|
+
* whole picked days, ending now when the end day is today.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createDateRange(start?: DateValue, end?: DateValue, now?: Date): DateRange;
|
|
12
|
+
/** Whether the range ends on the current Pacific day, i.e. it can still gain entries. */
|
|
13
|
+
export declare function includesToday(range: DateRange, now?: Date): boolean;
|
|
9
14
|
/** The same range with its end moved to the present. */
|
|
10
|
-
export declare function extendToNow(range: DateRange): DateRange;
|
|
15
|
+
export declare function extendToNow(range: DateRange, now?: Date): DateRange;
|
|
16
|
+
/** The range's Pacific days for display, e.g. "Nov 3 – Nov 4, 2026". */
|
|
17
|
+
export declare function dateRangeLabel(range: DateRange): string;
|
|
18
|
+
/** An instant's Pacific day for a file name, e.g. "04.Nov.2026". */
|
|
19
|
+
export declare function fileDay(iso: string): string;
|
|
@@ -1,24 +1,60 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { addPacificDays, endOfPacificDay, startOfPacificDay } from "@sjvair/sdk/datetime/pacific";
|
|
2
|
+
/** An instant on the given calendar day in Pacific time (noon UTC is 4–5 AM Pacific). */
|
|
3
|
+
function onPacificDay(day) {
|
|
4
|
+
return new Date(Date.UTC(day.year, day.month - 1, day.day, 12));
|
|
5
|
+
}
|
|
6
|
+
function samePacificDay(a, b) {
|
|
7
|
+
return startOfPacificDay(a).getTime() === startOfPacificDay(b).getTime();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The chart's range: with no days picked, the start of yesterday through now; otherwise the
|
|
11
|
+
* whole picked days, ending now when the end day is today.
|
|
12
|
+
*/
|
|
13
|
+
export function createDateRange(start, end, now = new Date()) {
|
|
3
14
|
if (!start || !end) {
|
|
4
15
|
return {
|
|
5
|
-
start:
|
|
6
|
-
end:
|
|
16
|
+
start: startOfPacificDay(addPacificDays(now, -1)).toISOString(),
|
|
17
|
+
end: now.toISOString()
|
|
7
18
|
};
|
|
8
19
|
}
|
|
9
|
-
const
|
|
10
|
-
let endDate = new Date(end.year, end.month - 1, end.day);
|
|
11
|
-
endDate = isToday(endDate) ? new Date() : endOfDay(endDate);
|
|
20
|
+
const endDay = onPacificDay(end);
|
|
12
21
|
return {
|
|
13
|
-
start:
|
|
14
|
-
end:
|
|
22
|
+
start: startOfPacificDay(onPacificDay(start)).toISOString(),
|
|
23
|
+
end: (samePacificDay(endDay, now) ? now : endOfPacificDay(endDay)).toISOString()
|
|
15
24
|
};
|
|
16
25
|
}
|
|
17
|
-
/** Whether the range ends on the current
|
|
18
|
-
export function includesToday(range) {
|
|
19
|
-
return
|
|
26
|
+
/** Whether the range ends on the current Pacific day, i.e. it can still gain entries. */
|
|
27
|
+
export function includesToday(range, now = new Date()) {
|
|
28
|
+
return samePacificDay(new Date(range.end), now);
|
|
20
29
|
}
|
|
21
30
|
/** The same range with its end moved to the present. */
|
|
22
|
-
export function extendToNow(range) {
|
|
23
|
-
return { start: range.start, end:
|
|
31
|
+
export function extendToNow(range, now = new Date()) {
|
|
32
|
+
return { start: range.start, end: now.toISOString() };
|
|
33
|
+
}
|
|
34
|
+
const PACIFIC = "America/Los_Angeles";
|
|
35
|
+
const shortDay = new Intl.DateTimeFormat("en-US", {
|
|
36
|
+
timeZone: PACIFIC,
|
|
37
|
+
month: "short",
|
|
38
|
+
day: "numeric"
|
|
39
|
+
});
|
|
40
|
+
const shortDayYear = new Intl.DateTimeFormat("en-US", {
|
|
41
|
+
timeZone: PACIFIC,
|
|
42
|
+
month: "short",
|
|
43
|
+
day: "numeric",
|
|
44
|
+
year: "numeric"
|
|
45
|
+
});
|
|
46
|
+
const fileDayParts = new Intl.DateTimeFormat("en-US", {
|
|
47
|
+
timeZone: PACIFIC,
|
|
48
|
+
day: "2-digit",
|
|
49
|
+
month: "short",
|
|
50
|
+
year: "numeric"
|
|
51
|
+
});
|
|
52
|
+
/** The range's Pacific days for display, e.g. "Nov 3 – Nov 4, 2026". */
|
|
53
|
+
export function dateRangeLabel(range) {
|
|
54
|
+
return `${shortDay.format(new Date(range.start))} – ${shortDayYear.format(new Date(range.end))}`;
|
|
55
|
+
}
|
|
56
|
+
/** An instant's Pacific day for a file name, e.g. "04.Nov.2026". */
|
|
57
|
+
export function fileDay(iso) {
|
|
58
|
+
const parts = Object.fromEntries(fileDayParts.formatToParts(new Date(iso)).map((part) => [part.type, part.value]));
|
|
59
|
+
return `${parts.day}.${parts.month}.${parts.year}`;
|
|
24
60
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tooltipsPlugin, uPlotCursorConfig } from "./tooltip";
|
|
2
|
-
import {
|
|
2
|
+
import { chartRangeMax } from "./pacific-time";
|
|
3
3
|
const PM25_AVG_60_STROKE = "3572b0";
|
|
4
4
|
const baseXSeriesConfig = {};
|
|
5
5
|
const baseYSeriesConfig = {
|
|
@@ -46,12 +46,7 @@ export function getChartConfig(sourceName, maxDiff, width, height, colors, label
|
|
|
46
46
|
},
|
|
47
47
|
scales: {
|
|
48
48
|
x: {
|
|
49
|
-
range: (_, min, max) =>
|
|
50
|
-
const maxDate = fromUnixTime(max);
|
|
51
|
-
const now = new Date();
|
|
52
|
-
max = isToday(maxDate) && isAfter(now, maxDate) ? getUnixTime(now) : max;
|
|
53
|
-
return [min, max];
|
|
54
|
-
}
|
|
49
|
+
range: (_, min, max) => [min, chartRangeMax(max)]
|
|
55
50
|
},
|
|
56
51
|
y: {
|
|
57
52
|
range: {
|
|
@@ -96,6 +91,9 @@ function getSeriesConfigs(monitorType, stops) {
|
|
|
96
91
|
case "airgradient":
|
|
97
92
|
case "purpleair":
|
|
98
93
|
return [baseXSeriesConfig, pm25SeriesConfig(stops)];
|
|
94
|
+
// Types without a real-time series of their own (CIMIS weather stations, from SDK 5.0).
|
|
95
|
+
default:
|
|
96
|
+
return [baseXSeriesConfig, singleSeriesConfig];
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
function autograd(u, fallback, stops) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
import type { Scale } from "@sjvair/map-sdk/data";
|
|
1
2
|
/** Color stops for a chart: each level's lower bound and its hex color without the `#`. */
|
|
2
3
|
export type LevelColors = Array<[number, string]>;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
color: string;
|
|
6
|
-
}>): LevelColors;
|
|
4
|
+
/** The chart's color stops from a range scale; a level with no lower bound or color is skipped. */
|
|
5
|
+
export declare function levelColors(scale: Scale | null): LevelColors;
|