@sjvair/monitor-map 3.0.1 → 3.0.3

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 (151) hide show
  1. package/dist/lib/LoadScreen.js +15 -15
  2. package/dist/lib/{src/MapView.js → MainMapView.js} +2 -2
  3. package/dist/lib/MapLegend.js +13 -0
  4. package/dist/lib/MonitorDetailPanel.js +169 -0
  5. package/dist/lib/MonitorMapLayout.js +91 -0
  6. package/dist/lib/_virtual/_rolldown/runtime.js +11 -0
  7. package/dist/lib/assets/logos/airgradient-blue.js +4 -0
  8. package/dist/lib/assets/logos/airnow-compact.js +4 -0
  9. package/dist/lib/assets/logos/carb-compact.js +4 -0
  10. package/dist/lib/assets/logos/purpleair-compact.js +4 -0
  11. package/dist/lib/assets/logos/sjvair.js +4 -0
  12. package/dist/lib/collocation-sites/CollocationTooltip.js +1 -1
  13. package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +20 -20
  14. package/dist/lib/components/DataBox.js +1 -1
  15. package/dist/lib/components/DisplayOption.js +1 -1
  16. package/dist/lib/components/MapLayersDisplayOptions.js +47 -31
  17. package/dist/lib/components/SegmentedControl.js +4 -4
  18. package/dist/lib/components/ToggleSwitch.js +1 -1
  19. package/dist/lib/components/ui/button/button.js +4 -4
  20. package/dist/lib/components/ui/range-calendar/range-calendar-caption.js +57 -54
  21. package/dist/lib/components/ui/range-calendar/range-calendar-cell.js +19 -18
  22. package/dist/lib/components/ui/range-calendar/range-calendar-day.js +19 -18
  23. package/dist/lib/components/ui/range-calendar/range-calendar-grid-body.js +17 -16
  24. package/dist/lib/components/ui/range-calendar/range-calendar-grid-head.js +17 -16
  25. package/dist/lib/components/ui/range-calendar/range-calendar-grid-row.js +19 -18
  26. package/dist/lib/components/ui/range-calendar/range-calendar-grid.js +19 -18
  27. package/dist/lib/components/ui/range-calendar/range-calendar-head-cell.js +19 -18
  28. package/dist/lib/components/ui/range-calendar/range-calendar-header.js +19 -18
  29. package/dist/lib/components/ui/range-calendar/range-calendar-month-select.js +31 -31
  30. package/dist/lib/components/ui/range-calendar/range-calendar-month.js +19 -19
  31. package/dist/lib/components/ui/range-calendar/range-calendar-months.js +19 -19
  32. package/dist/lib/components/ui/range-calendar/range-calendar-nav.js +19 -19
  33. package/dist/lib/components/ui/range-calendar/range-calendar-next-button.js +27 -26
  34. package/dist/lib/components/ui/range-calendar/range-calendar-prev-button.js +27 -26
  35. package/dist/lib/components/ui/range-calendar/range-calendar-year-select.js +29 -29
  36. package/dist/lib/components/ui/range-calendar/range-calendar.js +154 -154
  37. package/dist/lib/data-chart/DataChart.js +76 -76
  38. package/dist/lib/ev-stations/components/EvStationClusterTooltip.js +16 -0
  39. package/dist/lib/ev-stations/components/EvStationTooltip.js +120 -0
  40. package/dist/lib/ev-stations/components/EvStationsDisplayOptions.js +41 -0
  41. package/dist/lib/ev-stations/data-maps.js +4 -0
  42. package/dist/lib/ev-stations/ev-station-icons.js +32 -0
  43. package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +118 -0
  44. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +181 -0
  45. package/dist/lib/ev-stations/ev-stations.svelte.js +33 -0
  46. package/dist/lib/ev-stations/zip-codes.js +136 -0
  47. package/dist/lib/hms/components/HMSFireLegend.js +35 -0
  48. package/dist/lib/hms/hms.svelte.js +12 -12
  49. package/dist/lib/index.js +19 -17
  50. package/dist/lib/map/Map.js +1 -1
  51. package/dist/lib/map/MapStyleDisplayOptions.js +16 -16
  52. package/dist/lib/map/Menu.js +23 -24
  53. package/dist/lib/map/integrations/map-display-option.svelte.js +3 -2
  54. package/dist/lib/map/integrations/map-icon-manager.js +4 -1
  55. package/dist/lib/map/utils.js +16 -13
  56. package/dist/lib/monitors/components/MonitorClusterTooltip.js +4 -4
  57. package/dist/lib/monitors/components/MonitorMarkersLegend.js +35 -0
  58. package/dist/lib/monitors/components/MonitorSubscriptionManager.js +1 -1
  59. package/dist/lib/monitors/components/MonitorTooltip.js +1 -1
  60. package/dist/lib/monitors/components/MonitorWidgetModal.js +1 -1
  61. package/dist/lib/monitors/components/MonitorsDisplayOptions.js +48 -39
  62. package/dist/lib/monitors/monitors-icon-manager.svelte.js +24 -17
  63. package/dist/lib/monitors/monitors-map-integration.svelte.js +65 -59
  64. package/dist/lib/monitors/monitors.svelte.js +38 -42
  65. package/dist/lib/router-context.js +13 -0
  66. package/dist/lib/routes.js +10 -0
  67. package/dist/lib/search/Search.js +124 -0
  68. package/dist/lib/search/service.js +90 -0
  69. package/dist/lib/sjvair-air.webp +0 -0
  70. package/dist/lib/sjvair-air2.webp +0 -0
  71. package/dist/lib/sjvair-icon.avif +0 -0
  72. package/dist/lib/sjvair-icon.png +0 -0
  73. package/dist/lib/sjvair-icon2.avif +0 -0
  74. package/dist/lib/sjvair-icon2.png +0 -0
  75. package/dist/lib/sjvair-sjv.avif +0 -0
  76. package/dist/lib/sjvair-sjv.webp +0 -0
  77. package/dist/lib/sjvair-sjv2.avif +0 -0
  78. package/dist/lib/sjvair-sjv2.webp +0 -0
  79. package/dist/lib/utils.js +9 -3
  80. package/package.json +37 -36
  81. package/src/lib/LoadScreen.svelte +5 -5
  82. package/src/lib/MainMapView.svelte +1 -0
  83. package/src/lib/MapLegend.svelte +15 -0
  84. package/src/lib/MonitorDetailPanel.svelte +152 -0
  85. package/src/lib/MonitorMapLayout.svelte +193 -0
  86. package/src/lib/components/MapLayersDisplayOptions.svelte +27 -10
  87. package/src/lib/ev-stations/components/EvStationsDisplayOptions.svelte +14 -2
  88. package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +4 -4
  89. package/src/lib/hms/components/HMSFireLegend.svelte +21 -0
  90. package/src/lib/index.ts +34 -0
  91. package/src/lib/map/Map.svelte +1 -0
  92. package/src/lib/map/Menu.svelte +4 -7
  93. package/src/lib/map/integrations/map-display-option.svelte.ts +5 -1
  94. package/src/lib/map/integrations/map-icon-manager.ts +7 -3
  95. package/src/lib/monitors/components/MonitorsDisplayOptions.svelte +4 -1
  96. package/src/lib/monitors/monitors-icon-manager.svelte.ts +3 -1
  97. package/src/lib/monitors/monitors-map-integration.svelte.ts +9 -9
  98. package/src/lib/monitors/monitors.svelte.ts +1 -3
  99. package/src/lib/router-context.ts +48 -0
  100. package/src/lib/routes.ts +8 -0
  101. package/src/lib/search/Search.svelte +189 -0
  102. package/src/lib/search/service.ts +101 -0
  103. package/src/lib/utils.ts +11 -0
  104. package/dist/lib/assets/icons/adjustment-horizontal.js +0 -4
  105. package/dist/lib/node_modules/@internationalized/date/dist/private/CalendarDate.js +0 -122
  106. package/dist/lib/node_modules/@internationalized/date/dist/private/DateFormatter.js +0 -84
  107. package/dist/lib/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +0 -91
  108. package/dist/lib/node_modules/@internationalized/date/dist/private/conversion.js +0 -104
  109. package/dist/lib/node_modules/@internationalized/date/dist/private/manipulation.js +0 -159
  110. package/dist/lib/node_modules/@internationalized/date/dist/private/queries.js +0 -32
  111. package/dist/lib/node_modules/@internationalized/date/dist/private/string.js +0 -37
  112. package/dist/lib/node_modules/@internationalized/date/dist/private/utils.js +0 -6
  113. package/dist/lib/node_modules/clsx/dist/clsx.js +0 -16
  114. package/dist/lib/node_modules/tailwind-merge/dist/bundle-mjs.js +0 -1723
  115. package/dist/lib/node_modules/tailwind-variants/dist/chunk-LQJYWU4O.js +0 -70
  116. package/dist/lib/node_modules/tailwind-variants/dist/chunk-RZF76H2U.js +0 -166
  117. package/dist/lib/node_modules/tailwind-variants/dist/index.js +0 -19
  118. package/dist/lib/node_modules/uplot/dist/uPlot.esm.js +0 -2147
  119. package/dist/lib/src/MonitorDetailPanel.js +0 -169
  120. package/dist/lib/src/router.js +0 -9
  121. package/dist/monitor-map/MonitorDetailPanel.css +0 -1
  122. package/dist/monitor-map/assets/MonitorDetailPanel-DmN3xPui.js +0 -15
  123. package/dist/monitor-map/assets/validate-routes-CgbRa8fT.js +0 -1
  124. package/dist/monitor-map/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  125. package/dist/monitor-map/inter-cyrillic-wght-normal.woff2 +0 -0
  126. package/dist/monitor-map/inter-greek-ext-wght-normal.woff2 +0 -0
  127. package/dist/monitor-map/inter-greek-wght-normal.woff2 +0 -0
  128. package/dist/monitor-map/inter-latin-ext-wght-normal.woff2 +0 -0
  129. package/dist/monitor-map/inter-latin-wght-normal.woff2 +0 -0
  130. package/dist/monitor-map/inter-vietnamese-wght-normal.woff2 +0 -0
  131. package/dist/monitor-map/lungs.svg +0 -99
  132. package/dist/monitor-map/sjvair-air.avif +0 -0
  133. package/dist/monitor-map/sjvair-air.png +0 -0
  134. package/dist/monitor-map/sjvair-air.webp +0 -0
  135. package/dist/monitor-map/sjvair-air2.avif +0 -0
  136. package/dist/monitor-map/sjvair-air2.png +0 -0
  137. package/dist/monitor-map/sjvair-air2.webp +0 -0
  138. package/dist/monitor-map/sjvair-icon.avif +0 -0
  139. package/dist/monitor-map/sjvair-icon.png +0 -0
  140. package/dist/monitor-map/sjvair-icon.webp +0 -0
  141. package/dist/monitor-map/sjvair-icon2.avif +0 -0
  142. package/dist/monitor-map/sjvair-icon2.png +0 -0
  143. package/dist/monitor-map/sjvair-icon2.webp +0 -0
  144. package/dist/monitor-map/sjvair-sjv.avif +0 -0
  145. package/dist/monitor-map/sjvair-sjv.png +0 -0
  146. package/dist/monitor-map/sjvair-sjv.webp +0 -0
  147. package/dist/monitor-map/sjvair-sjv2.avif +0 -0
  148. package/dist/monitor-map/sjvair-sjv2.png +0 -0
  149. package/dist/monitor-map/sjvair-sjv2.webp +0 -0
  150. package/dist/monitor-map/sjvairMonitorMap.css +0 -1
  151. package/dist/monitor-map/sjvairMonitorMap.js +0 -4042
@@ -0,0 +1,152 @@
1
+ <script lang="ts">
2
+ import type { ComponentProps } from "svelte";
3
+ import type { SJVAirEntryLevel } from "@sjvair/sdk";
4
+ import DataBox from "$lib/components/DataBox.svelte";
5
+ import MonitorWidgetModal from "$lib/monitors/components/MonitorWidgetModal.svelte";
6
+ import MonitorSubscriptionManager from "$lib/monitors/components/MonitorSubscriptionManager.svelte";
7
+ import DataChart from "$lib/data-chart/DataChart.svelte";
8
+ import { monitorsManager } from "$lib";
9
+ import { useMonitorMapRouter } from "./router-context";
10
+ import { fetchTempByCoords } from "$lib/weather.ts";
11
+ import { Colors, valueToColor } from "$lib/colors";
12
+ import { getCurrentLevel } from "$lib/monitors/monitor-utils.ts";
13
+
14
+ type TempDataBox = Pick<ComponentProps<typeof DataBox>, "color" | "value">;
15
+
16
+ interface BadgeOpts {
17
+ bgClass?: string;
18
+ iconColorClass?: string;
19
+ labelColorClass?: string;
20
+ href?: string;
21
+ }
22
+ const { route, navigate, basePath } = useMonitorMapRouter();
23
+
24
+ const monitor = $derived(monitorsManager.latest?.get(route.params.id ?? ""));
25
+ let tempData: TempDataBox | undefined = $state();
26
+ const entryData: ComponentProps<typeof DataBox> | undefined = $derived.by(() => {
27
+ if (!monitor || !monitorsManager.meta) return;
28
+
29
+ const entryType = monitorsManager.meta.entryType(monitor.latest.entry_type);
30
+ const level: SJVAirEntryLevel | undefined = getCurrentLevel(
31
+ monitor.latest.value,
32
+ entryType.asIter.levels!
33
+ );
34
+
35
+ const updateDuration = monitorsManager.meta.monitorType(monitor.type).expected_interval;
36
+
37
+ return {
38
+ color: level?.color ?? "",
39
+ header: entryType.label,
40
+ subheading: `(${updateDuration} avg)`,
41
+ value: Math.round(parseInt(monitor.latest.value, 10)).toString()
42
+ };
43
+ });
44
+
45
+ const tempColors = [
46
+ { min: -Infinity, color: Colors.blue },
47
+ { min: 65, color: Colors.green },
48
+ { min: 78, color: Colors.yellow },
49
+ { min: 95, color: Colors.red }
50
+ ];
51
+
52
+ $effect(() => {
53
+ if (monitor && monitor.position) {
54
+ fetchTempByCoords(monitor.position.coordinates).then((temperature) => {
55
+ tempData = {
56
+ value: `${temperature}°F`,
57
+ color: valueToColor(temperature, tempColors)
58
+ };
59
+ });
60
+ }
61
+ });
62
+ </script>
63
+
64
+ {#snippet badge(icon: string, label: string, opts: BadgeOpts = {})}
65
+ <span
66
+ class="flex items-center justify-center gap-1 rounded-sm border px-1 py-0.5
67
+ {opts.bgClass ?? 'bg-neutral-100'}"
68
+ >
69
+ <span class={["svg-icon", icon, opts.iconColorClass ?? "bg-black/70"]}></span>
70
+ {#if opts.href}
71
+ <a class="text-link text-xs" href={opts.href} target="_blank" translate="no">{label}</a>
72
+ {:else}
73
+ <span
74
+ class={["text-xs whitespace-nowrap", opts.labelColorClass ?? "text-black/70"]}
75
+ translate="no">{label}</span
76
+ >
77
+ {/if}
78
+ </span>
79
+ {/snippet}
80
+
81
+ <div class="flex h-full w-full flex-col overflow-y-auto bg-white">
82
+ <div
83
+ class="flex flex-col items-center justify-center gap-8 border-b border-gray-200 p-4 shadow-lg"
84
+ >
85
+ <button
86
+ class="cursor-pointer self-end rounded border border-gray-300 px-2 py-1 text-sm hover:bg-gray-50"
87
+ onclick={() => navigate(`${basePath}/`)}
88
+ >
89
+ ✕ Close
90
+ </button>
91
+ {#if monitor}
92
+ <h2 class="text-center text-3xl font-semibold">{monitor.name}</h2>
93
+ <div class="flex flex-wrap items-center justify-center gap-4">
94
+ {#if monitor.is_sjvair}
95
+ {@render badge("lungs", "SJVAir", {
96
+ bgClass: "bg-aqua-haze",
97
+ iconColorClass: "bg-sky-600",
98
+ labelColorClass: "text-sky-600"
99
+ })}
100
+ {/if}
101
+
102
+ {@render badge("rss-feed", monitor.data_source.name, { href: monitor.data_source.url })}
103
+ {@render badge("router", monitor.device)}
104
+ {@render badge("location-on", monitor.county)}
105
+ {@render badge("location-searching", monitor.location)}
106
+ </div>
107
+ {/if}
108
+ </div>
109
+
110
+ <div class="flex flex-col items-center gap-12 overflow-y-auto p-5 pt-10">
111
+ {#if monitor}
112
+ <div class="flex w-full justify-around">
113
+ {#if tempData}
114
+ <DataBox color={tempData.color} header="Temperature" value={tempData.value} />
115
+ {/if}
116
+ {#if entryData}
117
+ <DataBox
118
+ color={entryData.color}
119
+ header={entryData.header}
120
+ subheading={entryData.subheading}
121
+ value={entryData.value}
122
+ />
123
+ {/if}
124
+ </div>
125
+
126
+ <MonitorWidgetModal monitorId={monitor.id} />
127
+ <!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
128
+ {#if (globalThis as any).USER?.is_authenticated}
129
+ <MonitorSubscriptionManager monitorId={monitor.id} />
130
+ {/if}
131
+
132
+ <div class="w-full">
133
+ <DataChart {monitor} />
134
+ </div>
135
+
136
+ <div class="w-full">
137
+ <h3 class="font-semibold">Data Provided Courtesy Of:</h3>
138
+ <ul>
139
+ {#each monitor.data_providers as provider, idx (idx)}
140
+ <li>
141
+ {#if provider.url}
142
+ <a class="text-link" href={provider.url} target="_blank">{provider.name}</a>
143
+ {:else}
144
+ <span>{provider.name}</span>
145
+ {/if}
146
+ </li>
147
+ {/each}
148
+ </ul>
149
+ </div>
150
+ {/if}
151
+ </div>
152
+ </div>
@@ -0,0 +1,193 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import { onDestroy } from "svelte";
4
+ import LoadScreen, { disable as disableLoadScreen } from "$lib/LoadScreen.svelte";
5
+ import Map from "$lib/map/Map.svelte";
6
+ import Menu from "$lib/map/Menu.svelte";
7
+ import MonitorsDisplayOptions from "$lib/monitors/components/MonitorsDisplayOptions.svelte";
8
+ import MapLayersDisplayOptions from "$lib/components/MapLayersDisplayOptions.svelte";
9
+ import MapStyleDisplayOptions from "$lib/map/MapStyleDisplayOptions.svelte";
10
+ import { mapManager } from "$lib/map/map.svelte";
11
+ import { monitorsManager } from "$lib/monitors/monitors.svelte";
12
+ import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte.ts";
13
+ import { windMapIntegration } from "$lib/wind/wind.svelte";
14
+ import { baseLayerSeperator } from "$lib/map/integrations/base-layer-seperator";
15
+ import type { SomeMapIntegration } from "$lib/map/integrations/types";
16
+ import { collocationSitesManager } from "$lib/collocation-sites/collocations.svelte";
17
+ import { collocationSitesMapIntegration } from "$lib/collocation-sites/collocations-map-integration.svelte";
18
+ import EvStationsDisplayOptions from "$lib/ev-stations/components/EvStationsDisplayOptions.svelte";
19
+ import { evStationsMapIntegration } from "$lib/ev-stations/ev-stations-map-integration.svelte.ts";
20
+ import { hmsManager } from "$lib/hms/hms.svelte.ts";
21
+ import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte.ts";
22
+ import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte.ts";
23
+ import MapLegend from "$lib/MapLegend.svelte";
24
+ import Search from "$lib/search/Search.svelte";
25
+ import { useMonitorMapRouter } from "./router-context";
26
+
27
+ interface Props {
28
+ children: Snippet;
29
+ }
30
+
31
+ let { children }: Props = $props();
32
+
33
+ const { route, navigate, basePath } = useMonitorMapRouter();
34
+
35
+ const TRANSITION_MS = 300;
36
+
37
+ const integrations: Array<SomeMapIntegration> = [
38
+ baseLayerSeperator,
39
+ collocationSitesMapIntegration,
40
+ windMapIntegration,
41
+ hmsSmokeMapIntegration,
42
+ hmsFireMapIntegration,
43
+ monitorsMapIntegration,
44
+ evStationsMapIntegration
45
+ ];
46
+
47
+ monitorsManager.init(route.search.pollutant);
48
+ collocationSitesManager.init();
49
+ hmsManager.init();
50
+ monitorsMapIntegration.onMonitorClick = (id: string) => {
51
+ navigate(`${basePath}/monitor/:id`, { params: { id } }).catch(console.error);
52
+ };
53
+
54
+ let panelOpen = $derived(route.pathname.startsWith(`${basePath}/monitor/`));
55
+
56
+ $effect(() => {
57
+ if (mapManager.map && monitorsManager.initialized) {
58
+ mapManager.map.once("idle", () => disableLoadScreen());
59
+ }
60
+ });
61
+
62
+ $effect(() => {
63
+ if (panelOpen) {
64
+ // Container width snapped to open; resize after layout settles
65
+ requestAnimationFrame(() => mapManager.map?.resize());
66
+ } else {
67
+ // Container width snaps back after the transform transition ends
68
+ setTimeout(() => mapManager.map?.resize(), TRANSITION_MS);
69
+ }
70
+ });
71
+
72
+ // Clear selected icon scale when the detail panel closes
73
+ $effect(() => {
74
+ if (panelOpen) return;
75
+ monitorsMapIntegration.selectedMonitorId = null;
76
+ });
77
+
78
+ onDestroy(() => {
79
+ monitorsManager.autoUpdate.stop();
80
+ });
81
+
82
+ /**
83
+ * HACK: Fix for escaping sv-router and allowing navigation to other pages,
84
+ * as well as navigating back
85
+ */
86
+ const rootPath = basePath || "/";
87
+ const knownRoutes = [`${basePath}/monitor/`];
88
+
89
+ window.addEventListener("pageshow", (e) => {
90
+ if (e.persisted) window.location.reload();
91
+ });
92
+
93
+ document.addEventListener(
94
+ "click",
95
+ (e) => {
96
+ const anchor = e
97
+ .composedPath()
98
+ .find((el) => el instanceof HTMLAnchorElement) as HTMLAnchorElement;
99
+ if (!anchor) return;
100
+ const { pathname } = new URL(anchor.href);
101
+ const isKnown = pathname === rootPath || knownRoutes.some((r) => pathname.startsWith(r));
102
+ if (!isKnown) {
103
+ e.stopImmediatePropagation();
104
+ window.location.href = anchor.href;
105
+ }
106
+ },
107
+ { capture: true }
108
+ );
109
+ </script>
110
+
111
+ <div class="shell" class:panel-open={panelOpen}>
112
+ <LoadScreen />
113
+ <div class="relative flex-1 overflow-hidden">
114
+ <Map {integrations} />
115
+ <div class="z-10 absolute bottom-0 left-0 pointer-events-none">
116
+ <MapLegend />
117
+ </div>
118
+ <div class="absolute top-4 left-4 z-10">
119
+ <div class="absolute z-10">
120
+ <Menu>
121
+ <MonitorsDisplayOptions />
122
+ <EvStationsDisplayOptions />
123
+ <MapLayersDisplayOptions />
124
+ <MapStyleDisplayOptions />
125
+ </Menu>
126
+ </div>
127
+ <div class="absolute left-16">
128
+ <Search />
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <div class="panel-container">
133
+ <div class="panel-content">
134
+ {@render children()}
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <style>
140
+ .shell {
141
+ display: flex;
142
+ position: relative;
143
+ width: 100%;
144
+ height: 100%;
145
+ }
146
+
147
+ .panel-container {
148
+ flex-shrink: 0;
149
+ overflow: hidden;
150
+ width: 0;
151
+ }
152
+
153
+ .shell.panel-open .panel-container {
154
+ width: 33.333vw;
155
+ }
156
+
157
+ .panel-content {
158
+ width: 33.333vw;
159
+ height: 100%;
160
+ transform: translateX(100%);
161
+ transition: transform 300ms ease-in-out;
162
+ }
163
+
164
+ .shell.panel-open .panel-content {
165
+ transform: translateX(0);
166
+ }
167
+
168
+ @media (max-width: 768px) {
169
+ .shell {
170
+ flex-direction: column;
171
+ }
172
+
173
+ .panel-container {
174
+ width: 100vw;
175
+ height: 0;
176
+ }
177
+
178
+ .shell.panel-open .panel-container {
179
+ width: 100vw;
180
+ height: 50vh;
181
+ }
182
+
183
+ .panel-content {
184
+ width: 100vw;
185
+ height: 50vh;
186
+ transform: translateY(100%);
187
+ }
188
+
189
+ .shell.panel-open .panel-content {
190
+ transform: translateY(0);
191
+ }
192
+ }
193
+ </style>
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { WindIcon, CrosshairIcon, WavesVerticalIcon } from "@lucide/svelte";
2
3
  import DisplayOption from "$lib/components/DisplayOption.svelte";
3
4
  import ToggleSwitch from "$lib/components/ToggleSwitch.svelte";
4
5
  import { windMapIntegration } from "$lib/wind/wind.svelte";
@@ -6,20 +7,36 @@
6
7
  import { monitorsManager } from "$lib/monitors/monitors.svelte";
7
8
  import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte";
8
9
  import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte";
10
+
11
+ const fireIcon = hmsFireMapIntegration.icons.get("hms-fire-xxl");
9
12
  </script>
10
13
 
11
14
  <DisplayOption>
12
15
  <p class="text-lg font-bold whitespace-nowrap underline">Map Layers</p>
13
- <ToggleSwitch id="wind" label="Wind" bind:value={windMapIntegration.enabled}></ToggleSwitch>
16
+ <div class="flex gap-1 items-center">
17
+ <WindIcon size="16" />
18
+ <ToggleSwitch id="wind" label="Wind" bind:value={windMapIntegration.enabled}></ToggleSwitch>
19
+ </div>
14
20
  {#if monitorsManager.pollutant === "pm25"}
15
- <ToggleSwitch
16
- id="collocation-sites"
17
- label="Collocation Sites"
18
- bind:value={collocationSitesMapIntegration.enabled}
19
- ></ToggleSwitch>
21
+ <div class="flex gap-1 items-center">
22
+ <CrosshairIcon size="16" color="#4A5FC6" />
23
+ <ToggleSwitch
24
+ id="collocation-sites"
25
+ label="Collocation Sites"
26
+ bind:value={collocationSitesMapIntegration.enabled}
27
+ ></ToggleSwitch>
28
+ </div>
20
29
  {/if}
21
- <ToggleSwitch id="hms-fire" label="HMS Fire" bind:value={hmsFireMapIntegration.enabled}
22
- ></ToggleSwitch>
23
- <ToggleSwitch id="hms-smoke" label="HMS Smoke" bind:value={hmsSmokeMapIntegration.enabled}
24
- ></ToggleSwitch>
30
+ <div class="flex gap-1 items-center">
31
+ {#if fireIcon}
32
+ <img class="w-4" src={fireIcon.image.src} alt={fireIcon.image.alt} />
33
+ {/if}
34
+ <ToggleSwitch id="hms-fire" label="HMS Fire" bind:value={hmsFireMapIntegration.enabled}
35
+ ></ToggleSwitch>
36
+ </div>
37
+ <div class="flex gap-1 items-center">
38
+ <WavesVerticalIcon size="16" />
39
+ <ToggleSwitch id="hms-smoke" label="HMS Smoke" bind:value={hmsSmokeMapIntegration.enabled}
40
+ ></ToggleSwitch>
41
+ </div>
25
42
  </DisplayOption>
@@ -2,6 +2,8 @@
2
2
  import DisplayOption from "$lib/components/DisplayOption.svelte";
3
3
  import ToggleSwitch from "$lib/components/ToggleSwitch.svelte";
4
4
  import { evStationsMapIntegration } from "../ev-stations-map-integration.svelte";
5
+
6
+ const { displayOptions } = evStationsMapIntegration;
5
7
  </script>
6
8
 
7
9
  <DisplayOption>
@@ -15,20 +17,30 @@
15
17
  />
16
18
  </div>
17
19
  </div>
18
- <label for="ev-lvl2" class="cursor-pointer select-none whitespace-nowrap">
20
+ <label for="ev-lvl2" class="flex gap-1 cursor-pointer select-none whitespace-nowrap">
19
21
  <input
20
22
  type="checkbox"
21
23
  id="ev-lvl2"
22
24
  bind:checked={evStationsMapIntegration.displayOptions.lvl2.value}
23
25
  />
26
+ <img
27
+ class="w-5"
28
+ src={displayOptions.lvl2.icon!.image.src}
29
+ alt={displayOptions.lvl2.icon!.image.alt}
30
+ />
24
31
  Level 2
25
32
  </label>
26
- <label for="ev-lvl3" class="cursor-pointer select-none whitespace-nowrap">
33
+ <label for="ev-lvl3" class="flex gap-1 cursor-pointer select-none whitespace-nowrap">
27
34
  <input
28
35
  type="checkbox"
29
36
  id="ev-lvl3"
30
37
  bind:checked={evStationsMapIntegration.displayOptions.lvl3.value}
31
38
  />
39
+ <img
40
+ class="w-5"
41
+ src={displayOptions.lvl3.icon!.image.src}
42
+ alt={displayOptions.lvl3.icon!.image.alt}
43
+ />
32
44
  Level 3
33
45
  </label>
34
46
  </DisplayOption>
@@ -34,10 +34,10 @@ class EvStationsMapIntegration
34
34
  private renderer: EvStationsClusterRenderer = new EvStationsClusterRenderer(this);
35
35
  private currentPopup: Popup | null = null;
36
36
 
37
- displayOptions = {
38
- lvl2: new MapDisplayOption("Level 2", false),
39
- lvl3: new MapDisplayOption("Level 3", false)
40
- };
37
+ displayOptions = $derived.by(() => ({
38
+ lvl2: new MapDisplayOption("Level 2", false, this.icons.get("ev-station-lvl2")!),
39
+ lvl3: new MapDisplayOption("Level 3", false, this.icons.get("ev-station-lvl3")!)
40
+ }));
41
41
 
42
42
  features: Array<EvStationMapFeature> = $derived.by(() => {
43
43
  const result: EvStationMapFeature[] = [];
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { hmsFireMapIntegration } from "../hms-fire-map-integration.svelte.ts";
3
+
4
+ const levels = ["<10", "10-49", "50-149", "150-349", "≥350"];
5
+ </script>
6
+
7
+ {#if hmsFireMapIntegration.enabled && hmsFireMapIntegration.features.length > 0}
8
+ <div class="flex flex-col bg-white rounded shadow w-fit p-1">
9
+ <p class="text-center whitespace-nowrap font-semibold">Fire Points</p>
10
+ <div class="flex flex-col">
11
+ {#each hmsFireMapIntegration.icons.iter as [_id, icon], idx (idx)}
12
+ <div class="flex gap-1 items-center">
13
+ <div class="size-9 flex justify-center items-center">
14
+ <img src={icon.image.src} alt={icon.image.alt} />
15
+ </div>
16
+ <span>{levels[idx]} MW</span>
17
+ </div>
18
+ {/each}
19
+ </div>
20
+ </div>
21
+ {/if}
package/src/lib/index.ts CHANGED
@@ -1,3 +1,37 @@
1
+ /**
2
+ * Usage in a host app:
3
+ *
4
+ * ```ts
5
+ * // router.ts
6
+ * import { createRouter } from "sv-router";
7
+ * import { monitorMapRoutes } from "@sjvair/monitor-map";
8
+ *
9
+ * export const { route, navigate, p, isActive, Router } = createRouter({
10
+ * "/my-map": monitorMapRoutes
11
+ * // ...the host app's own routes
12
+ * });
13
+ * ```
14
+ *
15
+ * ```svelte
16
+ * <!-- app root -->
17
+ * <script>
18
+ * import { provideMonitorMapRouter } from "@sjvair/monitor-map";
19
+ * import { route, navigate, p, isActive, Router } from "./router";
20
+ *
21
+ * provideMonitorMapRouter({ route, navigate, p, isActive, basePath: "/my-map" });
22
+ * </script>
23
+ *
24
+ * <Router />
25
+ * ```
26
+ */
27
+ export { monitorMapRoutes } from "./routes";
28
+ export {
29
+ ROUTER_KEY,
30
+ provideMonitorMapRouter,
31
+ useMonitorMapRouter,
32
+ type MonitorMapRouterApi
33
+ } from "./router-context";
34
+
1
35
  // State managers
2
36
  export { monitorsManager } from "./monitors/monitors.svelte";
3
37
  export type { MonitorsManager } from "./monitors/monitors.svelte";
@@ -6,6 +6,7 @@
6
6
 
7
7
  const { integrations }: { integrations: Array<SomeMapIntegration> } = $props();
8
8
 
9
+ // svelte-ignore state_referenced_locally (we only need the initial reference)
9
10
  integrationsManager.register(...integrations);
10
11
  </script>
11
12
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Attachment } from "svelte/attachments";
3
+ import { SlidersHorizontalIcon } from "@lucide/svelte";
3
4
 
4
5
  let { children } = $props();
5
6
  let isOpen: boolean = $state(false);
@@ -37,20 +38,16 @@
37
38
  data-trigger
38
39
  class:rounded-full={!isOpen}
39
40
  class:delay-100={!isOpen}
40
- class="absolute top-0 left-0 flex items-center justify-center overflow-hidden rounded bg-white p-1 shadow transition-all duration-0 select-none"
41
+ class="relative top-0 left-0 flex items-center justify-center overflow-hidden rounded bg-white p-1 shadow transition-all duration-0 select-none"
41
42
  >
42
43
  <button
43
44
  class:w-0={isOpen}
44
45
  class:h-0={isOpen}
45
46
  class:opacity-0={isOpen}
46
- class="relative size-8 transition-all"
47
+ class="relative size-10 transition-all flex justify-center items-center"
47
48
  aria-label="Display Options"
48
49
  >
49
- <enhanced:img
50
- src="$lib/assets/icons/adjustment-horizontal.svg"
51
- alt="SJVAir Icon"
52
- class="size-8"
53
- />
50
+ <SlidersHorizontalIcon size="24" />
54
51
  </button>
55
52
  <div
56
53
  class:h-55={isOpen}
@@ -1,8 +1,12 @@
1
+ import { MapImageIcon } from "./types";
2
+
1
3
  export class MapDisplayOption {
4
+ icon?: MapImageIcon;
2
5
  label: string;
3
6
  value: boolean;
4
7
 
5
- constructor(label: string, value: boolean) {
8
+ constructor(label: string, value: boolean, icon?: MapImageIcon) {
9
+ this.icon = icon;
6
10
  this.label = label;
7
11
  this.value = $state(value);
8
12
  }
@@ -1,17 +1,21 @@
1
1
  import type { Map as MaptilerMap } from "@maptiler/sdk";
2
- import { XMap } from "@tstk/builtin-extensions";
3
2
  import { mapManager } from "../map.svelte.ts";
4
3
  import type { MapImageIcon } from "./types";
4
+ import { SvelteMap } from "svelte/reactivity";
5
5
 
6
6
  export class MapIconManager {
7
- protected icons: XMap<string, MapImageIcon> = new XMap();
7
+ protected icons: SvelteMap<string, MapImageIcon> = new SvelteMap();
8
8
 
9
- constructor(iconMap?: XMap<string, MapImageIcon>) {
9
+ constructor(iconMap?: SvelteMap<string, MapImageIcon>) {
10
10
  if (iconMap) {
11
11
  this.icons = iconMap;
12
12
  }
13
13
  }
14
14
 
15
+ get iter(): MapIterator<[string, MapImageIcon]> {
16
+ return this.icons.entries();
17
+ }
18
+
15
19
  get(id: string): MapImageIcon | undefined {
16
20
  return this.icons.get(id);
17
21
  }
@@ -68,8 +68,11 @@
68
68
 
69
69
  {#each monitorDisplayOptions as [id, option] (option.label)}
70
70
  {@const inputId = `${id}-monitor-display-option`}
71
- <label for={inputId} class="cursor-pointer whitespace-nowrap select-none">
71
+ <label for={inputId} class="flex gap-1 cursor-pointer whitespace-nowrap select-none">
72
72
  <input type="checkbox" id={inputId} name={option.label} bind:checked={option.value} />
73
+ {#if option.icon}
74
+ <img class="w-3" src={option.icon.image.src} alt={option.icon.image.alt} />
75
+ {/if}
73
76
  {option.label}
74
77
  </label>
75
78
  {/each}
@@ -7,6 +7,7 @@ import { monitorsManager } from "./monitors.svelte.ts";
7
7
  const MONITOR_ICONS = { circle, square, triangle };
8
8
  const MONITOR_ICON_BORDER_WIDTH = 2;
9
9
  const MONITOR_ICON_DEFAULT_COLOR = "#969696"; // light gray
10
+ const MONITOR_ICON_DISPLAY_COLOR = "#48c78e"; // light gray
10
11
 
11
12
  export function getIconId<T extends MonitorData>(monitor: T, level: SJVAirEntryLevel): string {
12
13
  const id = `${monitor.location}-${monitor.is_active ? level.name : "default"}`;
@@ -43,7 +44,8 @@ export abstract class MonitorShapeIconManager extends MapIconManager {
43
44
  if (monitorsManager.levels) {
44
45
  const iconLevels = [
45
46
  ...monitorsManager.levels,
46
- { name: "default", color: MONITOR_ICON_DEFAULT_COLOR }
47
+ { name: "default", color: MONITOR_ICON_DEFAULT_COLOR },
48
+ { name: "display", color: MONITOR_ICON_DISPLAY_COLOR }
47
49
  ];
48
50
 
49
51
  for (const location of ["inside", "outside"]) {
@@ -38,15 +38,15 @@ class MonitorsMapIntegration extends MapIconLayerIntegration<MonitorMarkerProper
38
38
  tooltipManager: TooltipManager = new TooltipManager();
39
39
  private renderer: MonitorsClusterRenderer = new MonitorsClusterRenderer(this);
40
40
 
41
- displayOptions = {
42
- purpleair: new MapDisplayOption("PurpleAir", true),
43
- sjvair: new MapDisplayOption("SJVAir non-FEM", true),
44
- aqview: new MapDisplayOption("AQview", true),
45
- bam1022: new MapDisplayOption("SJVAir FEM", true),
46
- airnow: new MapDisplayOption("AirNow", true),
47
- inactive: new MapDisplayOption("Inactive", false),
48
- inside: new MapDisplayOption("Inside", false)
49
- };
41
+ displayOptions = $derived.by(() => ({
42
+ purpleair: new MapDisplayOption("PurpleAir", true, this.icons.get("outside-display-square")),
43
+ sjvair: new MapDisplayOption("SJVAir non-FEM", true, this.icons.get("outside-display-circle")),
44
+ aqview: new MapDisplayOption("AQview", true, this.icons.get("outside-display-triangle")),
45
+ bam1022: new MapDisplayOption("SJVAir FEM", true, this.icons.get("outside-display-triangle")),
46
+ airnow: new MapDisplayOption("AirNow", true, this.icons.get("outside-display-triangle")),
47
+ inactive: new MapDisplayOption("Inactive", false, this.icons.get("outside-default-square")),
48
+ inside: new MapDisplayOption("Inside", false, this.icons.get("inside-display-square"))
49
+ }));
50
50
 
51
51
  /** Set by the app to handle navigation when a monitor is clicked. Receives the monitor ID. */
52
52
  onMonitorClick: ((id: string) => void) | null = null;
@@ -7,7 +7,6 @@ import {
7
7
  type MonitorsMeta,
8
8
  type SJVAirEntryLevel
9
9
  } from "@sjvair/sdk";
10
- import { route } from "../../router.ts";
11
10
  import { XMap } from "@tstk/builtin-extensions";
12
11
  import { Interval } from "@tstk/utils";
13
12
 
@@ -26,12 +25,11 @@ class MonitorsManager {
26
25
  this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null
27
26
  );
28
27
 
29
- async init(): Promise<void> {
28
+ async init(urlPollutant?: string | number | boolean): Promise<void> {
30
29
  if (this.initialized) return;
31
30
 
32
31
  [this.list, this.meta] = await Promise.all([getMonitors(), getMonitorsMeta()]);
33
32
 
34
- const urlPollutant = route.search.pollutant;
35
33
  this.pollutant =
36
34
  urlPollutant === "pm25" || urlPollutant === "o3" ? urlPollutant : this.meta.default_pollutant;
37
35
  this.latest = await getMonitorsLatestMap(this.pollutant);