@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.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -138
  3. package/dist/lib/MonitorDetailPanel.svelte +23 -20
  4. package/dist/lib/MonitorMapLayout.svelte +147 -77
  5. package/dist/lib/components/DataBox.svelte +4 -1
  6. package/dist/lib/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
  7. package/dist/lib/config.svelte.d.ts +7 -0
  8. package/dist/lib/config.svelte.js +10 -0
  9. package/dist/lib/core.d.ts +2 -2
  10. package/dist/lib/core.js +2 -1
  11. package/dist/lib/data-chart/DataChart.svelte +40 -25
  12. package/dist/lib/data-chart/DateRange.d.ts +13 -4
  13. package/dist/lib/data-chart/DateRange.js +50 -14
  14. package/dist/lib/data-chart/chart-utils.js +5 -7
  15. package/dist/lib/data-chart/level-colors.d.ts +3 -4
  16. package/dist/lib/data-chart/level-colors.js +7 -4
  17. package/dist/lib/data-chart/pacific-time.d.ts +9 -0
  18. package/dist/lib/data-chart/pacific-time.js +25 -0
  19. package/dist/lib/data-chart/tooltip.js +2 -2
  20. package/{src/lib/ev-stations/components/EvStationTooltip.svelte → dist/lib/ev-stations/EvPopup.svelte} +6 -5
  21. package/dist/lib/ev-stations/EvPopup.svelte.d.ts +7 -0
  22. package/dist/lib/ev-stations/ev-plugin.svelte.d.ts +14 -0
  23. package/dist/lib/ev-stations/ev-plugin.svelte.js +71 -0
  24. package/dist/lib/ev-stations/ev-station-icons.d.ts +1 -4
  25. package/dist/lib/ev-stations/ev-station-icons.js +1 -13
  26. package/dist/lib/ev-stations/ev-stations.d.ts +7 -0
  27. package/dist/lib/ev-stations/ev-stations.js +53 -0
  28. package/dist/lib/ev-stations/ev-store.svelte.d.ts +21 -0
  29. package/dist/lib/ev-stations/ev-store.svelte.js +83 -0
  30. package/dist/lib/ev-stations/station-popup.svelte.d.ts +14 -0
  31. package/dist/lib/ev-stations/station-popup.svelte.js +54 -0
  32. package/dist/lib/ev-stations/types.d.ts +0 -29
  33. package/dist/lib/ev-stations/zip-codes.d.ts +11 -0
  34. package/dist/lib/ev-stations/zip-codes.js +28 -16
  35. package/dist/lib/index.d.ts +8 -56
  36. package/dist/lib/index.js +7 -46
  37. package/dist/lib/legend/Legend.svelte +18 -19
  38. package/dist/lib/legend/Legend.svelte.d.ts +2 -2
  39. package/dist/lib/levels.d.ts +7 -0
  40. package/dist/lib/levels.js +19 -0
  41. package/dist/lib/map/MapShell.svelte +17 -24
  42. package/dist/lib/map/MapShell.svelte.d.ts +4 -4
  43. package/dist/lib/map/basemaps.d.ts +25 -0
  44. package/dist/lib/map/basemaps.js +66 -0
  45. package/dist/lib/map/live-map.svelte.d.ts +3 -0
  46. package/dist/lib/map/live-map.svelte.js +22 -0
  47. package/dist/lib/map/sync-plugins.svelte.d.ts +19 -0
  48. package/dist/lib/map/sync-plugins.svelte.js +52 -0
  49. package/dist/lib/monitors/components/MonitorSubscriptionManager.svelte +66 -61
  50. package/dist/lib/monitors/filtered-source.svelte.d.ts +9 -0
  51. package/dist/lib/monitors/filtered-source.svelte.js +28 -0
  52. package/dist/lib/monitors/monitors.svelte.d.ts +25 -35
  53. package/dist/lib/monitors/monitors.svelte.js +35 -218
  54. package/dist/lib/monitors/retry-load.d.ts +8 -0
  55. package/dist/lib/monitors/retry-load.js +11 -0
  56. package/dist/lib/monitors/types.d.ts +1 -41
  57. package/dist/lib/options/map-option-defaults.d.ts +25 -36
  58. package/dist/lib/options/map-option-defaults.js +12 -30
  59. package/dist/lib/options/map-options.svelte.d.ts +13 -0
  60. package/dist/lib/options/map-options.svelte.js +26 -0
  61. package/dist/lib/options/menus/LayersMenu.svelte +8 -20
  62. package/dist/lib/options/menus/LayersMenu.svelte.d.ts +17 -2
  63. package/dist/lib/options/menus/MonitorsMenu.svelte +34 -18
  64. package/dist/lib/options/menus/OverlaysMenu.svelte +2 -2
  65. package/dist/lib/options/menus/PollutantMenu.svelte +4 -9
  66. package/dist/lib/options/menus/SettingsMenu.svelte +32 -25
  67. package/dist/lib/options/menus/SettingsMenu.svelte.d.ts +5 -16
  68. package/dist/lib/options/plugin-order.d.ts +8 -0
  69. package/dist/lib/options/plugin-order.js +36 -0
  70. package/dist/lib/options/pollutant-param.d.ts +7 -5
  71. package/dist/lib/options/pollutant-param.js +9 -5
  72. package/dist/lib/options/row-icons.d.ts +8 -0
  73. package/dist/lib/options/row-icons.js +15 -0
  74. package/dist/lib/options/rows/MarkerIcon.svelte +5 -5
  75. package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +1 -2
  76. package/dist/lib/search/Search.svelte +20 -33
  77. package/dist/lib/search/Search.svelte.d.ts +5 -1
  78. package/dist/lib/search/search-pin.svelte.d.ts +11 -0
  79. package/dist/lib/search/search-pin.svelte.js +28 -0
  80. package/dist/lib/search/service.d.ts +7 -2
  81. package/dist/lib/search/service.js +17 -10
  82. package/dist/lib/stores.svelte.d.ts +8 -0
  83. package/dist/lib/stores.svelte.js +9 -0
  84. package/dist/lib/wind/wind-plugin.d.ts +11 -0
  85. package/dist/lib/wind/wind-plugin.js +25 -0
  86. package/package.json +12 -10
  87. package/src/lib/MonitorDetailPanel.svelte +23 -20
  88. package/src/lib/MonitorMapLayout.svelte +147 -77
  89. package/src/lib/components/DataBox.svelte +4 -1
  90. package/src/lib/config.svelte.ts +16 -0
  91. package/src/lib/core.ts +2 -2
  92. package/src/lib/data-chart/DataChart.svelte +40 -25
  93. package/src/lib/data-chart/DateRange.ts +58 -15
  94. package/src/lib/data-chart/chart-utils.ts +6 -7
  95. package/src/lib/data-chart/level-colors.ts +8 -9
  96. package/src/lib/data-chart/pacific-time.ts +31 -0
  97. package/src/lib/data-chart/tooltip.ts +2 -2
  98. package/{dist/lib/ev-stations/components/EvStationTooltip.svelte → src/lib/ev-stations/EvPopup.svelte} +6 -5
  99. package/src/lib/ev-stations/ev-plugin.svelte.ts +86 -0
  100. package/src/lib/ev-stations/ev-station-icons.ts +1 -17
  101. package/src/lib/ev-stations/ev-stations.ts +72 -0
  102. package/src/lib/ev-stations/ev-store.svelte.ts +117 -0
  103. package/src/lib/ev-stations/station-popup.svelte.ts +64 -0
  104. package/src/lib/ev-stations/types.ts +0 -34
  105. package/src/lib/ev-stations/zip-codes.ts +29 -16
  106. package/src/lib/index.ts +8 -99
  107. package/src/lib/legend/Legend.svelte +18 -19
  108. package/src/lib/levels.ts +24 -0
  109. package/src/lib/map/MapShell.svelte +17 -24
  110. package/src/lib/map/basemaps.ts +76 -0
  111. package/src/lib/map/live-map.svelte.ts +24 -0
  112. package/src/lib/map/sync-plugins.svelte.ts +69 -0
  113. package/src/lib/monitors/components/MonitorSubscriptionManager.svelte +66 -61
  114. package/src/lib/monitors/filtered-source.svelte.ts +38 -0
  115. package/src/lib/monitors/monitors.svelte.ts +58 -261
  116. package/src/lib/monitors/retry-load.ts +14 -0
  117. package/src/lib/monitors/types.ts +1 -47
  118. package/src/lib/options/map-option-defaults.ts +30 -53
  119. package/src/lib/options/map-options.svelte.ts +30 -0
  120. package/src/lib/options/menus/LayersMenu.svelte +8 -20
  121. package/src/lib/options/menus/MonitorsMenu.svelte +34 -18
  122. package/src/lib/options/menus/OverlaysMenu.svelte +2 -2
  123. package/src/lib/options/menus/PollutantMenu.svelte +4 -9
  124. package/src/lib/options/menus/SettingsMenu.svelte +32 -25
  125. package/src/lib/options/plugin-order.ts +38 -0
  126. package/src/lib/options/pollutant-param.ts +14 -9
  127. package/src/lib/options/row-icons.ts +17 -0
  128. package/src/lib/options/rows/MarkerIcon.svelte +5 -5
  129. package/src/lib/search/Search.svelte +20 -33
  130. package/src/lib/search/search-pin.svelte.ts +35 -0
  131. package/src/lib/search/service.ts +34 -15
  132. package/src/lib/stores.svelte.ts +15 -0
  133. package/src/lib/wind/wind-plugin.ts +33 -0
  134. package/dist/lib/collocation-sites/CollocationTooltip.svelte +0 -98
  135. package/dist/lib/collocation-sites/CollocationTooltip.svelte.d.ts +0 -7
  136. package/dist/lib/collocation-sites/collocations-icon-manager.svelte.d.ts +0 -7
  137. package/dist/lib/collocation-sites/collocations-icon-manager.svelte.js +0 -18
  138. package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +0 -21
  139. package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +0 -113
  140. package/dist/lib/collocation-sites/collocations.svelte.d.ts +0 -10
  141. package/dist/lib/collocation-sites/collocations.svelte.js +0 -21
  142. package/dist/lib/collocation-sites/types.d.ts +0 -8
  143. package/dist/lib/collocation-sites/types.js +0 -1
  144. package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
  145. package/dist/lib/ev-stations/components/EvStationClusterTooltip.svelte.d.ts +0 -7
  146. package/dist/lib/ev-stations/components/EvStationTooltip.svelte.d.ts +0 -7
  147. package/dist/lib/ev-stations/ev-stations-cluster-renderer.d.ts +0 -19
  148. package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +0 -131
  149. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.d.ts +0 -31
  150. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +0 -226
  151. package/dist/lib/ev-stations/ev-stations.svelte.d.ts +0 -10
  152. package/dist/lib/ev-stations/ev-stations.svelte.js +0 -57
  153. package/dist/lib/hms/hms-fire-icon-manager.d.ts +0 -13
  154. package/dist/lib/hms/hms-fire-icon-manager.js +0 -39
  155. package/dist/lib/hms/hms-fire-map-integration.svelte.d.ts +0 -23
  156. package/dist/lib/hms/hms-fire-map-integration.svelte.js +0 -94
  157. package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +0 -39
  158. package/dist/lib/hms/hms-smoke-map-integration.svelte.js +0 -87
  159. package/dist/lib/hms/hms.svelte.d.ts +0 -20
  160. package/dist/lib/hms/hms.svelte.js +0 -237
  161. package/dist/lib/legend/LegendBar.svelte +0 -45
  162. package/dist/lib/legend/LegendBar.svelte.d.ts +0 -7
  163. package/dist/lib/legend/legend-data.d.ts +0 -40
  164. package/dist/lib/legend/legend-data.js +0 -65
  165. package/dist/lib/map/Map.svelte +0 -13
  166. package/dist/lib/map/Map.svelte.d.ts +0 -8
  167. package/dist/lib/map/basemap.d.ts +0 -14
  168. package/dist/lib/map/basemap.js +0 -10
  169. package/dist/lib/map/icons.d.ts +0 -7
  170. package/dist/lib/map/icons.js +0 -33
  171. package/dist/lib/map/integrations/base-layer-seperator.d.ts +0 -9
  172. package/dist/lib/map/integrations/base-layer-seperator.js +0 -11
  173. package/dist/lib/map/integrations/click-manager.d.ts +0 -14
  174. package/dist/lib/map/integrations/click-manager.js +0 -52
  175. package/dist/lib/map/integrations/integrations-manager.d.ts +0 -8
  176. package/dist/lib/map/integrations/integrations-manager.js +0 -20
  177. package/dist/lib/map/integrations/map-display-option.svelte.d.ts +0 -7
  178. package/dist/lib/map/integrations/map-display-option.svelte.js +0 -10
  179. package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +0 -32
  180. package/dist/lib/map/integrations/map-geojson-integration.svelte.js +0 -60
  181. package/dist/lib/map/integrations/map-icon-manager.d.ts +0 -15
  182. package/dist/lib/map/integrations/map-icon-manager.js +0 -52
  183. package/dist/lib/map/integrations/map-integration.svelte.d.ts +0 -6
  184. package/dist/lib/map/integrations/map-integration.svelte.js +0 -2
  185. package/dist/lib/map/integrations/map-layer-integration.svelte.d.ts +0 -11
  186. package/dist/lib/map/integrations/map-layer-integration.svelte.js +0 -40
  187. package/dist/lib/map/integrations/tooltip.svelte.d.ts +0 -36
  188. package/dist/lib/map/integrations/tooltip.svelte.js +0 -131
  189. package/dist/lib/map/integrations/types.d.ts +0 -14
  190. package/dist/lib/map/integrations/types.js +0 -1
  191. package/dist/lib/map/map-manager.svelte.d.ts +0 -20
  192. package/dist/lib/map/map-manager.svelte.js +0 -63
  193. package/dist/lib/map/map-style-state.svelte.d.ts +0 -23
  194. package/dist/lib/map/map-style-state.svelte.js +0 -86
  195. package/dist/lib/map/utils.d.ts +0 -7
  196. package/dist/lib/map/utils.js +0 -36
  197. package/dist/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
  198. package/dist/lib/monitors/components/MonitorClusterTooltip.svelte.d.ts +0 -7
  199. package/dist/lib/monitors/components/MonitorTooltip.svelte +0 -61
  200. package/dist/lib/monitors/components/MonitorTooltip.svelte.d.ts +0 -7
  201. package/dist/lib/monitors/monitor-utils.d.ts +0 -4
  202. package/dist/lib/monitors/monitor-utils.js +0 -41
  203. package/dist/lib/monitors/monitors-cluster-renderer.d.ts +0 -33
  204. package/dist/lib/monitors/monitors-cluster-renderer.js +0 -221
  205. package/dist/lib/monitors/monitors-icon-manager.svelte.d.ts +0 -15
  206. package/dist/lib/monitors/monitors-icon-manager.svelte.js +0 -78
  207. package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +0 -43
  208. package/dist/lib/monitors/monitors-map-integration.svelte.js +0 -301
  209. package/dist/lib/options/reset-map-options.d.ts +0 -2
  210. package/dist/lib/options/reset-map-options.js +0 -24
  211. package/dist/lib/reactivity.svelte.d.ts +0 -0
  212. package/dist/lib/reactivity.svelte.js +0 -83
  213. package/dist/lib/wind/wind.svelte.d.ts +0 -10
  214. package/dist/lib/wind/wind.svelte.js +0 -13
  215. package/src/lib/collocation-sites/CollocationTooltip.svelte +0 -98
  216. package/src/lib/collocation-sites/collocations-icon-manager.svelte.ts +0 -21
  217. package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +0 -136
  218. package/src/lib/collocation-sites/collocations.svelte.ts +0 -27
  219. package/src/lib/collocation-sites/types.ts +0 -10
  220. package/src/lib/ev-stations/components/EvStationClusterTooltip.svelte +0 -16
  221. package/src/lib/ev-stations/ev-stations-cluster-renderer.ts +0 -162
  222. package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +0 -248
  223. package/src/lib/ev-stations/ev-stations.svelte.ts +0 -84
  224. package/src/lib/hms/hms-fire-icon-manager.ts +0 -48
  225. package/src/lib/hms/hms-fire-map-integration.svelte.ts +0 -109
  226. package/src/lib/hms/hms-smoke-map-integration.svelte.ts +0 -105
  227. package/src/lib/hms/hms.svelte.ts +0 -270
  228. package/src/lib/legend/LegendBar.svelte +0 -45
  229. package/src/lib/legend/legend-data.ts +0 -101
  230. package/src/lib/map/Map.svelte +0 -13
  231. package/src/lib/map/basemap.ts +0 -24
  232. package/src/lib/map/icons.ts +0 -42
  233. package/src/lib/map/integrations/base-layer-seperator.ts +0 -17
  234. package/src/lib/map/integrations/click-manager.ts +0 -64
  235. package/src/lib/map/integrations/integrations-manager.ts +0 -25
  236. package/src/lib/map/integrations/map-display-option.svelte.ts +0 -13
  237. package/src/lib/map/integrations/map-geojson-integration.svelte.ts +0 -77
  238. package/src/lib/map/integrations/map-icon-manager.ts +0 -67
  239. package/src/lib/map/integrations/map-integration.svelte.ts +0 -8
  240. package/src/lib/map/integrations/map-layer-integration.svelte.ts +0 -49
  241. package/src/lib/map/integrations/tooltip.svelte.ts +0 -153
  242. package/src/lib/map/integrations/types.ts +0 -24
  243. package/src/lib/map/map-manager.svelte.ts +0 -83
  244. package/src/lib/map/map-style-state.svelte.ts +0 -94
  245. package/src/lib/map/utils.ts +0 -53
  246. package/src/lib/monitors/components/MonitorClusterTooltip.svelte +0 -112
  247. package/src/lib/monitors/components/MonitorTooltip.svelte +0 -61
  248. package/src/lib/monitors/monitor-utils.ts +0 -57
  249. package/src/lib/monitors/monitors-cluster-renderer.ts +0 -285
  250. package/src/lib/monitors/monitors-icon-manager.svelte.ts +0 -98
  251. package/src/lib/monitors/monitors-map-integration.svelte.ts +0 -318
  252. package/src/lib/options/reset-map-options.ts +0 -28
  253. package/src/lib/reactivity.svelte.ts +0 -82
  254. package/src/lib/wind/wind.svelte.ts +0 -20
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { BellCheckIcon, BellOffIcon, ChevronDownIcon } from "@lucide/svelte";
3
3
  import { subscribe, unsubscribe } from "@sjvair/sdk/account";
4
- import { monitorsManager } from "../..";
4
+ import LoadError from "../../LoadError.svelte";
5
+ import { monitorsManager } from "../monitors.svelte.js";
5
6
  import { getApiToken, loadSubscriptionLevel } from "../subscriptions";
6
7
 
7
8
  interface MonitorSubscriptionManagerProps {
@@ -22,23 +23,21 @@
22
23
  let saving = $state(false);
23
24
  let subscribedLevel: string | null = $state(null);
24
25
 
26
+ // The alert rows, in this order, from the PM2.5 scale; a key the scale lacks is skipped.
25
27
  const alertLevels = $derived.by(() => {
26
- if (!monitorsManager.meta) return [];
27
- try {
28
- return monitorsManager.meta
29
- .entryType("pm25")
30
- .asIter.levels!.filter((level) =>
31
- ALERT_LEVEL_NAMES.includes(level.name as (typeof ALERT_LEVEL_NAMES)[number])
32
- );
33
- } catch {
34
- return [];
35
- }
28
+ const levels = monitorsManager.meta?.entryType("pm25").scale?.levels ?? [];
29
+ return ALERT_LEVEL_NAMES.flatMap((key) => levels.find((level) => level.key === key) ?? []);
36
30
  });
37
31
 
32
+ let loadError = $state(false);
33
+ let attempt = $state(0);
34
+
38
35
  $effect(() => {
39
36
  const id = monitorId;
37
+ void attempt;
40
38
  open = false;
41
39
  subscribedLevel = null;
40
+ loadError = false;
42
41
  const controller = new AbortController();
43
42
  const { signal } = controller;
44
43
  loading = true;
@@ -47,7 +46,9 @@
47
46
  if (!signal.aborted) subscribedLevel = level;
48
47
  })
49
48
  .catch((error) => {
50
- if (!signal.aborted) throw error;
49
+ if (signal.aborted) return;
50
+ console.error("Could not load subscription status", error);
51
+ loadError = true;
51
52
  })
52
53
  .finally(() => {
53
54
  if (!signal.aborted) loading = false;
@@ -78,53 +79,57 @@
78
79
  }
79
80
  </script>
80
81
 
81
- <div class="relative select-none">
82
- <button
83
- class={[
84
- "flex w-full cursor-pointer items-center justify-center gap-2 rounded border px-3 py-1.5 text-sm font-semibold text-white",
85
- subscribedLevel
86
- ? "border-green-700 bg-green-600 hover:bg-green-700"
87
- : "border-blue-600 bg-blue-500 hover:bg-blue-600"
88
- ]}
89
- disabled={loading}
90
- onclick={() => (open = !open)}
91
- >
92
- {subscribedLevel ? "Manage Subscription" : "Subscribe to Alerts"}
93
- <ChevronDownIcon
94
- size={16}
95
- class={["transition-transform duration-300", open ? "rotate-180" : ""]}
96
- />
97
- </button>
82
+ {#if loadError}
83
+ <LoadError message="Couldn't load your alert settings." onretry={() => attempt++} />
84
+ {:else}
85
+ <div class="relative select-none">
86
+ <button
87
+ class={[
88
+ "flex w-full cursor-pointer items-center justify-center gap-2 rounded border px-3 py-1.5 text-sm font-semibold text-white",
89
+ subscribedLevel
90
+ ? "border-green-700 bg-green-600 hover:bg-green-700"
91
+ : "border-blue-600 bg-blue-500 hover:bg-blue-600"
92
+ ]}
93
+ disabled={loading}
94
+ onclick={() => (open = !open)}
95
+ >
96
+ {subscribedLevel ? "Manage Subscription" : "Subscribe to Alerts"}
97
+ <ChevronDownIcon
98
+ size={16}
99
+ class={["transition-transform duration-300", open ? "rotate-180" : ""]}
100
+ />
101
+ </button>
98
102
 
99
- {#if open}
100
- <button aria-label="Close" class="fixed inset-0 z-10" onclick={() => (open = false)}></button>
101
- <div class="absolute top-full left-0 z-20 mt-1 w-full overflow-hidden rounded shadow-lg">
102
- {#if subscribedLevel}
103
- <button
104
- class="flex w-full cursor-pointer items-center justify-between rounded-t border border-black/30 px-4 py-2 text-sm hover:bg-[#F5F5F3]"
105
- onclick={() => handleLevelClick(subscribedLevel)}
106
- >
107
- Unsubscribe
108
- <BellOffIcon size={14} />
109
- </button>
110
- {/if}
111
- {#each alertLevels as level (level.name)}
112
- {@const isSubscribed = level.name === subscribedLevel}
113
- <button
114
- class={[
115
- "flex w-full items-center justify-between px-4 py-2 text-sm text-white last:rounded-b",
116
- !isSubscribed ? "cursor-pointer first:rounded-t hover:brightness-90" : ""
117
- ]}
118
- style="background-color: {level.color}"
119
- disabled={saving}
120
- onclick={!isSubscribed ? () => handleLevelClick(level.name) : null}
121
- >
122
- <span>{level.label}</span>
123
- {#if isSubscribed}
124
- <BellCheckIcon size={14} />
125
- {/if}
126
- </button>
127
- {/each}
128
- </div>
129
- {/if}
130
- </div>
103
+ {#if open}
104
+ <button aria-label="Close" class="fixed inset-0 z-10" onclick={() => (open = false)}></button>
105
+ <div class="absolute top-full left-0 z-20 mt-1 w-full overflow-hidden rounded shadow-lg">
106
+ {#if subscribedLevel}
107
+ <button
108
+ class="flex w-full cursor-pointer items-center justify-between rounded-t border border-black/30 px-4 py-2 text-sm hover:bg-[#F5F5F3]"
109
+ onclick={() => handleLevelClick(subscribedLevel)}
110
+ >
111
+ Unsubscribe
112
+ <BellOffIcon size={14} />
113
+ </button>
114
+ {/if}
115
+ {#each alertLevels as level (level.key)}
116
+ {@const isSubscribed = level.key === subscribedLevel}
117
+ <button
118
+ class={[
119
+ "flex w-full items-center justify-between px-4 py-2 text-sm text-white last:rounded-b",
120
+ !isSubscribed ? "cursor-pointer first:rounded-t hover:brightness-90" : ""
121
+ ]}
122
+ style="background-color: {level.color}"
123
+ disabled={saving}
124
+ onclick={!isSubscribed ? () => handleLevelClick(level.key) : null}
125
+ >
126
+ <span>{level.label}</span>
127
+ {#if isSubscribed}
128
+ <BellCheckIcon size={14} />
129
+ {/if}
130
+ </button>
131
+ {/each}
132
+ </div>
133
+ {/if}
134
+ </div>
135
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { DataSource, MonitorFeatureProperties } from "@sjvair/map-sdk/data";
2
+ import type { MapOptions } from "../options/map-option-defaults";
3
+ type Options = Pick<MapOptions, "monitorTypes" | "inactive" | "inside">;
4
+ /**
5
+ * `source` with only the monitors the Monitors menu shows: types that are on, inactive and
6
+ * inside ones only when those are on. Clusters are built from these, so their averages follow.
7
+ */
8
+ export declare function filteredMonitors(source: DataSource<MonitorFeatureProperties>, options: Options): DataSource<MonitorFeatureProperties>;
9
+ export {};
@@ -0,0 +1,28 @@
1
+ function shown(props, options) {
2
+ return (options.monitorTypes[props.type] === true &&
3
+ (props.isActive || options.inactive) &&
4
+ (props.location !== "inside" || options.inside));
5
+ }
6
+ /**
7
+ * `source` with only the monitors the Monitors menu shows: types that are on, inactive and
8
+ * inside ones only when those are on. Clusters are built from these, so their averages follow.
9
+ */
10
+ export function filteredMonitors(source, options) {
11
+ const features = $derived.by(() => {
12
+ const all = source.features;
13
+ if (!all)
14
+ return null;
15
+ return { ...all, features: all.features.filter((f) => shown(f.properties, options)) };
16
+ });
17
+ return {
18
+ get status() {
19
+ return source.status;
20
+ },
21
+ get features() {
22
+ return features;
23
+ },
24
+ get scale() {
25
+ return source.scale;
26
+ }
27
+ };
28
+ }
@@ -1,8 +1,5 @@
1
- import { type MonitorData, type MonitorLatestType, type MonitorsMeta, type SJVAirEntryLevel } from "@sjvair/sdk";
2
- import { XMap } from "@tstk/builtin-extensions";
3
- import { Interval } from "@tstk/utils";
4
- import { type SignalOptions } from "../abort";
5
- import type { MonitorsDataSource } from "./types";
1
+ import type { MonitorData, MonitorsMeta } from "@sjvair/sdk";
2
+ import type { MonitorsStore } from "@sjvair/map-sdk/stores";
6
3
  export interface MonitorsInitOptions {
7
4
  /**
8
5
  * Start the 2-minute polling once the initial load finishes. Defaults to true; pass false to
@@ -12,41 +9,34 @@ export interface MonitorsInitOptions {
12
9
  /**
13
10
  * Stops waiting: the returned promise rejects with the signal's reason. The shared run keeps
14
11
  * going for the other calls waiting on it, and is cancelled only when every call waiting on it
15
- * passed a signal and all of those aborted. An aborted call's pollutant and `autoUpdate` stay
16
- * recorded. Ignored once initialized.
12
+ * passed a signal and all of those aborted. Ignored once initialized.
17
13
  */
18
14
  signal?: AbortSignal;
19
15
  }
20
- declare class MonitorsManager implements MonitorsDataSource {
21
- #private;
22
- autoUpdate: Interval;
23
- initialized: boolean;
24
- latest: XMap<string, MonitorLatestType<"pm25" | "o3">> | null;
25
- list: Array<MonitorData> | null;
26
- meta: MonitorsMeta | null;
27
- pollutant: "pm25" | "o3" | null;
28
- levels: Array<SJVAirEntryLevel> | null;
29
- constructor();
16
+ /**
17
+ * The live monitors data, as hosts have used it since 4.x: a thin view over map-sdk's monitors
18
+ * store, which owns the data, the shared init and the polling.
19
+ */
20
+ export interface MonitorsManager {
21
+ /** Loads the monitors. Only "pm25" and "o3" pass through; anything else means the default. */
22
+ init(pollutant?: unknown, options?: MonitorsInitOptions): Promise<void>;
23
+ readonly initialized: boolean;
24
+ readonly meta: MonitorsMeta | null;
30
25
  /**
31
- * Safe to call from several places: overlapping calls share one run, and a failed run can
32
- * be retried. An explicit "pm25"/"o3" wins over a call without one, whichever came first;
33
- * between two explicit values the most recent wins. Polling starts when the run finishes
34
- * unless every call sharing it passed `autoUpdate: false`. A no-op once initialized, so it
35
- * never restarts polling either: that is `resumeAutoUpdate()`.
36
- */
37
- init(urlPollutant?: string | number | boolean, options?: MonitorsInitOptions): Promise<void>;
38
- /**
39
- * Starts polling when it is not running: after something stopped it (MonitorMapLayout stops
40
- * it on destroy) or after an init that opted out of it. Refreshes first since the data may
41
- * be stale. A no-op before init or while polling.
26
+ * Every monitor, or null until initialized. The same array between reads until the store's
27
+ * monitors are replaced.
42
28
  */
29
+ readonly list: MonitorData[] | null;
30
+ /** The pollutant the readings are for, or null before init. */
31
+ readonly pollutant: string | null;
32
+ setPollutant(pollutant: "pm25" | "o3"): void;
33
+ update(options?: {
34
+ signal?: AbortSignal;
35
+ }): Promise<void>;
43
36
  resumeAutoUpdate(): void;
44
- /**
45
- * Refreshes the list and the latest readings. Starting an update aborts the previous
46
- * in-flight one, which then resolves without touching state. Pass `signal` to cancel from the
47
- * outside: that rejects with the signal's reason.
48
- */
49
- update(options?: SignalOptions): Promise<void>;
37
+ pauseAutoUpdate(): void;
38
+ /** The store underneath, for map-sdk plugins and stores. */
39
+ readonly store: MonitorsStore;
50
40
  }
41
+ export declare function createMonitorsManager(store: MonitorsStore): MonitorsManager;
51
42
  export declare const monitorsManager: MonitorsManager;
52
- export type { MonitorsManager };
@@ -1,220 +1,37 @@
1
- import { getMonitors, getMonitorsList, getMonitorsMeta } from "@sjvair/sdk";
2
- import { XMap } from "@tstk/builtin-extensions";
3
- import { Interval } from "@tstk/utils";
4
- import { untrack } from "svelte";
5
- import { anySignal, untilAborted } from "../abort";
6
- class MonitorsManager {
7
- autoUpdate = new Interval(async () => await this.update(), 2 * 60 * 1000);
8
- initialized = $state(false);
9
- latest = $state(null);
10
- list = $state(null);
11
- meta = $state(null);
12
- pollutant = $state(null);
13
- levels = $derived(this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null);
14
- #initRun = null;
15
- #requestedPollutant = null;
16
- #requestedAutoUpdate = false;
17
- #updateController = null;
18
- constructor() {
19
- // Refetch monitors when the pollutant changes after the initial load, wherever the change
20
- // comes from (Pollutant menu, URL sync, a host's own UI). Clearing first blanks the markers
21
- // instead of briefly coloring them with the previous pollutant's readings.
22
- let fetchedPollutant = null;
23
- $effect.root(() => {
24
- $effect(() => {
25
- const pollutant = this.pollutant;
26
- if (!this.initialized)
27
- return;
28
- if (fetchedPollutant !== null && fetchedPollutant !== pollutant) {
29
- untrack(() => {
30
- this.list = [];
31
- this.latest = null;
32
- this.update();
33
- });
34
- }
35
- fetchedPollutant = pollutant;
36
- });
37
- });
38
- }
39
- /**
40
- * Safe to call from several places: overlapping calls share one run, and a failed run can
41
- * be retried. An explicit "pm25"/"o3" wins over a call without one, whichever came first;
42
- * between two explicit values the most recent wins. Polling starts when the run finishes
43
- * unless every call sharing it passed `autoUpdate: false`. A no-op once initialized, so it
44
- * never restarts polling either: that is `resumeAutoUpdate()`.
45
- */
46
- init(urlPollutant, options = {}) {
47
- if (this.initialized)
48
- return Promise.resolve();
49
- const { signal } = options;
50
- // Nothing is recorded or started for a call that is already cancelled.
51
- if (signal?.aborted)
52
- return Promise.reject(signal.reason);
53
- if (options.autoUpdate !== false)
54
- this.#requestedAutoUpdate = true;
55
- if (urlPollutant === "pm25" || urlPollutant === "o3") {
56
- this.#requestedPollutant = urlPollutant;
57
- }
58
- const run = (this.#initRun ??= this.#startInitRun());
59
- if (!signal) {
60
- run.unsignalled = true;
61
- return run.promise;
62
- }
63
- run.signalled++;
64
- const onAbort = () => {
65
- run.aborted++;
66
- if (!run.unsignalled && run.aborted === run.signalled)
67
- this.#abortInitRun(run);
68
- };
69
- signal.addEventListener("abort", onAbort, { once: true });
70
- const cleanup = () => signal.removeEventListener("abort", onAbort);
71
- run.promise.then(cleanup, cleanup);
72
- return untilAborted(run.promise, signal);
73
- }
74
- #startInitRun() {
75
- const controller = new AbortController();
76
- const run = {
77
- promise: this.#runInit(controller.signal).finally(() => {
78
- if (this.#initRun === run)
79
- this.#clearInitRun();
80
- }),
81
- controller,
82
- signalled: 0,
83
- aborted: 0,
84
- unsignalled: false
85
- };
86
- return run;
87
- }
88
- /**
89
- * Cancels a run nobody is waiting on. It is detached at once, so a call arriving before its
90
- * rejection settles starts a fresh run instead of joining the cancelled one.
91
- */
92
- #abortInitRun(run) {
93
- if (this.#initRun !== run)
94
- return;
95
- this.#clearInitRun();
96
- run.controller.abort();
97
- }
98
- #clearInitRun() {
99
- this.#initRun = null;
100
- this.#requestedPollutant = null;
101
- this.#requestedAutoUpdate = false;
102
- }
103
- async #runInit(signal) {
104
- // The pollutant the latest readings were requested for.
105
- let fetchedPollutant = this.#requestedPollutant;
106
- const metaRequest = getMonitorsMeta({ signal });
107
- const listAndMeta = Promise.all([getMonitorsList({ signal }), metaRequest]).then(([list, meta]) => {
108
- if (signal.aborted)
109
- throw signal.reason;
110
- this.list = list;
111
- this.meta = meta;
112
- return meta;
113
- });
114
- // The latest readings only need the pollutant, so they never wait for the list. An explicit
115
- // pollutant may still arrive before meta does, in which case the default is never fetched.
116
- const latestRequest = fetchedPollutant
117
- ? getMonitorsLatestMap(fetchedPollutant, signal)
118
- : metaRequest.then((meta) => {
119
- const pollutant = this.#requestedPollutant ?? meta.default_pollutant;
120
- fetchedPollutant = pollutant;
121
- return getMonitorsLatestMap(pollutant, signal);
122
- });
123
- // One Promise.all so a failure in either never leaves the other unhandled.
124
- const [meta, firstLatest] = await Promise.all([listAndMeta, latestRequest]);
125
- let latest = firstLatest;
126
- // An explicit pollutant may have arrived while the requests above were in flight.
127
- let wanted = this.#requestedPollutant ?? meta.default_pollutant;
128
- while (wanted !== fetchedPollutant) {
129
- latest = await getMonitorsLatestMap(wanted, signal);
130
- fetchedPollutant = wanted;
131
- wanted = this.#requestedPollutant ?? meta.default_pollutant;
132
- }
133
- // Cancelled while a request that ignores the signal was settling: leave state to the next run.
134
- if (signal.aborted)
135
- throw signal.reason;
136
- this.pollutant = fetchedPollutant;
137
- this.latest = latest;
138
- if (this.#requestedAutoUpdate)
139
- this.autoUpdate.start();
140
- this.initialized = true;
141
- }
142
- /**
143
- * Starts polling when it is not running: after something stopped it (MonitorMapLayout stops
144
- * it on destroy) or after an init that opted out of it. Refreshes first since the data may
145
- * be stale. A no-op before init or while polling.
146
- */
147
- resumeAutoUpdate() {
148
- if (!this.initialized || this.autoUpdate.isActive)
149
- return;
150
- this.update().catch(console.error);
151
- this.autoUpdate.start();
152
- }
153
- /**
154
- * Refreshes the list and the latest readings. Starting an update aborts the previous
155
- * in-flight one, which then resolves without touching state. Pass `signal` to cancel from the
156
- * outside: that rejects with the signal's reason.
157
- */
158
- async update(options = {}) {
159
- if (!this.initialized)
160
- return;
161
- // Nothing has started yet, so an in-flight update is left alone.
162
- if (options.signal?.aborted)
163
- throw options.signal.reason;
164
- const supersededInFlight = this.#updateController !== null;
165
- this.#updateController?.abort();
166
- const controller = new AbortController();
167
- this.#updateController = controller;
168
- const combined = anySignal(controller.signal, options.signal);
169
- // The caller cancelled this update. If it is still the latest and it cut off an update that
170
- // was in flight, that update's data is lost too, so refresh again without a signal.
171
- const callerAborted = (signal) => {
172
- if (supersededInFlight && this.#updateController === controller) {
173
- this.update().catch(console.error);
174
- }
175
- return signal.reason;
176
- };
177
- // Set once callerAborted has run, so the catch below does not run it a second time.
178
- let abortedError;
179
- try {
180
- const pollutant = this.pollutant;
181
- const [list, latest] = await Promise.all([
182
- getMonitorsList({ signal: combined.signal }),
183
- getMonitorsLatestMap(pollutant || this.meta?.default_pollutant || "pm25", combined.signal)
184
- ]);
185
- // Superseded or cancelled while the requests were settling.
186
- if (options.signal?.aborted)
187
- throw (abortedError = callerAborted(options.signal));
188
- if (controller.signal.aborted)
189
- return;
190
- // The pollutant changed while this was in flight: these readings belong to the previous
191
- // one, and the change has already started its own update.
192
- if (this.pollutant !== pollutant)
193
- return;
194
- this.list = list;
195
- this.latest = latest;
196
- }
197
- catch (error) {
198
- // The caller's own abort rejects with its reason. A superseded update resolves quietly.
199
- if (options.signal?.aborted)
200
- throw error === abortedError ? error : callerAborted(options.signal);
201
- if (controller.signal.aborted)
202
- return;
203
- throw error;
204
- }
205
- finally {
206
- combined.dispose();
207
- if (this.#updateController === controller)
208
- this.#updateController = null;
209
- }
210
- }
1
+ import { monitorsStore } from "../stores.svelte.js";
2
+ function initPollutant(pollutant) {
3
+ return pollutant === "pm25" || pollutant === "o3" ? pollutant : undefined;
211
4
  }
212
- async function getMonitorsLatestMap(pollutant, signal) {
213
- const monitors = await getMonitors(pollutant, undefined, { signal });
214
- const latest = new XMap();
215
- for (const monitor of monitors) {
216
- latest.set(monitor.id, monitor);
217
- }
218
- return latest;
5
+ export function createMonitorsManager(store) {
6
+ // The list built from `store.monitors`, which the store replaces rather than mutates.
7
+ let listFrom = null;
8
+ let list = [];
9
+ return {
10
+ init: (pollutant, options) => store.init(initPollutant(pollutant), options),
11
+ get initialized() {
12
+ return store.initialized;
13
+ },
14
+ get meta() {
15
+ return store.meta;
16
+ },
17
+ get list() {
18
+ if (!store.initialized)
19
+ return null;
20
+ const monitors = store.monitors;
21
+ if (monitors !== listFrom) {
22
+ listFrom = monitors;
23
+ list = [...monitors.values()];
24
+ }
25
+ return list;
26
+ },
27
+ get pollutant() {
28
+ return store.entryType;
29
+ },
30
+ setPollutant: (pollutant) => store.entryTypeFor(pollutant),
31
+ update: (options) => store.update(options),
32
+ resumeAutoUpdate: () => store.resumeAutoUpdate(),
33
+ pauseAutoUpdate: () => store.pauseAutoUpdate(),
34
+ store
35
+ };
219
36
  }
220
- export const monitorsManager = new MonitorsManager();
37
+ export const monitorsManager = createMonitorsManager(monitorsStore);
@@ -0,0 +1,8 @@
1
+ import type { monitorsStore } from "../stores.svelte.js";
2
+ /**
3
+ * Retries a failed initial monitors load through the store's own `status.retry()`, which repeats
4
+ * the failed `init()` call. When the status is not an error (nothing to retry), runs `fallback`.
5
+ */
6
+ export declare function retryMonitorsLoad(store: {
7
+ dataSource: Pick<typeof monitorsStore.dataSource, "status">;
8
+ }, fallback: () => void): void;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Retries a failed initial monitors load through the store's own `status.retry()`, which repeats
3
+ * the failed `init()` call. When the status is not an error (nothing to retry), runs `fallback`.
4
+ */
5
+ export function retryMonitorsLoad(store, fallback) {
6
+ const { status } = store.dataSource;
7
+ if (status.state === "error")
8
+ status.retry();
9
+ else
10
+ fallback();
11
+ }
@@ -1,42 +1,2 @@
1
- import type { Feature, Geometry } from "geojson";
2
- import type { MonitorLatestType, MonitorsMeta, MonitorType, SJVAirEntryLevel } from "@sjvair/sdk";
3
- import type { MapGeoJSONFeature } from "@maptiler/sdk";
4
- import type { XMap } from "@tstk/builtin-extensions";
5
- export type MonitorMapFeature = Feature<Geometry, MonitorMarkerProperties>;
6
- export interface MonitorClusterMapFeature extends MapGeoJSONFeature {
7
- properties: MonitorClusterMarkerProperties;
8
- }
9
- export interface MonitorMarkerProperties {
10
- icon: string;
11
- id: string;
12
- is_active: boolean;
13
- is_sjvair?: boolean;
14
- location: string;
15
- name: string;
16
- order: number;
17
- type: MonitorType;
18
- value: string;
19
- }
20
- export interface MonitorClusterMarkerProperties {
21
- cluster: true;
22
- cluster_id: number;
23
- point_count: number;
24
- point_count_abbreviated: string | number;
25
- sumValues: number;
26
- countValues: number;
27
- }
28
- /** The pollutants monitorsMapIntegration/monitorsIconManager can render. */
1
+ /** The pollutants the map can show. */
29
2
  export type MonitorsPollutant = "pm25" | "o3";
30
- /**
31
- * The reactive data a monitors map display needs: which monitors exist, their
32
- * per-monitor "latest" value for the active pollutant, and the entry-level metadata
33
- * used to color icons. `monitorsManager` (live, auto-polling) is the default
34
- * implementation; a host app can supply its own (e.g. one backed by historical
35
- * summaries instead of live readings) to reuse the map rendering with different data.
36
- */
37
- export interface MonitorsDataSource {
38
- readonly meta: MonitorsMeta | null;
39
- readonly pollutant: MonitorsPollutant | null;
40
- readonly latest: XMap<string, MonitorLatestType<MonitorsPollutant>> | null;
41
- readonly levels: Array<SJVAirEntryLevel> | null;
42
- }