@sjvair/monitor-map 3.6.3 → 4.0.1

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 (156) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/MonitorMapLayout.svelte +74 -36
  3. package/dist/lib/assets/icons/ozone/ozone-08-sunrise-haze.svg +1 -0
  4. package/dist/lib/assets/icons/pm25/pm25-02-particle-field.svg +1 -0
  5. package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +3 -0
  6. package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +15 -1
  7. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +27 -6
  8. package/dist/lib/hms/hms-fire-icon-manager.d.ts +8 -0
  9. package/dist/lib/hms/hms-fire-icon-manager.js +10 -1
  10. package/dist/lib/hms/hms-fire-map-integration.svelte.js +1 -1
  11. package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +20 -0
  12. package/dist/lib/hms/hms-smoke-map-integration.svelte.js +22 -7
  13. package/dist/lib/index.d.ts +21 -7
  14. package/dist/lib/index.js +22 -7
  15. package/dist/lib/legend/Legend.svelte +40 -0
  16. package/dist/lib/legend/Legend.svelte.d.ts +7 -0
  17. package/dist/lib/legend/LegendBar.svelte +45 -0
  18. package/dist/lib/legend/LegendBar.svelte.d.ts +7 -0
  19. package/dist/lib/legend/legend-data.d.ts +40 -0
  20. package/dist/lib/legend/legend-data.js +65 -0
  21. package/dist/lib/map/MapShell.svelte +7 -6
  22. package/dist/lib/map/MapShell.svelte.d.ts +3 -1
  23. package/dist/lib/map/basemap.d.ts +14 -0
  24. package/dist/lib/map/basemap.js +10 -0
  25. package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +6 -0
  26. package/dist/lib/map/integrations/map-geojson-integration.svelte.js +15 -2
  27. package/dist/lib/map/integrations/map-layer-integration.svelte.js +7 -3
  28. package/dist/lib/map/map-style-state.svelte.d.ts +13 -0
  29. package/dist/lib/map/map-style-state.svelte.js +35 -0
  30. package/dist/lib/map/map.svelte.d.ts +6 -0
  31. package/dist/lib/map/map.svelte.js +6 -0
  32. package/dist/lib/monitors/monitor-groups.d.ts +11 -0
  33. package/dist/lib/monitors/monitor-groups.js +7 -0
  34. package/dist/lib/monitors/monitor-utils.js +1 -3
  35. package/dist/lib/monitors/monitors-icon-manager.svelte.js +2 -5
  36. package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +4 -4
  37. package/dist/lib/monitors/monitors-map-integration.svelte.js +45 -32
  38. package/dist/lib/monitors/monitors.svelte.d.ts +1 -0
  39. package/dist/lib/monitors/monitors.svelte.js +22 -0
  40. package/dist/lib/options/OptionsBar.svelte +96 -0
  41. package/dist/lib/options/OptionsBar.svelte.d.ts +10 -0
  42. package/dist/lib/options/OptionsGroup.svelte +12 -0
  43. package/dist/lib/options/OptionsGroup.svelte.d.ts +7 -0
  44. package/dist/lib/options/OptionsMenu.svelte +71 -0
  45. package/dist/lib/options/OptionsMenu.svelte.d.ts +15 -0
  46. package/dist/lib/options/group-state.d.ts +7 -0
  47. package/dist/lib/options/group-state.js +15 -0
  48. package/dist/lib/options/layout.d.ts +7 -0
  49. package/dist/lib/options/layout.js +9 -0
  50. package/dist/lib/options/map-option-defaults.d.ts +56 -0
  51. package/dist/lib/options/map-option-defaults.js +47 -0
  52. package/dist/lib/options/menus/LayersMenu.svelte +47 -0
  53. package/dist/lib/options/menus/LayersMenu.svelte.d.ts +3 -0
  54. package/dist/lib/options/menus/MonitorsMenu.svelte +68 -0
  55. package/dist/lib/options/menus/MonitorsMenu.svelte.d.ts +3 -0
  56. package/dist/lib/options/menus/OverlaysMenu.svelte +18 -0
  57. package/dist/lib/{MapLegend.svelte.d.ts → options/menus/OverlaysMenu.svelte.d.ts} +3 -3
  58. package/dist/lib/options/menus/PollutantMenu.svelte +89 -0
  59. package/dist/lib/options/menus/PollutantMenu.svelte.d.ts +3 -0
  60. package/dist/lib/options/menus/SettingsMenu.svelte +73 -0
  61. package/dist/lib/{hms/components/HMSFireLegend.svelte.d.ts → options/menus/SettingsMenu.svelte.d.ts} +3 -3
  62. package/dist/lib/options/options-state.svelte.d.ts +12 -0
  63. package/dist/lib/options/options-state.svelte.js +42 -0
  64. package/dist/lib/options/pollutant-param.d.ts +17 -0
  65. package/dist/lib/options/pollutant-param.js +14 -0
  66. package/dist/lib/options/reset-map-options.d.ts +2 -0
  67. package/dist/lib/options/reset-map-options.js +24 -0
  68. package/dist/lib/options/rows/CheckboxRow.svelte +51 -0
  69. package/dist/lib/options/rows/CheckboxRow.svelte.d.ts +13 -0
  70. package/dist/lib/options/rows/GroupRow.svelte +43 -0
  71. package/dist/lib/options/rows/GroupRow.svelte.d.ts +17 -0
  72. package/dist/lib/options/rows/MarkerIcon.svelte +10 -0
  73. package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +8 -0
  74. package/dist/lib/options/rows/RadioRow.svelte +33 -0
  75. package/dist/lib/options/rows/RadioRow.svelte.d.ts +13 -0
  76. package/dist/lib/options/rows/SubmenuRow.svelte +22 -0
  77. package/dist/lib/options/rows/SubmenuRow.svelte.d.ts +11 -0
  78. package/dist/lib/search/Search.svelte +6 -6
  79. package/package.json +5 -3
  80. package/src/lib/MonitorMapLayout.svelte +74 -36
  81. package/src/lib/assets/icons/ozone/ozone-08-sunrise-haze.svg +1 -0
  82. package/src/lib/assets/icons/pm25/pm25-02-particle-field.svg +1 -0
  83. package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +17 -1
  84. package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +26 -6
  85. package/src/lib/hms/hms-fire-icon-manager.ts +16 -2
  86. package/src/lib/hms/hms-fire-map-integration.svelte.ts +1 -1
  87. package/src/lib/hms/hms-smoke-map-integration.svelte.ts +25 -7
  88. package/src/lib/index.ts +31 -7
  89. package/src/lib/legend/Legend.svelte +40 -0
  90. package/src/lib/legend/LegendBar.svelte +45 -0
  91. package/src/lib/legend/legend-data.ts +101 -0
  92. package/src/lib/map/MapShell.svelte +7 -6
  93. package/src/lib/map/basemap.ts +24 -0
  94. package/src/lib/map/integrations/map-geojson-integration.svelte.ts +15 -2
  95. package/src/lib/map/integrations/map-layer-integration.svelte.ts +6 -3
  96. package/src/lib/map/map-style-state.svelte.ts +42 -0
  97. package/src/lib/map/map.svelte.ts +6 -0
  98. package/src/lib/monitors/monitor-groups.ts +20 -0
  99. package/src/lib/monitors/monitor-utils.ts +1 -5
  100. package/src/lib/monitors/monitors-icon-manager.svelte.ts +2 -8
  101. package/src/lib/monitors/monitors-map-integration.svelte.ts +39 -29
  102. package/src/lib/monitors/monitors.svelte.ts +22 -0
  103. package/src/lib/options/OptionsBar.svelte +96 -0
  104. package/src/lib/options/OptionsGroup.svelte +12 -0
  105. package/src/lib/options/OptionsMenu.svelte +71 -0
  106. package/src/lib/options/group-state.ts +16 -0
  107. package/src/lib/options/layout.ts +10 -0
  108. package/src/lib/options/map-option-defaults.ts +71 -0
  109. package/src/lib/options/menus/LayersMenu.svelte +47 -0
  110. package/src/lib/options/menus/MonitorsMenu.svelte +68 -0
  111. package/src/lib/options/menus/OverlaysMenu.svelte +18 -0
  112. package/src/lib/options/menus/PollutantMenu.svelte +89 -0
  113. package/src/lib/options/menus/SettingsMenu.svelte +73 -0
  114. package/src/lib/options/options-state.svelte.ts +48 -0
  115. package/src/lib/options/pollutant-param.ts +35 -0
  116. package/src/lib/options/reset-map-options.ts +28 -0
  117. package/src/lib/options/rows/CheckboxRow.svelte +51 -0
  118. package/src/lib/options/rows/GroupRow.svelte +43 -0
  119. package/src/lib/options/rows/MarkerIcon.svelte +10 -0
  120. package/src/lib/options/rows/RadioRow.svelte +33 -0
  121. package/src/lib/options/rows/SubmenuRow.svelte +22 -0
  122. package/src/lib/search/Search.svelte +6 -6
  123. package/dist/lib/MapLegend.svelte +0 -15
  124. package/dist/lib/collocation-sites/collocation-utils.d.ts +0 -3
  125. package/dist/lib/collocation-sites/collocation-utils.js +0 -30
  126. package/dist/lib/components/DisplayOption.svelte +0 -7
  127. package/dist/lib/components/DisplayOption.svelte.d.ts +0 -5
  128. package/dist/lib/components/MapLayersDisplayOptions.svelte +0 -42
  129. package/dist/lib/components/MapLayersDisplayOptions.svelte.d.ts +0 -18
  130. package/dist/lib/components/SegmentedControl.svelte +0 -52
  131. package/dist/lib/components/SegmentedControl.svelte.d.ts +0 -11
  132. package/dist/lib/components/ToggleSwitch.svelte +0 -93
  133. package/dist/lib/components/ToggleSwitch.svelte.d.ts +0 -9
  134. package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
  135. package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte.d.ts +0 -18
  136. package/dist/lib/hms/components/HMSFireLegend.svelte +0 -21
  137. package/dist/lib/map/MapStyleDisplayOptions.svelte +0 -61
  138. package/dist/lib/map/MapStyleDisplayOptions.svelte.d.ts +0 -3
  139. package/dist/lib/map/Menu.svelte +0 -65
  140. package/dist/lib/map/Menu.svelte.d.ts +0 -5
  141. package/dist/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
  142. package/dist/lib/monitors/components/MonitorMarkersLegend.svelte.d.ts +0 -3
  143. package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte +0 -85
  144. package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte.d.ts +0 -3
  145. package/src/lib/MapLegend.svelte +0 -15
  146. package/src/lib/collocation-sites/collocation-utils.ts +0 -41
  147. package/src/lib/components/DisplayOption.svelte +0 -7
  148. package/src/lib/components/MapLayersDisplayOptions.svelte +0 -42
  149. package/src/lib/components/SegmentedControl.svelte +0 -52
  150. package/src/lib/components/ToggleSwitch.svelte +0 -93
  151. package/src/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
  152. package/src/lib/hms/components/HMSFireLegend.svelte +0 -21
  153. package/src/lib/map/MapStyleDisplayOptions.svelte +0 -61
  154. package/src/lib/map/Menu.svelte +0 -65
  155. package/src/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
  156. package/src/lib/monitors/components/MonitorsDisplayOptions.svelte +0 -85
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ import { ContrastIcon, MapIcon, NetworkIcon, RotateCcwIcon, SettingsIcon } from "@lucide/svelte";
3
+ import OptionsMenu from "../OptionsMenu.svelte";
4
+ import CheckboxRow from "../rows/CheckboxRow.svelte";
5
+ import RadioRow from "../rows/RadioRow.svelte";
6
+ import { resetMapOptions } from "../reset-map-options";
7
+ import { MAP_STYLE_OPTIONS } from "../../map/utils";
8
+ import { mapStyleState } from "../../map/map-style-state.svelte.js";
9
+ import { monitorsMapIntegration } from "../../monitors/monitors-map-integration.svelte";
10
+ import { evStationsMapIntegration } from "../../ev-stations/ev-stations-map-integration.svelte";
11
+ </script>
12
+
13
+ <OptionsMenu id="settings" label="Settings" iconOnly alignRight>
14
+ {#snippet icon()}
15
+ <SettingsIcon size={20} color="#696969" />
16
+ {/snippet}
17
+ <CheckboxRow
18
+ label="Marker Clusters"
19
+ bind:checked={
20
+ () => monitorsMapIntegration.clustered && evStationsMapIntegration.clustered,
21
+ (value) => {
22
+ monitorsMapIntegration.clustered = value;
23
+ evStationsMapIntegration.clustered = value;
24
+ }
25
+ }
26
+ >
27
+ {#snippet icon()}
28
+ <NetworkIcon size={18} color="#1e90ff" />
29
+ {/snippet}
30
+ </CheckboxRow>
31
+ <hr class="my-1 border-gray-200" />
32
+ <div class="px-3 py-1.5 text-sm">
33
+ <label for="monitor-map-basemap" class="mb-1 flex items-center gap-2 font-semibold">
34
+ <MapIcon size={18} color="#708090" />
35
+ Basemap
36
+ </label>
37
+ <select
38
+ id="monitor-map-basemap"
39
+ class="w-full rounded-md border border-gray-300 bg-white p-1.5"
40
+ bind:value={
41
+ () => mapStyleState.referenceStyle, (style) => mapStyleState.selectReferenceStyle(style)
42
+ }
43
+ >
44
+ {#each MAP_STYLE_OPTIONS as style (style.getId())}
45
+ <option value={style}>{style.getName()}</option>
46
+ {/each}
47
+ </select>
48
+ </div>
49
+ <hr class="my-1 border-gray-200" />
50
+ <p class="flex items-center gap-2 px-3 pt-1.5 text-sm font-semibold">
51
+ <ContrastIcon size={18} color="#708090" />
52
+ Theme
53
+ </p>
54
+ {#each mapStyleState.referenceStyle.getVariants() as variant (variant.getId())}
55
+ <RadioRow
56
+ name="monitor-map-theme"
57
+ label={variant.getName()}
58
+ checked={variant === mapStyleState.variant}
59
+ onselect={() => mapStyleState.selectVariant(variant)}
60
+ />
61
+ {/each}
62
+ <hr class="my-1 border-gray-200" />
63
+ <div class="px-3 py-1.5">
64
+ <button
65
+ type="button"
66
+ class="flex w-full items-center justify-center gap-2 rounded-md border border-gray-300 px-3 py-1.5 text-sm hover:bg-gray-100"
67
+ onclick={resetMapOptions}
68
+ >
69
+ <RotateCcwIcon size={16} />
70
+ Reset Map Options
71
+ </button>
72
+ </div>
73
+ </OptionsMenu>
@@ -11,8 +11,8 @@ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> =
11
11
  };
12
12
  z_$$bindings?: Bindings;
13
13
  }
14
- declare const HMSFireLegend: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
14
+ declare const SettingsMenu: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
15
  [evt: string]: CustomEvent<any>;
16
16
  }, {}, {}, string>;
17
- type HMSFireLegend = InstanceType<typeof HMSFireLegend>;
18
- export default HMSFireLegend;
17
+ type SettingsMenu = InstanceType<typeof SettingsMenu>;
18
+ export default SettingsMenu;
@@ -0,0 +1,12 @@
1
+ /** Which menus are opened by click: at most one on the toolbar, any number in the panel. */
2
+ export declare class OptionsMenusState {
3
+ private open;
4
+ isOpen(id: string): boolean;
5
+ toggle(id: string): void;
6
+ /** Hovering another toolbar trigger closes any dropdown left open by a click. */
7
+ closeOthers(id: string): void;
8
+ close(id: string): void;
9
+ closeAll(): void;
10
+ }
11
+ export declare function provideOptionsMenus(): OptionsMenusState;
12
+ export declare function useOptionsMenus(): OptionsMenusState;
@@ -0,0 +1,42 @@
1
+ import { getContext, setContext } from "svelte";
2
+ import { SvelteSet } from "svelte/reactivity";
3
+ import { isWideLayout } from "./layout";
4
+ const OPTIONS_MENUS_KEY = Symbol("monitor-map-options-menus");
5
+ /** Which menus are opened by click: at most one on the toolbar, any number in the panel. */
6
+ export class OptionsMenusState {
7
+ open = new SvelteSet();
8
+ isOpen(id) {
9
+ return this.open.has(id);
10
+ }
11
+ toggle(id) {
12
+ if (this.open.has(id)) {
13
+ this.open.delete(id);
14
+ return;
15
+ }
16
+ if (isWideLayout())
17
+ this.open.clear();
18
+ this.open.add(id);
19
+ }
20
+ /** Hovering another toolbar trigger closes any dropdown left open by a click. */
21
+ closeOthers(id) {
22
+ for (const other of [...this.open]) {
23
+ if (other !== id)
24
+ this.open.delete(other);
25
+ }
26
+ }
27
+ close(id) {
28
+ this.open.delete(id);
29
+ }
30
+ closeAll() {
31
+ this.open.clear();
32
+ }
33
+ }
34
+ export function provideOptionsMenus() {
35
+ return setContext(OPTIONS_MENUS_KEY, new OptionsMenusState());
36
+ }
37
+ export function useOptionsMenus() {
38
+ const menus = getContext(OPTIONS_MENUS_KEY);
39
+ if (!menus)
40
+ throw new Error("OptionsMenu must be rendered inside OptionsBar");
41
+ return menus;
42
+ }
@@ -0,0 +1,17 @@
1
+ import type { MonitorsPollutant } from "../monitors/types";
2
+ /**
3
+ * What the Pollutant menu shows. "none" is not a monitorsManager.pollutant value — it means
4
+ * the monitors integration is disabled while the manager keeps its last real pollutant.
5
+ */
6
+ export type PollutantSelection = MonitorsPollutant | "none";
7
+ export interface PollutantTargets {
8
+ manager: {
9
+ pollutant: MonitorsPollutant | null;
10
+ };
11
+ integration: {
12
+ enabled: boolean;
13
+ };
14
+ }
15
+ export declare function parsePollutantParam(value: unknown): PollutantSelection | null;
16
+ export declare function currentSelection(pollutant: MonitorsPollutant | null, monitorsEnabled: boolean): PollutantSelection | null;
17
+ export declare function applyPollutantSelection(selection: PollutantSelection, targets: PollutantTargets): void;
@@ -0,0 +1,14 @@
1
+ export function parsePollutantParam(value) {
2
+ return value === "pm25" || value === "o3" || value === "none" ? value : null;
3
+ }
4
+ export function currentSelection(pollutant, monitorsEnabled) {
5
+ return monitorsEnabled ? pollutant : "none";
6
+ }
7
+ export function applyPollutantSelection(selection, targets) {
8
+ if (selection === "none") {
9
+ targets.integration.enabled = false;
10
+ return;
11
+ }
12
+ targets.manager.pollutant = selection;
13
+ targets.integration.enabled = true;
14
+ }
@@ -0,0 +1,2 @@
1
+ /** "Reset Map Options": restores every map option to its default. */
2
+ export declare function resetMapOptions(): void;
@@ -0,0 +1,24 @@
1
+ import { monitorsManager } from "../monitors/monitors.svelte";
2
+ import { monitorsMapIntegration } from "../monitors/monitors-map-integration.svelte";
3
+ import { collocationSitesMapIntegration } from "../collocation-sites/collocations-map-integration.svelte";
4
+ import { hmsFireMapIntegration } from "../hms/hms-fire-map-integration.svelte";
5
+ import { hmsSmokeMapIntegration } from "../hms/hms-smoke-map-integration.svelte";
6
+ import { evStationsMapIntegration } from "../ev-stations/ev-stations-map-integration.svelte";
7
+ import { windMapIntegration } from "../wind/wind.svelte";
8
+ import { mapStyleState } from "../map/map-style-state.svelte.js";
9
+ import { applyMapOptionDefaults } from "./map-option-defaults";
10
+ /** "Reset Map Options": restores every map option to its default. */
11
+ export function resetMapOptions() {
12
+ applyMapOptionDefaults({
13
+ manager: monitorsManager,
14
+ integration: monitorsMapIntegration,
15
+ collocationSites: collocationSitesMapIntegration,
16
+ hmsFire: hmsFireMapIntegration,
17
+ hmsSmoke: hmsSmokeMapIntegration,
18
+ evStations: evStationsMapIntegration,
19
+ wind: windMapIntegration,
20
+ // The basemap reset may start loading a new style; integrations skip the style while
21
+ // mapManager.styleLoading is set and the refresh on "style.load" re-applies them.
22
+ mapStyle: mapStyleState
23
+ }, monitorsManager.meta?.default_pollutant ?? null);
24
+ }
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { Attachment } from "svelte/attachments";
4
+ import { ChevronDownIcon } from "@lucide/svelte";
5
+
6
+ interface Props {
7
+ label: string;
8
+ checked: boolean;
9
+ icon?: Snippet;
10
+ indeterminate?: boolean;
11
+ disabled?: boolean;
12
+ /** When set, shows a chevron reflecting whether this row's children are expanded */
13
+ expanded?: boolean;
14
+ }
15
+
16
+ let {
17
+ label,
18
+ checked = $bindable(),
19
+ icon,
20
+ indeterminate = false,
21
+ disabled = false,
22
+ expanded
23
+ }: Props = $props();
24
+
25
+ // indeterminate is a DOM property with no HTML attribute; re-runs when the prop changes.
26
+ const syncIndeterminate: Attachment<HTMLInputElement> = (input) => {
27
+ input.indeterminate = indeterminate;
28
+ };
29
+ </script>
30
+
31
+ <label
32
+ class="flex cursor-pointer items-center gap-2 px-3 py-1.5 text-sm whitespace-nowrap select-none hover:bg-gray-100 has-disabled:cursor-not-allowed has-disabled:opacity-50"
33
+ >
34
+ <input
35
+ type="checkbox"
36
+ class="accent-brand size-4 shrink-0"
37
+ bind:checked
38
+ {disabled}
39
+ {@attach syncIndeterminate}
40
+ />
41
+ {#if icon}
42
+ <span class="flex size-5 shrink-0 items-center justify-center">{@render icon()}</span>
43
+ {/if}
44
+ <span class="flex-1">{label}</span>
45
+ {#if expanded !== undefined}
46
+ <ChevronDownIcon
47
+ size={16}
48
+ class="ml-3 shrink-0 text-gray-500 transition-transform {expanded ? '' : '-rotate-90'}"
49
+ />
50
+ {/if}
51
+ </label>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ label: string;
4
+ checked: boolean;
5
+ icon?: Snippet;
6
+ indeterminate?: boolean;
7
+ disabled?: boolean;
8
+ /** When set, shows a chevron reflecting whether this row's children are expanded */
9
+ expanded?: boolean;
10
+ }
11
+ declare const CheckboxRow: import("svelte").Component<Props, {}, "checked">;
12
+ type CheckboxRow = ReturnType<typeof CheckboxRow>;
13
+ export default CheckboxRow;
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import CheckboxRow from "./CheckboxRow.svelte";
4
+ import SubmenuRow from "./SubmenuRow.svelte";
5
+ import { groupState, setGroup } from "../group-state";
6
+
7
+ interface GroupOption {
8
+ key: string;
9
+ label: string;
10
+ option: { value: boolean };
11
+ }
12
+
13
+ interface Props {
14
+ label: string;
15
+ /** Only the rows currently visible; hidden rows are never touched by select-all */
16
+ options: Array<GroupOption>;
17
+ icon?: Snippet;
18
+ }
19
+
20
+ let { label, options, icon }: Props = $props();
21
+
22
+ const status = $derived(groupState(options.map((child) => child.option.value)));
23
+ </script>
24
+
25
+ {#if status !== "empty"}
26
+ <SubmenuRow
27
+ {label}
28
+ {icon}
29
+ indeterminate={status === "indeterminate"}
30
+ bind:checked={
31
+ () => status === "checked",
32
+ (value) =>
33
+ setGroup(
34
+ options.map((child) => child.option),
35
+ value
36
+ )
37
+ }
38
+ >
39
+ {#each options as child (child.key)}
40
+ <CheckboxRow label={child.label} bind:checked={child.option.value} />
41
+ {/each}
42
+ </SubmenuRow>
43
+ {/if}
@@ -0,0 +1,17 @@
1
+ import type { Snippet } from "svelte";
2
+ interface GroupOption {
3
+ key: string;
4
+ label: string;
5
+ option: {
6
+ value: boolean;
7
+ };
8
+ }
9
+ interface Props {
10
+ label: string;
11
+ /** Only the rows currently visible; hidden rows are never touched by select-all */
12
+ options: Array<GroupOption>;
13
+ icon?: Snippet;
14
+ }
15
+ declare const GroupRow: import("svelte").Component<Props, {}, "">;
16
+ type GroupRow = ReturnType<typeof GroupRow>;
17
+ export default GroupRow;
@@ -0,0 +1,10 @@
1
+ <script lang="ts">
2
+ import type { MapImageIcon } from "../../map/integrations/types";
3
+
4
+ let { icon, class: className = "w-4" }: { icon: MapImageIcon | undefined; class?: string } =
5
+ $props();
6
+ </script>
7
+
8
+ {#if icon}
9
+ <img src={icon.image.src} alt="" class={className} />
10
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { MapImageIcon } from "../../map/integrations/types";
2
+ type $$ComponentProps = {
3
+ icon: MapImageIcon | undefined;
4
+ class?: string;
5
+ };
6
+ declare const MarkerIcon: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type MarkerIcon = ReturnType<typeof MarkerIcon>;
8
+ export default MarkerIcon;
@@ -0,0 +1,33 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+
4
+ interface Props {
5
+ name: string;
6
+ label: string;
7
+ checked: boolean;
8
+ onselect: () => void;
9
+ unit?: string;
10
+ help?: string;
11
+ icon?: Snippet;
12
+ }
13
+
14
+ let { name, label, checked, onselect, unit, help, icon }: Props = $props();
15
+ </script>
16
+
17
+ <label
18
+ class="flex cursor-pointer flex-col gap-0.5 px-3 py-1.5 text-sm select-none hover:bg-gray-100"
19
+ >
20
+ <span class="flex items-center gap-2 whitespace-nowrap">
21
+ <input type="radio" class="accent-brand size-4 shrink-0" {name} {checked} onchange={onselect} />
22
+ {#if icon}
23
+ <span class="flex size-5 shrink-0 items-center justify-center">{@render icon()}</span>
24
+ {/if}
25
+ <span>
26
+ {label}
27
+ {#if unit}<span class="text-xs text-gray-500">({unit})</span>{/if}
28
+ </span>
29
+ </span>
30
+ {#if help}
31
+ <span class="pl-6 text-xs whitespace-normal text-gray-500">{help}</span>
32
+ {/if}
33
+ </label>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ name: string;
4
+ label: string;
5
+ checked: boolean;
6
+ onselect: () => void;
7
+ unit?: string;
8
+ help?: string;
9
+ icon?: Snippet;
10
+ }
11
+ declare const RadioRow: import("svelte").Component<Props, {}, "">;
12
+ type RadioRow = ReturnType<typeof RadioRow>;
13
+ export default RadioRow;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import CheckboxRow from "./CheckboxRow.svelte";
4
+
5
+ interface Props {
6
+ label: string;
7
+ checked: boolean;
8
+ indeterminate?: boolean;
9
+ icon?: Snippet;
10
+ children: Snippet;
11
+ }
12
+
13
+ let { label, checked = $bindable(), indeterminate = false, icon, children }: Props = $props();
14
+
15
+ // Children show only while the parent is (partly) on, as in the demo.
16
+ const expanded = $derived(checked || indeterminate);
17
+ </script>
18
+
19
+ <CheckboxRow {label} {icon} {indeterminate} {expanded} bind:checked />
20
+ {#if expanded}
21
+ <div class="ml-5 border-l border-gray-200">{@render children()}</div>
22
+ {/if}
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
3
+ label: string;
4
+ checked: boolean;
5
+ indeterminate?: boolean;
6
+ icon?: Snippet;
7
+ children: Snippet;
8
+ }
9
+ declare const SubmenuRow: import("svelte").Component<Props, {}, "checked">;
10
+ type SubmenuRow = ReturnType<typeof SubmenuRow>;
11
+ export default SubmenuRow;
@@ -111,19 +111,19 @@
111
111
 
112
112
  <div
113
113
  class={[
114
- "flex flex-col overflow-hidden bg-white shadow-md transition-all duration-300 select-none",
115
- collapsed ? "h-12 w-12 rounded-full" : "w-90 rounded-3xl",
116
- !collapsed && results.length ? "max-h-90" : "max-h-12"
114
+ "flex flex-col overflow-hidden bg-white shadow transition-all duration-300 select-none",
115
+ collapsed ? "h-10 w-10 rounded-md" : "w-[min(22.5rem,calc(100vw-5.5rem))] rounded-lg",
116
+ !collapsed && results.length ? "max-h-90" : "max-h-10"
117
117
  ]}
118
118
  use:clickOutside
119
119
  >
120
- <div class="flex h-12 w-full shrink-0 items-center">
120
+ <div class="flex h-10 w-full shrink-0 items-center">
121
121
  <button
122
- class="bg-brand flex h-12 w-12 shrink-0 items-center justify-center rounded-full border-4 border-white"
122
+ class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md text-gray-700"
123
123
  onclick={openSearch}
124
124
  aria-label="Open search"
125
125
  >
126
- <SearchIcon size="24" color="#FFFFFF" />
126
+ <SearchIcon size="20" />
127
127
  </button>
128
128
  <div class="flex h-full w-full items-center overflow-hidden">
129
129
  <input
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sjvair/monitor-map",
3
3
  "description": "SJVAir's interactive monitor map",
4
- "version": "3.6.3",
4
+ "version": "4.0.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SJVAir/monitor-map"
@@ -15,7 +15,8 @@
15
15
  "check": "svelte-check --tsconfig ./tsconfig.json",
16
16
  "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
17
17
  "format": "prettier --write .",
18
- "lint": "prettier --check . && eslint ."
18
+ "lint": "prettier --check . && eslint .",
19
+ "test": "vitest run"
19
20
  },
20
21
  "type": "module",
21
22
  "svelte": "./dist/lib/index.js",
@@ -65,7 +66,8 @@
65
66
  "tw-animate-css": "^1.4.0",
66
67
  "typescript": "^6.0.3",
67
68
  "typescript-eslint": "^8.70.0",
68
- "vite": "^8.3.0"
69
+ "vite": "^8.3.0",
70
+ "vitest": "^5.0.3"
69
71
  },
70
72
  "dependencies": {
71
73
  "@lucide/svelte": "^1.46.0",
@@ -1,10 +1,25 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import { onDestroy, untrack } from "svelte";
4
+ import { searchParams } from "sv-router";
4
5
  import MapShell from "$lib/map/MapShell.svelte";
5
- import MonitorsDisplayOptions from "$lib/monitors/components/MonitorsDisplayOptions.svelte";
6
- import MapLayersDisplayOptions from "$lib/components/MapLayersDisplayOptions.svelte";
7
- import MapStyleDisplayOptions from "$lib/map/MapStyleDisplayOptions.svelte";
6
+ import OptionsGroup from "$lib/options/OptionsGroup.svelte";
7
+ import PollutantMenu from "$lib/options/menus/PollutantMenu.svelte";
8
+ import MonitorsMenu from "$lib/options/menus/MonitorsMenu.svelte";
9
+ import LayersMenu from "$lib/options/menus/LayersMenu.svelte";
10
+ import OverlaysMenu from "$lib/options/menus/OverlaysMenu.svelte";
11
+ import SettingsMenu from "$lib/options/menus/SettingsMenu.svelte";
12
+ import {
13
+ applyPollutantSelection,
14
+ currentSelection,
15
+ parsePollutantParam
16
+ } from "$lib/options/pollutant-param";
17
+ import Legend from "$lib/legend/Legend.svelte";
18
+ import {
19
+ fireSmokeLegendSections,
20
+ pollutantLegendSection,
21
+ type LegendSection
22
+ } from "$lib/legend/legend-data";
8
23
  import { monitorsManager } from "$lib/monitors/monitors.svelte";
9
24
  import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
10
25
  import { windMapIntegration } from "$lib/wind/wind.svelte";
@@ -12,14 +27,12 @@
12
27
  import type { SomeMapIntegration } from "$lib/map/integrations/types";
13
28
  import { collocationSitesManager } from "$lib/collocation-sites/collocations.svelte";
14
29
  import { collocationSitesMapIntegration } from "$lib/collocation-sites/collocations-map-integration.svelte";
15
- import EvStationsDisplayOptions from "$lib/ev-stations/components/EvStationsDisplayOptions.svelte";
16
30
  import { evStationsMapIntegration } from "$lib/ev-stations/ev-stations-map-integration.svelte";
17
31
  import { hmsManager } from "$lib/hms/hms.svelte";
18
32
  import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte";
19
- import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte";
20
- import MapLegend from "$lib/MapLegend.svelte";
33
+ import { FIRE_LEGEND_CATEGORIES } from "$lib/hms/hms-fire-icon-manager";
34
+ import { hmsSmokeMapIntegration, SMOKE_LEGEND } from "$lib/hms/hms-smoke-map-integration.svelte";
21
35
  import Search from "$lib/search/Search.svelte";
22
- import { searchParams } from "sv-router";
23
36
  import { useMonitorMapRouter } from "./router-context";
24
37
 
25
38
  interface Props {
@@ -40,6 +53,14 @@
40
53
  evStationsMapIntegration
41
54
  ];
42
55
 
56
+ const pollutantTargets = { manager: monitorsManager, integration: monitorsMapIntegration };
57
+
58
+ // "?pollutant=none" must disable monitors before the effects below first run, so the
59
+ // state->URL effect never sees enabled monitors and overwrites the param.
60
+ if (parsePollutantParam(route.search.pollutant) === "none") {
61
+ monitorsMapIntegration.enabled = false;
62
+ }
63
+
43
64
  monitorsManager.init(route.search.pollutant);
44
65
  collocationSitesManager.init();
45
66
  hmsManager.init();
@@ -49,30 +70,30 @@
49
70
 
50
71
  let panelOpen = $derived(route.pathname.startsWith(`${basePath}/monitor/`));
51
72
 
52
- // Keep monitorsManager.pollutant and the "pollutant" URL param in sync, both directions.
53
- // init() only seeds pollutant on the manager's first-ever initialization; this effect is what
54
- // applies a later "?pollutant=" change (e.g. navigating in from elsewhere) to an already-running manager.
73
+ // Keep the Pollutant menu's selection (pm25 / o3 / none) and the "pollutant" URL param in
74
+ // sync, both directions. init() only seeds the pollutant on the manager's first-ever
75
+ // initialization; this effect applies a later "?pollutant=" change (e.g. navigating in from
76
+ // elsewhere) to an already-running map.
55
77
  $effect(() => {
56
- const urlPollutant = route.search.pollutant;
57
- // Read monitorsManager.pollutant via untrack: this effect must only react to the URL
58
- // changing (e.g. navigating in from elsewhere), not to monitorsManager.pollutant itself.
59
- // Tracking it here would make a UI-driven pollutant change (e.g. the display-options
60
- // toggle) re-trigger this effect before the manager->URL effect below can sync the URL,
61
- // so this effect would see the still-stale URL and immediately revert the user's change.
62
- if (
63
- monitorsManager.initialized &&
64
- (urlPollutant === "pm25" || urlPollutant === "o3") &&
65
- untrack(() => monitorsManager.pollutant) !== urlPollutant
66
- ) {
67
- monitorsManager.pollutant = urlPollutant;
68
- }
78
+ const selection = parsePollutantParam(route.search.pollutant);
79
+ if (!monitorsManager.initialized || !selection) return;
80
+ // Read current state via untrack: this effect must only react to the URL changing. Tracking
81
+ // state here would make a UI-driven change re-trigger this effect before the effect below
82
+ // syncs the URL, so it would see the stale URL and revert the user's change.
83
+ untrack(() => {
84
+ if (
85
+ currentSelection(monitorsManager.pollutant, monitorsMapIntegration.enabled) !== selection
86
+ ) {
87
+ applyPollutantSelection(selection, pollutantTargets);
88
+ }
89
+ });
69
90
  });
70
91
 
71
- // ...and the reverse: reflect UI-driven pollutant changes (e.g. the display-options toggle) back to the URL.
92
+ // ...and the reverse: reflect UI-driven changes back to the URL.
72
93
  $effect(() => {
73
- const pollutant = monitorsManager.pollutant;
74
- if (pollutant && searchParams.get("pollutant") !== pollutant) {
75
- searchParams.set("pollutant", pollutant);
94
+ const selection = currentSelection(monitorsManager.pollutant, monitorsMapIntegration.enabled);
95
+ if (selection && searchParams.get("pollutant") !== selection) {
96
+ searchParams.set("pollutant", selection);
76
97
  }
77
98
  });
78
99
 
@@ -82,6 +103,18 @@
82
103
  monitorsMapIntegration.selectedMonitorId = null;
83
104
  });
84
105
 
106
+ const legendSections: Array<LegendSection> = $derived.by(() => {
107
+ const sections: Array<LegendSection> = [];
108
+ if (monitorsMapIntegration.enabled && monitorsManager.pollutant && monitorsManager.levels) {
109
+ const pollutant = pollutantLegendSection(monitorsManager.pollutant, monitorsManager.levels);
110
+ if (pollutant) sections.push(pollutant);
111
+ }
112
+ if (hmsFireMapIntegration.enabled && hmsSmokeMapIntegration.enabled) {
113
+ sections.push(...fireSmokeLegendSections(FIRE_LEGEND_CATEGORIES, SMOKE_LEGEND));
114
+ }
115
+ return sections;
116
+ });
117
+
85
118
  onDestroy(() => {
86
119
  monitorsManager.autoUpdate.stop();
87
120
  });
@@ -95,17 +128,22 @@
95
128
  {basePath}
96
129
  >
97
130
  {#snippet menu()}
98
- <MonitorsDisplayOptions />
99
- <EvStationsDisplayOptions />
100
- <MapLayersDisplayOptions />
101
- <MapStyleDisplayOptions />
131
+ <OptionsGroup>
132
+ <PollutantMenu />
133
+ <MonitorsMenu />
134
+ </OptionsGroup>
135
+ <OptionsGroup>
136
+ <LayersMenu />
137
+ <OverlaysMenu />
138
+ <SettingsMenu />
139
+ </OptionsGroup>
140
+ {/snippet}
141
+ {#snippet search()}
142
+ <Search />
102
143
  {/snippet}
103
144
  {#snippet overlays()}
104
- <div class="pointer-events-none absolute bottom-0 left-0 z-10">
105
- <MapLegend />
106
- </div>
107
- <div class="absolute top-4 left-20 z-10">
108
- <Search />
145
+ <div class="pointer-events-none absolute bottom-4 left-4 z-10">
146
+ <Legend sections={legendSections} />
109
147
  </div>
110
148
  {/snippet}
111
149
  {@render children()}