@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
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { format, fromUnixTime } from "date-fns";
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 = format(fromUnixTime(xVal), "MMM d, yyyy h:mm a");
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 { EvStationMapFeature } from "../types";
4
+ import type { EvStation } from "./types";
5
5
 
6
6
  interface Props {
7
- feature: EvStationMapFeature;
7
+ station: EvStation;
8
8
  }
9
9
 
10
- const { feature }: Props = $props();
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 = feature.properties;
14
+ const p = station;
14
15
 
15
- const connectors: string[] = JSON.parse(p.ev_connector_types || "[]");
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
+ }
@@ -0,0 +1,117 @@
1
+ import type { FeatureCollection, Point } from "geojson";
2
+ import {
3
+ encodeRef,
4
+ type DataSource,
5
+ type DataStatus,
6
+ type MapFeatureProperties
7
+ } from "@sjvair/map-sdk/data";
8
+ import { fetchEvStations } from "./ev-stations";
9
+ import type { EvStation } from "./types";
10
+
11
+ export type EvLevel = "lvl2" | "lvl3";
12
+
13
+ /** A charger on the map: `name` is the station's name; chargers have no value. */
14
+ export interface EvFeatureProperties extends MapFeatureProperties {
15
+ level: EvLevel;
16
+ }
17
+
18
+ export type FetchEvStations = (level: EvLevel, signal?: AbortSignal) => Promise<EvStation[]>;
19
+
20
+ export interface EvStore {
21
+ /** One level's chargers for its plugin; `loading` until the first `load(level)`. */
22
+ dataSource(level: EvLevel): DataSource<EvFeatureProperties>;
23
+ /**
24
+ * Fetches a level once; overlapping calls share the run. A failure sets the status to `error`
25
+ * (its `retry()` fetches again) and rejects.
26
+ */
27
+ load(level: EvLevel): Promise<void>;
28
+ /** A loaded station by id, from either level. */
29
+ station(id: string): EvStation | undefined;
30
+ }
31
+
32
+ /** NREL has this station in the wrong place, so it is left out (as in 4.x). */
33
+ const DROPPED_STATION = 226154;
34
+
35
+ function toFeatures(
36
+ stations: EvStation[],
37
+ level: EvLevel
38
+ ): FeatureCollection<Point, EvFeatureProperties> {
39
+ return {
40
+ type: "FeatureCollection",
41
+ features: stations.map((station) => ({
42
+ type: "Feature",
43
+ properties: {
44
+ refKey: encodeRef({ kind: "feature", id: String(station.id) }),
45
+ name: station.station_name ?? "",
46
+ value: null,
47
+ level
48
+ },
49
+ geometry: { type: "Point", coordinates: [station.longitude, station.latitude] }
50
+ }))
51
+ };
52
+ }
53
+
54
+ class EvLevelSource implements DataSource<EvFeatureProperties> {
55
+ status: DataStatus = $state.raw({ state: "loading" });
56
+ features: FeatureCollection<Point, EvFeatureProperties> | null = $state.raw(null);
57
+ readonly scale = null;
58
+ stations: EvStation[] = [];
59
+
60
+ #run: Promise<void> | null = null;
61
+ #loaded = false;
62
+
63
+ constructor(
64
+ private level: EvLevel,
65
+ private fetchStations: FetchEvStations
66
+ ) {}
67
+
68
+ load(): Promise<void> {
69
+ if (this.#loaded) return Promise.resolve();
70
+ this.#run ??= this.#fetch();
71
+ return this.#run;
72
+ }
73
+
74
+ async #fetch(): Promise<void> {
75
+ if (this.status.state !== "loading") this.status = { state: "loading" };
76
+ try {
77
+ const stations = (await this.fetchStations(this.level)).filter(
78
+ (station) => station.id !== DROPPED_STATION
79
+ );
80
+ this.stations = stations;
81
+ this.features = toFeatures(stations, this.level);
82
+ this.status = { state: "ready" };
83
+ this.#loaded = true;
84
+ } catch (error) {
85
+ this.features = null;
86
+ this.status = {
87
+ state: "error",
88
+ error,
89
+ retry: () => {
90
+ this.load().catch(() => {});
91
+ }
92
+ };
93
+ throw error;
94
+ } finally {
95
+ this.#run = null;
96
+ }
97
+ }
98
+ }
99
+
100
+ /** EV chargers by level, each fetched when first wanted. */
101
+ export function createEvStore(fetchStations: FetchEvStations = fetchEvStations): EvStore {
102
+ const levels = {
103
+ lvl2: new EvLevelSource("lvl2", fetchStations),
104
+ lvl3: new EvLevelSource("lvl3", fetchStations)
105
+ };
106
+ return {
107
+ dataSource: (level) => levels[level],
108
+ load: (level) => levels[level].load(),
109
+ station(id) {
110
+ for (const source of Object.values(levels)) {
111
+ const found = source.stations.find((station) => String(station.id) === id);
112
+ if (found) return found;
113
+ }
114
+ return undefined;
115
+ }
116
+ };
117
+ }
@@ -0,0 +1,64 @@
1
+ import { mount, unmount, untrack } from "svelte";
2
+ import { Popup } from "@maptiler/sdk";
3
+ import { maptilerMap, type MapContext } from "@sjvair/map-sdk";
4
+ import EvPopup from "./EvPopup.svelte";
5
+ import type { EvStation } from "./types";
6
+
7
+ export interface StationPopups {
8
+ /** Opens `station`'s popup at `lngLat`, closing any other; it belongs to plugin `pluginId`. */
9
+ open(pluginId: string, station: EvStation, lngLat: [number, number]): void;
10
+ close(): void;
11
+ }
12
+
13
+ /**
14
+ * The EV station popup, 4.x's (a MapTiler `Popup` with a close button that a map click leaves
15
+ * open). One is open at a time; it closes on a basemap switch, when its plugin leaves the map,
16
+ * and when the GL map is replaced. Call it during component setup: its effects live as long as
17
+ * the component.
18
+ */
19
+ export function stationPopups(map: MapContext): StationPopups {
20
+ let current: { popup: Popup; pluginId: string } | null = $state.raw(null);
21
+
22
+ function close() {
23
+ const open = current;
24
+ current = null;
25
+ open?.popup.remove();
26
+ }
27
+
28
+ // A basemap switch or a new GL map closes it.
29
+ $effect(() => {
30
+ void map.basemap.current;
31
+ void map.gl;
32
+ untrack(close);
33
+ });
34
+
35
+ // So does its plugin leaving the map (its level turned off). A Marker Clusters flip removes and
36
+ // re-adds the plugin in one pass, so the popup stays: the station is still there.
37
+ $effect(() => {
38
+ const ids = map.plugins.map((plugin) => plugin.id);
39
+ const open = current;
40
+ if (open && !ids.includes(open.pluginId)) untrack(close);
41
+ });
42
+
43
+ $effect(() => () => untrack(close));
44
+
45
+ return {
46
+ open(pluginId, station, lngLat) {
47
+ close();
48
+ const gl = maptilerMap(map);
49
+ if (!gl) return;
50
+ const target = document.createElement("div");
51
+ const instance = mount(EvPopup, { target, props: { station } });
52
+ const popup = new Popup({ closeButton: true, closeOnClick: false, maxWidth: "none" })
53
+ .setLngLat(lngLat)
54
+ .setDOMContent(target);
55
+ popup.on("close", () => {
56
+ unmount(instance);
57
+ if (current?.popup === popup) current = null;
58
+ });
59
+ current = { popup, pluginId };
60
+ popup.addTo(gl);
61
+ },
62
+ close
63
+ };
64
+ }
@@ -1,6 +1,3 @@
1
- import type { Feature, Geometry } from "geojson";
2
- import type { MapGeoJSONFeature } from "@maptiler/sdk";
3
-
4
1
  export interface EvStation {
5
2
  access_code: string;
6
3
  access_days_time: string;
@@ -25,34 +22,3 @@ export interface EvStation {
25
22
  ev_network: string;
26
23
  ev_pricing: string;
27
24
  }
28
-
29
- export interface EvStationMarkerProperties {
30
- icon: string;
31
- id: number;
32
- level: "lvl2" | "lvl3";
33
- station_name: string;
34
- facility_type: string;
35
- ev_network: string;
36
- station_phone: string;
37
- street_address: string;
38
- city: string;
39
- state: string;
40
- zip: string;
41
- access_days_time: string;
42
- access_detail_code: string;
43
- cards_accepted: string | null;
44
- ev_pricing: string;
45
- ev_connector_types: string; // JSON.stringify(string[])
46
- }
47
-
48
- export type EvStationMapFeature = Feature<Geometry, EvStationMarkerProperties>;
49
-
50
- export interface EvStationClusterMapFeature extends MapGeoJSONFeature {
51
- properties: {
52
- cluster: true;
53
- cluster_id: number;
54
- point_count: number;
55
- point_count_abbreviated: string | number;
56
- };
57
- // level is derived from feature.source: "ev-stations-lvl2" | "ev-stations-lvl3"
58
- }
@@ -48,7 +48,6 @@ export const SanJoaquin = [
48
48
  export const Stanislaus = [
49
49
  "95355",
50
50
  "95350",
51
- "94550",
52
51
  "95351",
53
52
  "95307",
54
53
  "95380",
@@ -80,7 +79,6 @@ export const Stanislaus = [
80
79
  ];
81
80
 
82
81
  export const Merced = [
83
- "95023",
84
82
  "95380",
85
83
  "93635",
86
84
  "95301",
@@ -101,13 +99,11 @@ export const Merced = [
101
99
  "95369",
102
100
  "95303",
103
101
  "95317",
104
- "95325",
105
102
  "93665",
106
103
  "95342",
107
104
  "95344",
108
105
  "95343",
109
106
  "93661",
110
- "09534",
111
107
  "95312"
112
108
  ];
113
109
 
@@ -141,7 +137,6 @@ export const Fresno = [
141
137
  "93706",
142
138
  "93711",
143
139
  "93705",
144
- "93245",
145
140
  "93612",
146
141
  "93657",
147
142
  "93710",
@@ -189,7 +184,6 @@ export const Fresno = [
189
184
  "93652",
190
185
  "93627",
191
186
  "93606",
192
- "93665",
193
187
  "93628",
194
188
  "93605",
195
189
  "93607",
@@ -235,8 +229,7 @@ export const Fresno = [
235
229
  "93888",
236
230
  "93844",
237
231
  "93784",
238
- "93780",
239
- "93633"
232
+ "93780"
240
233
  ];
241
234
 
242
235
  export const Kings = [
@@ -247,7 +240,6 @@ export const Kings = [
247
240
  "93204",
248
241
  "93656",
249
242
  "93242",
250
- "93249",
251
243
  "93266",
252
244
  "93201",
253
245
  "93202",
@@ -288,12 +280,9 @@ export const Tulare = [
288
280
  "93238",
289
281
  "93261",
290
282
  "93641",
291
- "93255",
292
283
  "93603",
293
284
  "93244",
294
285
  "93262",
295
- "93549",
296
- "93628",
297
286
  "93208",
298
287
  "93227",
299
288
  "93237",
@@ -412,7 +401,6 @@ export const Calaveras = [
412
401
  export const Tuolumne = [
413
402
  "95370",
414
403
  "95327",
415
- "95247",
416
404
  "95383",
417
405
  "95321",
418
406
  "95379",
@@ -423,8 +411,6 @@ export const Tuolumne = [
423
411
  "95310",
424
412
  "95346",
425
413
  "95364",
426
- "95224",
427
- "93541",
428
414
  "95305",
429
415
  "95335",
430
416
  "95314",
@@ -438,7 +424,6 @@ export const Mono = ["93514", "93546", "93517", "96107", "93529", "96133", "9354
438
424
 
439
425
  export const Mariposa = [
440
426
  "95338",
441
- "95333",
442
427
  "93601",
443
428
  "95311",
444
429
  "95329",
@@ -468,3 +453,31 @@ export const Inyo = [
468
453
  "93522",
469
454
  "93530"
470
455
  ];
456
+
457
+ /**
458
+ * Every county's ZIPs once, sorted: the EV charger query's `zip` list. A ZIP that spans two
459
+ * counties is in both lists above; `scripts/check-ev-zips.mjs` checks the lists against the
460
+ * Census ZCTA-to-county file.
461
+ *
462
+ * The county lists began as a hand-scraped list. 5.0 only removes ZIPs with no land in their
463
+ * county; it adds none. Border ZIPs with some San Joaquin land (95632 Galt, 95641 Isleton,
464
+ * 94505 Discovery Bay) are left out (decided 2026-11-07): their chargers sit mostly outside
465
+ * the county. Revisit if users ask for chargers just over the county line.
466
+ */
467
+ export const EV_ZIP_CODES: readonly string[] = [
468
+ ...new Set([
469
+ ...SanJoaquin,
470
+ ...Stanislaus,
471
+ ...Merced,
472
+ ...Madera,
473
+ ...Fresno,
474
+ ...Kings,
475
+ ...Tulare,
476
+ ...Kern,
477
+ ...Calaveras,
478
+ ...Tuolumne,
479
+ ...Mono,
480
+ ...Mariposa,
481
+ ...Inyo
482
+ ])
483
+ ].sort();