@sjvair/monitor-map 3.6.2 → 4.0.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 (152) hide show
  1. package/README.md +2 -2
  2. package/dist/lib/MonitorMapLayout.svelte +74 -36
  3. package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +3 -0
  4. package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +15 -1
  5. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +27 -6
  6. package/dist/lib/hms/hms-fire-icon-manager.d.ts +8 -0
  7. package/dist/lib/hms/hms-fire-icon-manager.js +10 -1
  8. package/dist/lib/hms/hms-fire-map-integration.svelte.js +1 -1
  9. package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +20 -0
  10. package/dist/lib/hms/hms-smoke-map-integration.svelte.js +22 -7
  11. package/dist/lib/index.d.ts +21 -7
  12. package/dist/lib/index.js +22 -7
  13. package/dist/lib/legend/Legend.svelte +40 -0
  14. package/dist/lib/legend/Legend.svelte.d.ts +7 -0
  15. package/dist/lib/legend/LegendBar.svelte +45 -0
  16. package/dist/lib/legend/LegendBar.svelte.d.ts +7 -0
  17. package/dist/lib/legend/legend-data.d.ts +40 -0
  18. package/dist/lib/legend/legend-data.js +65 -0
  19. package/dist/lib/map/MapShell.svelte +7 -6
  20. package/dist/lib/map/MapShell.svelte.d.ts +3 -1
  21. package/dist/lib/map/basemap.d.ts +14 -0
  22. package/dist/lib/map/basemap.js +10 -0
  23. package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +6 -0
  24. package/dist/lib/map/integrations/map-geojson-integration.svelte.js +15 -2
  25. package/dist/lib/map/integrations/map-layer-integration.svelte.js +7 -3
  26. package/dist/lib/map/map-style-state.svelte.d.ts +13 -0
  27. package/dist/lib/map/map-style-state.svelte.js +35 -0
  28. package/dist/lib/map/map.svelte.d.ts +6 -0
  29. package/dist/lib/map/map.svelte.js +6 -0
  30. package/dist/lib/monitors/monitor-groups.d.ts +11 -0
  31. package/dist/lib/monitors/monitor-groups.js +7 -0
  32. package/dist/lib/monitors/monitor-utils.js +1 -3
  33. package/dist/lib/monitors/monitors-icon-manager.svelte.js +2 -5
  34. package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +4 -4
  35. package/dist/lib/monitors/monitors-map-integration.svelte.js +43 -30
  36. package/dist/lib/monitors/monitors.svelte.d.ts +1 -0
  37. package/dist/lib/monitors/monitors.svelte.js +22 -0
  38. package/dist/lib/options/OptionsBar.svelte +96 -0
  39. package/dist/lib/options/OptionsBar.svelte.d.ts +10 -0
  40. package/dist/lib/options/OptionsGroup.svelte +12 -0
  41. package/dist/lib/options/OptionsGroup.svelte.d.ts +7 -0
  42. package/dist/lib/options/OptionsMenu.svelte +71 -0
  43. package/dist/lib/options/OptionsMenu.svelte.d.ts +15 -0
  44. package/dist/lib/options/group-state.d.ts +7 -0
  45. package/dist/lib/options/group-state.js +15 -0
  46. package/dist/lib/options/layout.d.ts +7 -0
  47. package/dist/lib/options/layout.js +9 -0
  48. package/dist/lib/options/map-option-defaults.d.ts +56 -0
  49. package/dist/lib/options/map-option-defaults.js +47 -0
  50. package/dist/lib/options/menus/LayersMenu.svelte +47 -0
  51. package/dist/lib/options/menus/LayersMenu.svelte.d.ts +3 -0
  52. package/dist/lib/options/menus/MonitorsMenu.svelte +68 -0
  53. package/dist/lib/options/menus/MonitorsMenu.svelte.d.ts +3 -0
  54. package/dist/lib/options/menus/OverlaysMenu.svelte +18 -0
  55. package/dist/lib/{MapLegend.svelte.d.ts → options/menus/OverlaysMenu.svelte.d.ts} +3 -3
  56. package/dist/lib/options/menus/PollutantMenu.svelte +79 -0
  57. package/dist/lib/options/menus/PollutantMenu.svelte.d.ts +3 -0
  58. package/dist/lib/options/menus/SettingsMenu.svelte +73 -0
  59. package/dist/lib/{hms/components/HMSFireLegend.svelte.d.ts → options/menus/SettingsMenu.svelte.d.ts} +3 -3
  60. package/dist/lib/options/options-state.svelte.d.ts +12 -0
  61. package/dist/lib/options/options-state.svelte.js +42 -0
  62. package/dist/lib/options/pollutant-param.d.ts +17 -0
  63. package/dist/lib/options/pollutant-param.js +14 -0
  64. package/dist/lib/options/reset-map-options.d.ts +2 -0
  65. package/dist/lib/options/reset-map-options.js +24 -0
  66. package/dist/lib/options/rows/CheckboxRow.svelte +51 -0
  67. package/dist/lib/options/rows/CheckboxRow.svelte.d.ts +13 -0
  68. package/dist/lib/options/rows/GroupRow.svelte +43 -0
  69. package/dist/lib/options/rows/GroupRow.svelte.d.ts +17 -0
  70. package/dist/lib/options/rows/MarkerIcon.svelte +10 -0
  71. package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +8 -0
  72. package/dist/lib/options/rows/RadioRow.svelte +33 -0
  73. package/dist/lib/options/rows/RadioRow.svelte.d.ts +13 -0
  74. package/dist/lib/options/rows/SubmenuRow.svelte +22 -0
  75. package/dist/lib/options/rows/SubmenuRow.svelte.d.ts +11 -0
  76. package/dist/lib/search/Search.svelte +6 -6
  77. package/package.json +6 -4
  78. package/src/lib/MonitorMapLayout.svelte +74 -36
  79. package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +17 -1
  80. package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +26 -6
  81. package/src/lib/hms/hms-fire-icon-manager.ts +16 -2
  82. package/src/lib/hms/hms-fire-map-integration.svelte.ts +1 -1
  83. package/src/lib/hms/hms-smoke-map-integration.svelte.ts +25 -7
  84. package/src/lib/index.ts +31 -7
  85. package/src/lib/legend/Legend.svelte +40 -0
  86. package/src/lib/legend/LegendBar.svelte +45 -0
  87. package/src/lib/legend/legend-data.ts +101 -0
  88. package/src/lib/map/MapShell.svelte +7 -6
  89. package/src/lib/map/basemap.ts +24 -0
  90. package/src/lib/map/integrations/map-geojson-integration.svelte.ts +15 -2
  91. package/src/lib/map/integrations/map-layer-integration.svelte.ts +6 -3
  92. package/src/lib/map/map-style-state.svelte.ts +42 -0
  93. package/src/lib/map/map.svelte.ts +6 -0
  94. package/src/lib/monitors/monitor-groups.ts +20 -0
  95. package/src/lib/monitors/monitor-utils.ts +1 -5
  96. package/src/lib/monitors/monitors-icon-manager.svelte.ts +2 -8
  97. package/src/lib/monitors/monitors-map-integration.svelte.ts +37 -27
  98. package/src/lib/monitors/monitors.svelte.ts +22 -0
  99. package/src/lib/options/OptionsBar.svelte +96 -0
  100. package/src/lib/options/OptionsGroup.svelte +12 -0
  101. package/src/lib/options/OptionsMenu.svelte +71 -0
  102. package/src/lib/options/group-state.ts +16 -0
  103. package/src/lib/options/layout.ts +10 -0
  104. package/src/lib/options/map-option-defaults.ts +71 -0
  105. package/src/lib/options/menus/LayersMenu.svelte +47 -0
  106. package/src/lib/options/menus/MonitorsMenu.svelte +68 -0
  107. package/src/lib/options/menus/OverlaysMenu.svelte +18 -0
  108. package/src/lib/options/menus/PollutantMenu.svelte +79 -0
  109. package/src/lib/options/menus/SettingsMenu.svelte +73 -0
  110. package/src/lib/options/options-state.svelte.ts +48 -0
  111. package/src/lib/options/pollutant-param.ts +35 -0
  112. package/src/lib/options/reset-map-options.ts +28 -0
  113. package/src/lib/options/rows/CheckboxRow.svelte +51 -0
  114. package/src/lib/options/rows/GroupRow.svelte +43 -0
  115. package/src/lib/options/rows/MarkerIcon.svelte +10 -0
  116. package/src/lib/options/rows/RadioRow.svelte +33 -0
  117. package/src/lib/options/rows/SubmenuRow.svelte +22 -0
  118. package/src/lib/search/Search.svelte +6 -6
  119. package/dist/lib/MapLegend.svelte +0 -15
  120. package/dist/lib/collocation-sites/collocation-utils.d.ts +0 -3
  121. package/dist/lib/collocation-sites/collocation-utils.js +0 -30
  122. package/dist/lib/components/DisplayOption.svelte +0 -7
  123. package/dist/lib/components/DisplayOption.svelte.d.ts +0 -5
  124. package/dist/lib/components/MapLayersDisplayOptions.svelte +0 -42
  125. package/dist/lib/components/MapLayersDisplayOptions.svelte.d.ts +0 -18
  126. package/dist/lib/components/SegmentedControl.svelte +0 -52
  127. package/dist/lib/components/SegmentedControl.svelte.d.ts +0 -11
  128. package/dist/lib/components/ToggleSwitch.svelte +0 -93
  129. package/dist/lib/components/ToggleSwitch.svelte.d.ts +0 -9
  130. package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
  131. package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte.d.ts +0 -18
  132. package/dist/lib/hms/components/HMSFireLegend.svelte +0 -21
  133. package/dist/lib/map/MapStyleDisplayOptions.svelte +0 -61
  134. package/dist/lib/map/MapStyleDisplayOptions.svelte.d.ts +0 -3
  135. package/dist/lib/map/Menu.svelte +0 -65
  136. package/dist/lib/map/Menu.svelte.d.ts +0 -5
  137. package/dist/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
  138. package/dist/lib/monitors/components/MonitorMarkersLegend.svelte.d.ts +0 -3
  139. package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte +0 -85
  140. package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte.d.ts +0 -3
  141. package/src/lib/MapLegend.svelte +0 -15
  142. package/src/lib/collocation-sites/collocation-utils.ts +0 -41
  143. package/src/lib/components/DisplayOption.svelte +0 -7
  144. package/src/lib/components/MapLayersDisplayOptions.svelte +0 -42
  145. package/src/lib/components/SegmentedControl.svelte +0 -52
  146. package/src/lib/components/ToggleSwitch.svelte +0 -93
  147. package/src/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
  148. package/src/lib/hms/components/HMSFireLegend.svelte +0 -21
  149. package/src/lib/map/MapStyleDisplayOptions.svelte +0 -61
  150. package/src/lib/map/Menu.svelte +0 -65
  151. package/src/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
  152. package/src/lib/monitors/components/MonitorsDisplayOptions.svelte +0 -85
@@ -0,0 +1,71 @@
1
+ import type { MonitorsPollutant } from "$lib/monitors/types";
2
+ import { applyPollutantSelection, type PollutantTargets } from "./pollutant-param";
3
+
4
+ /**
5
+ * Defaults restored by "Reset Map Options". Keep in sync with the constructor defaults in each
6
+ * integration (monitors/EV display options, `enabled`, `clustered`) so a reset matches a fresh load.
7
+ */
8
+ export const MAP_OPTION_DEFAULTS = {
9
+ monitorTypes: {
10
+ airnow: true,
11
+ aqlite: true,
12
+ aqview: true,
13
+ bam1022: true,
14
+ airgradient: true,
15
+ purpleair: true,
16
+ vozbox: true,
17
+ inactive: false,
18
+ inside: false
19
+ },
20
+ clustered: true,
21
+ collocationSites: false,
22
+ fireAndSmoke: true,
23
+ evStations: false,
24
+ evLevels: { lvl2: true, lvl3: true },
25
+ wind: false
26
+ } as const;
27
+
28
+ type Toggle = { enabled: boolean };
29
+ type Option = { value: boolean };
30
+
31
+ export interface MapOptionTargets extends PollutantTargets {
32
+ integration: PollutantTargets["integration"] & {
33
+ clustered: boolean;
34
+ displayOptions: Record<string, Option>;
35
+ };
36
+ collocationSites: Toggle;
37
+ hmsFire: Toggle;
38
+ hmsSmoke: Toggle;
39
+ evStations: Toggle & { clustered: boolean; displayOptions: { lvl2: Option; lvl3: Option } };
40
+ wind: Toggle;
41
+ mapStyle: { reset(): void };
42
+ }
43
+
44
+ export function applyMapOptionDefaults(
45
+ targets: MapOptionTargets,
46
+ defaultPollutant: MonitorsPollutant | null
47
+ ): void {
48
+ if (defaultPollutant) {
49
+ applyPollutantSelection(defaultPollutant, targets);
50
+ } else {
51
+ targets.integration.enabled = true;
52
+ }
53
+
54
+ for (const [key, value] of Object.entries(MAP_OPTION_DEFAULTS.monitorTypes)) {
55
+ const option = targets.integration.displayOptions[key];
56
+ if (option) option.value = value;
57
+ }
58
+ targets.integration.clustered = MAP_OPTION_DEFAULTS.clustered;
59
+
60
+ targets.collocationSites.enabled = MAP_OPTION_DEFAULTS.collocationSites;
61
+ targets.hmsFire.enabled = MAP_OPTION_DEFAULTS.fireAndSmoke;
62
+ targets.hmsSmoke.enabled = MAP_OPTION_DEFAULTS.fireAndSmoke;
63
+
64
+ targets.evStations.enabled = MAP_OPTION_DEFAULTS.evStations;
65
+ targets.evStations.clustered = MAP_OPTION_DEFAULTS.clustered;
66
+ targets.evStations.displayOptions.lvl2.value = MAP_OPTION_DEFAULTS.evLevels.lvl2;
67
+ targets.evStations.displayOptions.lvl3.value = MAP_OPTION_DEFAULTS.evLevels.lvl3;
68
+
69
+ targets.wind.enabled = MAP_OPTION_DEFAULTS.wind;
70
+ targets.mapStyle.reset();
71
+ }
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import { FlameIcon, LayersIcon, EvChargerIcon } from "@lucide/svelte";
3
+ import OptionsMenu from "../OptionsMenu.svelte";
4
+ import CheckboxRow from "../rows/CheckboxRow.svelte";
5
+ import SubmenuRow from "../rows/SubmenuRow.svelte";
6
+ import MarkerIcon from "../rows/MarkerIcon.svelte";
7
+ import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte";
8
+ import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte";
9
+ import { evStationsMapIntegration } from "$lib/ev-stations/ev-stations-map-integration.svelte";
10
+
11
+ const levels = $derived(evStationsMapIntegration.displayOptions);
12
+ </script>
13
+
14
+ <OptionsMenu id="layers" label="Layers">
15
+ {#snippet icon()}
16
+ <LayersIcon size={20} color="#708090" />
17
+ {/snippet}
18
+ <CheckboxRow
19
+ label="Fire & Smoke"
20
+ bind:checked={
21
+ () => hmsFireMapIntegration.enabled && hmsSmokeMapIntegration.enabled,
22
+ (value) => {
23
+ hmsFireMapIntegration.enabled = value;
24
+ hmsSmokeMapIntegration.enabled = value;
25
+ }
26
+ }
27
+ >
28
+ {#snippet icon()}
29
+ <FlameIcon size={18} color="#ff4500" />
30
+ {/snippet}
31
+ </CheckboxRow>
32
+ <SubmenuRow label="EV Chargers" bind:checked={evStationsMapIntegration.enabled}>
33
+ {#snippet icon()}
34
+ <EvChargerIcon size={18} color="#708090" />
35
+ {/snippet}
36
+ <CheckboxRow label="Level 2" bind:checked={evStationsMapIntegration.displayOptions.lvl2.value}>
37
+ {#snippet icon()}
38
+ <MarkerIcon icon={levels.lvl2.icon} class="w-5" />
39
+ {/snippet}
40
+ </CheckboxRow>
41
+ <CheckboxRow label="Level 3" bind:checked={evStationsMapIntegration.displayOptions.lvl3.value}>
42
+ {#snippet icon()}
43
+ <MarkerIcon icon={levels.lvl3.icon} class="w-5" />
44
+ {/snippet}
45
+ </CheckboxRow>
46
+ </SubmenuRow>
47
+ </OptionsMenu>
@@ -0,0 +1,68 @@
1
+ <script lang="ts">
2
+ import { CrosshairIcon, RadioTowerIcon } from "@lucide/svelte";
3
+ import OptionsMenu from "../OptionsMenu.svelte";
4
+ import CheckboxRow from "../rows/CheckboxRow.svelte";
5
+ import GroupRow from "../rows/GroupRow.svelte";
6
+ import MarkerIcon from "../rows/MarkerIcon.svelte";
7
+ import { monitorsManager } from "$lib/monitors/monitors.svelte";
8
+ import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
9
+ import { collocationSitesMapIntegration } from "$lib/collocation-sites/collocations-map-integration.svelte";
10
+ import {
11
+ LOW_COST_TYPES,
12
+ REFERENCE_GRADE_TYPES,
13
+ typeSupportsPollutant,
14
+ type MonitorGroupType
15
+ } from "$lib/monitors/monitor-groups";
16
+
17
+ // Rows for types that don't report the current pollutant are hidden (meta-driven).
18
+ function visibleRows(types: ReadonlyArray<MonitorGroupType>) {
19
+ const displayOptions = monitorsMapIntegration.displayOptions;
20
+ return types
21
+ .filter((type) =>
22
+ typeSupportsPollutant(monitorsManager.meta, type, monitorsManager.pollutant)
23
+ )
24
+ .map((type) => ({
25
+ key: type,
26
+ label: displayOptions[type].label,
27
+ option: displayOptions[type]
28
+ }));
29
+ }
30
+
31
+ const referenceGrade = $derived(visibleRows(REFERENCE_GRADE_TYPES));
32
+ const lowCost = $derived(visibleRows(LOW_COST_TYPES));
33
+ const icons = monitorsMapIntegration.icons;
34
+ </script>
35
+
36
+ <OptionsMenu id="monitors" label="Monitors" disabled={!monitorsMapIntegration.enabled}>
37
+ {#snippet icon()}
38
+ <RadioTowerIcon size={20} color="#4682b4" />
39
+ {/snippet}
40
+ <GroupRow label="Reference-grade" options={referenceGrade}>
41
+ {#snippet icon()}
42
+ <MarkerIcon icon={icons.get("outside-display-triangle")} />
43
+ {/snippet}
44
+ </GroupRow>
45
+ <GroupRow label="Low-cost sensors" options={lowCost}>
46
+ {#snippet icon()}
47
+ <MarkerIcon icon={icons.get("outside-display-circle")} />
48
+ {/snippet}
49
+ </GroupRow>
50
+ <hr class="my-1 border-gray-200" />
51
+ <CheckboxRow label="Inactive" bind:checked={monitorsMapIntegration.displayOptions.inactive.value}>
52
+ {#snippet icon()}
53
+ <MarkerIcon icon={monitorsMapIntegration.displayOptions.inactive.icon} />
54
+ {/snippet}
55
+ </CheckboxRow>
56
+ <CheckboxRow label="Inside" bind:checked={monitorsMapIntegration.displayOptions.inside.value}>
57
+ {#snippet icon()}
58
+ <MarkerIcon icon={monitorsMapIntegration.displayOptions.inside.icon} />
59
+ {/snippet}
60
+ </CheckboxRow>
61
+ {#if monitorsManager.pollutant === "pm25"}
62
+ <CheckboxRow label="Collocation Sites" bind:checked={collocationSitesMapIntegration.enabled}>
63
+ {#snippet icon()}
64
+ <CrosshairIcon size={16} color="#4A5FC6" />
65
+ {/snippet}
66
+ </CheckboxRow>
67
+ {/if}
68
+ </OptionsMenu>
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import { SquareStackIcon, WindIcon } from "@lucide/svelte";
3
+ import OptionsMenu from "../OptionsMenu.svelte";
4
+ import CheckboxRow from "../rows/CheckboxRow.svelte";
5
+ import { windMapIntegration } from "$lib/wind/wind.svelte";
6
+ </script>
7
+
8
+ <!-- Independent checkboxes: several overlays may be on at once. -->
9
+ <OptionsMenu id="overlays" label="Overlays" alignRight>
10
+ {#snippet icon()}
11
+ <SquareStackIcon size={20} color="#708090" />
12
+ {/snippet}
13
+ <CheckboxRow label="Wind" bind:checked={windMapIntegration.enabled}>
14
+ {#snippet icon()}
15
+ <WindIcon size={18} color="#1e90ff" />
16
+ {/snippet}
17
+ </CheckboxRow>
18
+ </OptionsMenu>
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ import { BanIcon, HazeIcon, SunDimIcon } from "@lucide/svelte";
3
+ import OptionsMenu from "../OptionsMenu.svelte";
4
+ import RadioRow from "../rows/RadioRow.svelte";
5
+ import { useOptionsMenus } from "../options-state.svelte.js";
6
+ import { isWideLayout } from "../layout";
7
+ import {
8
+ applyPollutantSelection,
9
+ currentSelection,
10
+ type PollutantSelection
11
+ } from "../pollutant-param";
12
+ import { monitorsManager } from "$lib/monitors/monitors.svelte";
13
+ import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
14
+
15
+ interface PollutantOption {
16
+ value: PollutantSelection;
17
+ label: string;
18
+ unit?: string;
19
+ help?: string;
20
+ icon: typeof HazeIcon;
21
+ color: string;
22
+ }
23
+
24
+ const options: Array<PollutantOption> = [
25
+ {
26
+ value: "pm25",
27
+ label: "PM2.5",
28
+ unit: "µg/m³",
29
+ help: "Fine particles from smoke, dust, and vehicle exhaust.",
30
+ icon: HazeIcon,
31
+ color: "#bc8f8f"
32
+ },
33
+ {
34
+ value: "o3",
35
+ label: "Ozone",
36
+ unit: "ppb",
37
+ help: "Ground-level gas that forms in sunlight on hot days.",
38
+ icon: SunDimIcon,
39
+ color: "#daa520"
40
+ },
41
+ { value: "none", label: "None", icon: BanIcon, color: "#778899" }
42
+ ];
43
+
44
+ const menus = useOptionsMenus();
45
+ const selection = $derived(
46
+ currentSelection(monitorsManager.pollutant, monitorsMapIntegration.enabled)
47
+ );
48
+ const selected = $derived(options.find((o) => o.value === selection) ?? options[0]);
49
+
50
+ function select(value: PollutantSelection) {
51
+ applyPollutantSelection(value, {
52
+ manager: monitorsManager,
53
+ integration: monitorsMapIntegration
54
+ });
55
+ // Toolbar dropdown closes on pick; the panel section stays open.
56
+ if (isWideLayout()) menus.close("pollutant");
57
+ }
58
+ </script>
59
+
60
+ <OptionsMenu id="pollutant" label={selected.label}>
61
+ {#snippet icon()}
62
+ <selected.icon size={20} color={selected.color} />
63
+ {/snippet}
64
+ {#each options as option, i (option.value)}
65
+ {#if i > 0}<hr class="my-1 border-gray-200" />{/if}
66
+ <RadioRow
67
+ name="monitor-map-pollutant"
68
+ label={option.label}
69
+ unit={option.unit}
70
+ help={option.help}
71
+ checked={selection === option.value}
72
+ onselect={() => select(option.value)}
73
+ >
74
+ {#snippet icon()}
75
+ <option.icon size={18} color={option.color} />
76
+ {/snippet}
77
+ </RadioRow>
78
+ {/each}
79
+ </OptionsMenu>
@@ -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 "$lib/map/utils";
8
+ import { mapStyleState } from "$lib/map/map-style-state.svelte.js";
9
+ import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
10
+ import { evStationsMapIntegration } from "$lib/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>
@@ -0,0 +1,48 @@
1
+ import { getContext, setContext } from "svelte";
2
+ import { SvelteSet } from "svelte/reactivity";
3
+ import { isWideLayout } from "./layout";
4
+
5
+ const OPTIONS_MENUS_KEY = Symbol("monitor-map-options-menus");
6
+
7
+ /** Which menus are opened by click: at most one on the toolbar, any number in the panel. */
8
+ export class OptionsMenusState {
9
+ private open = new SvelteSet<string>();
10
+
11
+ isOpen(id: string): boolean {
12
+ return this.open.has(id);
13
+ }
14
+
15
+ toggle(id: string): void {
16
+ if (this.open.has(id)) {
17
+ this.open.delete(id);
18
+ return;
19
+ }
20
+ if (isWideLayout()) this.open.clear();
21
+ this.open.add(id);
22
+ }
23
+
24
+ /** Hovering another toolbar trigger closes any dropdown left open by a click. */
25
+ closeOthers(id: string): void {
26
+ for (const other of [...this.open]) {
27
+ if (other !== id) this.open.delete(other);
28
+ }
29
+ }
30
+
31
+ close(id: string): void {
32
+ this.open.delete(id);
33
+ }
34
+
35
+ closeAll(): void {
36
+ this.open.clear();
37
+ }
38
+ }
39
+
40
+ export function provideOptionsMenus(): OptionsMenusState {
41
+ return setContext(OPTIONS_MENUS_KEY, new OptionsMenusState());
42
+ }
43
+
44
+ export function useOptionsMenus(): OptionsMenusState {
45
+ const menus = getContext<OptionsMenusState | undefined>(OPTIONS_MENUS_KEY);
46
+ if (!menus) throw new Error("OptionsMenu must be rendered inside OptionsBar");
47
+ return menus;
48
+ }
@@ -0,0 +1,35 @@
1
+ import type { MonitorsPollutant } from "$lib/monitors/types";
2
+
3
+ /**
4
+ * What the Pollutant menu shows. "none" is not a monitorsManager.pollutant value — it means
5
+ * the monitors integration is disabled while the manager keeps its last real pollutant.
6
+ */
7
+ export type PollutantSelection = MonitorsPollutant | "none";
8
+
9
+ export interface PollutantTargets {
10
+ manager: { pollutant: MonitorsPollutant | null };
11
+ integration: { enabled: boolean };
12
+ }
13
+
14
+ export function parsePollutantParam(value: unknown): PollutantSelection | null {
15
+ return value === "pm25" || value === "o3" || value === "none" ? value : null;
16
+ }
17
+
18
+ export function currentSelection(
19
+ pollutant: MonitorsPollutant | null,
20
+ monitorsEnabled: boolean
21
+ ): PollutantSelection | null {
22
+ return monitorsEnabled ? pollutant : "none";
23
+ }
24
+
25
+ export function applyPollutantSelection(
26
+ selection: PollutantSelection,
27
+ targets: PollutantTargets
28
+ ): void {
29
+ if (selection === "none") {
30
+ targets.integration.enabled = false;
31
+ return;
32
+ }
33
+ targets.manager.pollutant = selection;
34
+ targets.integration.enabled = true;
35
+ }
@@ -0,0 +1,28 @@
1
+ import { monitorsManager } from "$lib/monitors/monitors.svelte";
2
+ import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
3
+ import { collocationSitesMapIntegration } from "$lib/collocation-sites/collocations-map-integration.svelte";
4
+ import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte";
5
+ import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte";
6
+ import { evStationsMapIntegration } from "$lib/ev-stations/ev-stations-map-integration.svelte";
7
+ import { windMapIntegration } from "$lib/wind/wind.svelte";
8
+ import { mapStyleState } from "$lib/map/map-style-state.svelte.js";
9
+ import { applyMapOptionDefaults } from "./map-option-defaults";
10
+
11
+ /** "Reset Map Options": restores every map option to its default. */
12
+ export function resetMapOptions(): void {
13
+ applyMapOptionDefaults(
14
+ {
15
+ manager: monitorsManager,
16
+ integration: monitorsMapIntegration,
17
+ collocationSites: collocationSitesMapIntegration,
18
+ hmsFire: hmsFireMapIntegration,
19
+ hmsSmoke: hmsSmokeMapIntegration,
20
+ evStations: evStationsMapIntegration,
21
+ wind: windMapIntegration,
22
+ // The basemap reset may start loading a new style; integrations skip the style while
23
+ // mapManager.styleLoading is set and the refresh on "style.load" re-applies them.
24
+ mapStyle: mapStyleState
25
+ },
26
+ monitorsManager.meta?.default_pollutant ?? null
27
+ );
28
+ }
@@ -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,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,10 @@
1
+ <script lang="ts">
2
+ import type { MapImageIcon } from "$lib/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,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,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}
@@ -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
@@ -1,15 +0,0 @@
1
- <script lang="ts">
2
- import MonitorMarkersLegend from "./monitors/components/MonitorMarkersLegend.svelte";
3
- import HMSFireLegend from "./hms/components/HMSFireLegend.svelte";
4
- </script>
5
-
6
- <!--
7
- <div class="z-10 absolute bottom-6 left-6 w-1/2 lg:w-1/5">
8
- <MonitorMarkersLegend />
9
- <HMSFireLegend />
10
- </div>
11
- -->
12
- <div class="pointer-events-none flex flex-col gap-1 pb-4 pl-4">
13
- <HMSFireLegend />
14
- <MonitorMarkersLegend />
15
- </div>