@sjvair/monitor-map 3.6.3 → 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.
- package/README.md +2 -2
- package/dist/lib/MonitorMapLayout.svelte +74 -36
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.d.ts +3 -0
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +15 -1
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +27 -6
- package/dist/lib/hms/hms-fire-icon-manager.d.ts +8 -0
- package/dist/lib/hms/hms-fire-icon-manager.js +10 -1
- package/dist/lib/hms/hms-fire-map-integration.svelte.js +1 -1
- package/dist/lib/hms/hms-smoke-map-integration.svelte.d.ts +20 -0
- package/dist/lib/hms/hms-smoke-map-integration.svelte.js +22 -7
- package/dist/lib/index.d.ts +21 -7
- package/dist/lib/index.js +22 -7
- package/dist/lib/legend/Legend.svelte +40 -0
- package/dist/lib/legend/Legend.svelte.d.ts +7 -0
- package/dist/lib/legend/LegendBar.svelte +45 -0
- package/dist/lib/legend/LegendBar.svelte.d.ts +7 -0
- package/dist/lib/legend/legend-data.d.ts +40 -0
- package/dist/lib/legend/legend-data.js +65 -0
- package/dist/lib/map/MapShell.svelte +7 -6
- package/dist/lib/map/MapShell.svelte.d.ts +3 -1
- package/dist/lib/map/basemap.d.ts +14 -0
- package/dist/lib/map/basemap.js +10 -0
- package/dist/lib/map/integrations/map-geojson-integration.svelte.d.ts +6 -0
- package/dist/lib/map/integrations/map-geojson-integration.svelte.js +15 -2
- package/dist/lib/map/integrations/map-layer-integration.svelte.js +7 -3
- package/dist/lib/map/map-style-state.svelte.d.ts +13 -0
- package/dist/lib/map/map-style-state.svelte.js +35 -0
- package/dist/lib/map/map.svelte.d.ts +6 -0
- package/dist/lib/map/map.svelte.js +6 -0
- package/dist/lib/monitors/monitor-groups.d.ts +11 -0
- package/dist/lib/monitors/monitor-groups.js +7 -0
- package/dist/lib/monitors/monitor-utils.js +1 -3
- package/dist/lib/monitors/monitors-icon-manager.svelte.js +2 -5
- package/dist/lib/monitors/monitors-map-integration.svelte.d.ts +4 -4
- package/dist/lib/monitors/monitors-map-integration.svelte.js +43 -30
- package/dist/lib/monitors/monitors.svelte.d.ts +1 -0
- package/dist/lib/monitors/monitors.svelte.js +22 -0
- package/dist/lib/options/OptionsBar.svelte +96 -0
- package/dist/lib/options/OptionsBar.svelte.d.ts +10 -0
- package/dist/lib/options/OptionsGroup.svelte +12 -0
- package/dist/lib/options/OptionsGroup.svelte.d.ts +7 -0
- package/dist/lib/options/OptionsMenu.svelte +71 -0
- package/dist/lib/options/OptionsMenu.svelte.d.ts +15 -0
- package/dist/lib/options/group-state.d.ts +7 -0
- package/dist/lib/options/group-state.js +15 -0
- package/dist/lib/options/layout.d.ts +7 -0
- package/dist/lib/options/layout.js +9 -0
- package/dist/lib/options/map-option-defaults.d.ts +56 -0
- package/dist/lib/options/map-option-defaults.js +47 -0
- package/dist/lib/options/menus/LayersMenu.svelte +47 -0
- package/dist/lib/options/menus/LayersMenu.svelte.d.ts +3 -0
- package/dist/lib/options/menus/MonitorsMenu.svelte +68 -0
- package/dist/lib/options/menus/MonitorsMenu.svelte.d.ts +3 -0
- package/dist/lib/options/menus/OverlaysMenu.svelte +18 -0
- package/dist/lib/{MapLegend.svelte.d.ts → options/menus/OverlaysMenu.svelte.d.ts} +3 -3
- package/dist/lib/options/menus/PollutantMenu.svelte +79 -0
- package/dist/lib/options/menus/PollutantMenu.svelte.d.ts +3 -0
- package/dist/lib/options/menus/SettingsMenu.svelte +73 -0
- package/dist/lib/{hms/components/HMSFireLegend.svelte.d.ts → options/menus/SettingsMenu.svelte.d.ts} +3 -3
- package/dist/lib/options/options-state.svelte.d.ts +12 -0
- package/dist/lib/options/options-state.svelte.js +42 -0
- package/dist/lib/options/pollutant-param.d.ts +17 -0
- package/dist/lib/options/pollutant-param.js +14 -0
- package/dist/lib/options/reset-map-options.d.ts +2 -0
- package/dist/lib/options/reset-map-options.js +24 -0
- package/dist/lib/options/rows/CheckboxRow.svelte +51 -0
- package/dist/lib/options/rows/CheckboxRow.svelte.d.ts +13 -0
- package/dist/lib/options/rows/GroupRow.svelte +43 -0
- package/dist/lib/options/rows/GroupRow.svelte.d.ts +17 -0
- package/dist/lib/options/rows/MarkerIcon.svelte +10 -0
- package/dist/lib/options/rows/MarkerIcon.svelte.d.ts +8 -0
- package/dist/lib/options/rows/RadioRow.svelte +33 -0
- package/dist/lib/options/rows/RadioRow.svelte.d.ts +13 -0
- package/dist/lib/options/rows/SubmenuRow.svelte +22 -0
- package/dist/lib/options/rows/SubmenuRow.svelte.d.ts +11 -0
- package/dist/lib/search/Search.svelte +6 -6
- package/package.json +5 -3
- package/src/lib/MonitorMapLayout.svelte +74 -36
- package/src/lib/collocation-sites/collocations-map-integration.svelte.ts +17 -1
- package/src/lib/ev-stations/ev-stations-map-integration.svelte.ts +26 -6
- package/src/lib/hms/hms-fire-icon-manager.ts +16 -2
- package/src/lib/hms/hms-fire-map-integration.svelte.ts +1 -1
- package/src/lib/hms/hms-smoke-map-integration.svelte.ts +25 -7
- package/src/lib/index.ts +31 -7
- package/src/lib/legend/Legend.svelte +40 -0
- package/src/lib/legend/LegendBar.svelte +45 -0
- package/src/lib/legend/legend-data.ts +101 -0
- package/src/lib/map/MapShell.svelte +7 -6
- package/src/lib/map/basemap.ts +24 -0
- package/src/lib/map/integrations/map-geojson-integration.svelte.ts +15 -2
- package/src/lib/map/integrations/map-layer-integration.svelte.ts +6 -3
- package/src/lib/map/map-style-state.svelte.ts +42 -0
- package/src/lib/map/map.svelte.ts +6 -0
- package/src/lib/monitors/monitor-groups.ts +20 -0
- package/src/lib/monitors/monitor-utils.ts +1 -5
- package/src/lib/monitors/monitors-icon-manager.svelte.ts +2 -8
- package/src/lib/monitors/monitors-map-integration.svelte.ts +37 -27
- package/src/lib/monitors/monitors.svelte.ts +22 -0
- package/src/lib/options/OptionsBar.svelte +96 -0
- package/src/lib/options/OptionsGroup.svelte +12 -0
- package/src/lib/options/OptionsMenu.svelte +71 -0
- package/src/lib/options/group-state.ts +16 -0
- package/src/lib/options/layout.ts +10 -0
- package/src/lib/options/map-option-defaults.ts +71 -0
- package/src/lib/options/menus/LayersMenu.svelte +47 -0
- package/src/lib/options/menus/MonitorsMenu.svelte +68 -0
- package/src/lib/options/menus/OverlaysMenu.svelte +18 -0
- package/src/lib/options/menus/PollutantMenu.svelte +79 -0
- package/src/lib/options/menus/SettingsMenu.svelte +73 -0
- package/src/lib/options/options-state.svelte.ts +48 -0
- package/src/lib/options/pollutant-param.ts +35 -0
- package/src/lib/options/reset-map-options.ts +28 -0
- package/src/lib/options/rows/CheckboxRow.svelte +51 -0
- package/src/lib/options/rows/GroupRow.svelte +43 -0
- package/src/lib/options/rows/MarkerIcon.svelte +10 -0
- package/src/lib/options/rows/RadioRow.svelte +33 -0
- package/src/lib/options/rows/SubmenuRow.svelte +22 -0
- package/src/lib/search/Search.svelte +6 -6
- package/dist/lib/MapLegend.svelte +0 -15
- package/dist/lib/collocation-sites/collocation-utils.d.ts +0 -3
- package/dist/lib/collocation-sites/collocation-utils.js +0 -30
- package/dist/lib/components/DisplayOption.svelte +0 -7
- package/dist/lib/components/DisplayOption.svelte.d.ts +0 -5
- package/dist/lib/components/MapLayersDisplayOptions.svelte +0 -42
- package/dist/lib/components/MapLayersDisplayOptions.svelte.d.ts +0 -18
- package/dist/lib/components/SegmentedControl.svelte +0 -52
- package/dist/lib/components/SegmentedControl.svelte.d.ts +0 -11
- package/dist/lib/components/ToggleSwitch.svelte +0 -93
- package/dist/lib/components/ToggleSwitch.svelte.d.ts +0 -9
- package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
- package/dist/lib/ev-stations/components/EvStationsDisplayOptions.svelte.d.ts +0 -18
- package/dist/lib/hms/components/HMSFireLegend.svelte +0 -21
- package/dist/lib/map/MapStyleDisplayOptions.svelte +0 -61
- package/dist/lib/map/MapStyleDisplayOptions.svelte.d.ts +0 -3
- package/dist/lib/map/Menu.svelte +0 -65
- package/dist/lib/map/Menu.svelte.d.ts +0 -5
- package/dist/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
- package/dist/lib/monitors/components/MonitorMarkersLegend.svelte.d.ts +0 -3
- package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte +0 -85
- package/dist/lib/monitors/components/MonitorsDisplayOptions.svelte.d.ts +0 -3
- package/src/lib/MapLegend.svelte +0 -15
- package/src/lib/collocation-sites/collocation-utils.ts +0 -41
- package/src/lib/components/DisplayOption.svelte +0 -7
- package/src/lib/components/MapLayersDisplayOptions.svelte +0 -42
- package/src/lib/components/SegmentedControl.svelte +0 -52
- package/src/lib/components/ToggleSwitch.svelte +0 -93
- package/src/lib/ev-stations/components/EvStationsDisplayOptions.svelte +0 -46
- package/src/lib/hms/components/HMSFireLegend.svelte +0 -21
- package/src/lib/map/MapStyleDisplayOptions.svelte +0 -61
- package/src/lib/map/Menu.svelte +0 -65
- package/src/lib/monitors/components/MonitorMarkersLegend.svelte +0 -41
- 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>
|
package/dist/lib/{hms/components/HMSFireLegend.svelte.d.ts → options/menus/SettingsMenu.svelte.d.ts}
RENAMED
|
@@ -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
|
|
14
|
+
declare const SettingsMenu: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
15
|
[evt: string]: CustomEvent<any>;
|
|
16
16
|
}, {}, {}, string>;
|
|
17
|
-
type
|
|
18
|
-
export default
|
|
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,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
|
|
115
|
-
collapsed ? "h-
|
|
116
|
-
!collapsed && results.length ? "max-h-90" : "max-h-
|
|
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-
|
|
120
|
+
<div class="flex h-10 w-full shrink-0 items-center">
|
|
121
121
|
<button
|
|
122
|
-
class="
|
|
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="
|
|
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": "
|
|
4
|
+
"version": "4.0.0",
|
|
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
|
|
6
|
-
import
|
|
7
|
-
import
|
|
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 {
|
|
20
|
-
import
|
|
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
|
|
53
|
-
// init() only seeds pollutant on the manager's first-ever
|
|
54
|
-
// applies a later "?pollutant=" change (e.g. navigating in from
|
|
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
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
92
|
+
// ...and the reverse: reflect UI-driven changes back to the URL.
|
|
72
93
|
$effect(() => {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
75
|
-
searchParams.set("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
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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-
|
|
105
|
-
<
|
|
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()}
|