@sjvair/monitor-map 3.0.2 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/LoadScreen.js +14 -14
- package/dist/lib/{src/MapView.js → MainMapView.js} +2 -2
- package/dist/lib/MapLegend.js +13 -0
- package/dist/lib/MonitorDetailPanel.js +169 -0
- package/dist/lib/MonitorMapLayout.js +91 -0
- package/dist/lib/_virtual/_rolldown/runtime.js +11 -0
- package/dist/lib/assets/logos/airgradient-blue.js +4 -0
- package/dist/lib/assets/logos/airnow-compact.js +4 -0
- package/dist/lib/assets/logos/carb-compact.js +4 -0
- package/dist/lib/assets/logos/purpleair-compact.js +4 -0
- package/dist/lib/assets/logos/sjvair.js +4 -0
- package/dist/lib/collocation-sites/CollocationTooltip.js +1 -1
- package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +20 -20
- package/dist/lib/components/DataBox.js +1 -1
- package/dist/lib/components/DisplayOption.js +1 -1
- package/dist/lib/components/MapLayersDisplayOptions.js +44 -35
- package/dist/lib/components/SegmentedControl.js +4 -4
- package/dist/lib/components/ToggleSwitch.js +1 -1
- package/dist/lib/components/ui/button/button.js +4 -4
- package/dist/lib/components/ui/range-calendar/range-calendar-caption.js +57 -54
- package/dist/lib/components/ui/range-calendar/range-calendar-cell.js +19 -18
- package/dist/lib/components/ui/range-calendar/range-calendar-day.js +19 -18
- package/dist/lib/components/ui/range-calendar/range-calendar-grid-body.js +17 -16
- package/dist/lib/components/ui/range-calendar/range-calendar-grid-head.js +17 -16
- package/dist/lib/components/ui/range-calendar/range-calendar-grid-row.js +19 -18
- package/dist/lib/components/ui/range-calendar/range-calendar-grid.js +19 -18
- package/dist/lib/components/ui/range-calendar/range-calendar-head-cell.js +19 -18
- package/dist/lib/components/ui/range-calendar/range-calendar-header.js +19 -18
- package/dist/lib/components/ui/range-calendar/range-calendar-month-select.js +31 -31
- package/dist/lib/components/ui/range-calendar/range-calendar-month.js +19 -19
- package/dist/lib/components/ui/range-calendar/range-calendar-months.js +19 -19
- package/dist/lib/components/ui/range-calendar/range-calendar-nav.js +19 -19
- package/dist/lib/components/ui/range-calendar/range-calendar-next-button.js +27 -26
- package/dist/lib/components/ui/range-calendar/range-calendar-prev-button.js +27 -26
- package/dist/lib/components/ui/range-calendar/range-calendar-year-select.js +29 -29
- package/dist/lib/components/ui/range-calendar/range-calendar.js +154 -154
- package/dist/lib/data-chart/DataChart.js +76 -76
- package/dist/lib/ev-stations/components/EvStationClusterTooltip.js +16 -0
- package/dist/lib/ev-stations/components/EvStationTooltip.js +120 -0
- package/dist/lib/ev-stations/components/EvStationsDisplayOptions.js +41 -0
- package/dist/lib/ev-stations/data-maps.js +4 -0
- package/dist/lib/ev-stations/ev-station-icons.js +32 -0
- package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +118 -0
- package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +181 -0
- package/dist/lib/ev-stations/ev-stations.svelte.js +33 -0
- package/dist/lib/ev-stations/zip-codes.js +136 -0
- package/dist/lib/hms/components/HMSFireLegend.js +35 -0
- package/dist/lib/index.js +19 -17
- package/dist/lib/map/Map.js +1 -1
- package/dist/lib/map/MapStyleDisplayOptions.js +16 -16
- package/dist/lib/map/Menu.js +23 -24
- package/dist/lib/map/integrations/map-icon-manager.js +3 -0
- package/dist/lib/map/utils.js +16 -13
- package/dist/lib/monitors/components/MonitorClusterTooltip.js +4 -4
- package/dist/lib/monitors/components/MonitorMarkersLegend.js +35 -0
- package/dist/lib/monitors/components/MonitorSubscriptionManager.js +1 -1
- package/dist/lib/monitors/components/MonitorTooltip.js +1 -1
- package/dist/lib/monitors/components/MonitorWidgetModal.js +1 -1
- package/dist/lib/monitors/components/MonitorsDisplayOptions.js +21 -21
- package/dist/lib/monitors/monitors-icon-manager.svelte.js +9 -9
- package/dist/lib/monitors/monitors-map-integration.svelte.js +36 -36
- package/dist/lib/monitors/monitors.svelte.js +38 -42
- package/dist/lib/router-context.js +13 -0
- package/dist/lib/routes.js +10 -0
- package/dist/lib/search/Search.js +124 -0
- package/dist/lib/search/service.js +90 -0
- package/dist/lib/sjvair-sjv.avif +0 -0
- package/dist/lib/sjvair-sjv.webp +0 -0
- package/dist/lib/sjvair-sjv2.avif +0 -0
- package/dist/lib/sjvair-sjv2.webp +0 -0
- package/dist/lib/utils.js +9 -3
- package/package.json +37 -36
- package/src/lib/LoadScreen.svelte +5 -5
- package/src/lib/MainMapView.svelte +1 -0
- package/src/lib/MapLegend.svelte +15 -0
- package/src/lib/MonitorDetailPanel.svelte +152 -0
- package/src/lib/MonitorMapLayout.svelte +193 -0
- package/src/lib/components/MapLayersDisplayOptions.svelte +6 -2
- package/src/lib/hms/components/HMSFireLegend.svelte +21 -0
- package/src/lib/index.ts +34 -0
- package/src/lib/map/Map.svelte +1 -0
- package/src/lib/map/Menu.svelte +4 -7
- package/src/lib/map/integrations/map-icon-manager.ts +4 -1
- package/src/lib/monitors/monitors.svelte.ts +1 -3
- package/src/lib/router-context.ts +48 -0
- package/src/lib/routes.ts +8 -0
- package/src/lib/search/Search.svelte +189 -0
- package/src/lib/search/service.ts +101 -0
- package/src/lib/utils.ts +11 -0
- package/dist/lib/assets/icons/adjustment-horizontal.js +0 -4
- package/dist/lib/node_modules/@internationalized/date/dist/private/CalendarDate.js +0 -122
- package/dist/lib/node_modules/@internationalized/date/dist/private/DateFormatter.js +0 -84
- package/dist/lib/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +0 -91
- package/dist/lib/node_modules/@internationalized/date/dist/private/conversion.js +0 -104
- package/dist/lib/node_modules/@internationalized/date/dist/private/manipulation.js +0 -159
- package/dist/lib/node_modules/@internationalized/date/dist/private/queries.js +0 -32
- package/dist/lib/node_modules/@internationalized/date/dist/private/string.js +0 -37
- package/dist/lib/node_modules/@internationalized/date/dist/private/utils.js +0 -6
- package/dist/lib/node_modules/clsx/dist/clsx.js +0 -16
- package/dist/lib/node_modules/tailwind-merge/dist/bundle-mjs.js +0 -1723
- package/dist/lib/node_modules/tailwind-variants/dist/chunk-LQJYWU4O.js +0 -70
- package/dist/lib/node_modules/tailwind-variants/dist/chunk-RZF76H2U.js +0 -166
- package/dist/lib/node_modules/tailwind-variants/dist/index.js +0 -19
- package/dist/lib/node_modules/uplot/dist/uPlot.esm.js +0 -2147
- package/dist/lib/src/MonitorDetailPanel.js +0 -169
- package/dist/lib/src/router.js +0 -9
- package/dist/monitor-map/MonitorDetailPanel.css +0 -1
- package/dist/monitor-map/assets/MonitorDetailPanel-BU_pXZjp.js +0 -15
- package/dist/monitor-map/assets/validate-routes-CgbRa8fT.js +0 -1
- package/dist/monitor-map/inter-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/monitor-map/inter-cyrillic-wght-normal.woff2 +0 -0
- package/dist/monitor-map/inter-greek-ext-wght-normal.woff2 +0 -0
- package/dist/monitor-map/inter-greek-wght-normal.woff2 +0 -0
- package/dist/monitor-map/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/monitor-map/inter-latin-wght-normal.woff2 +0 -0
- package/dist/monitor-map/inter-vietnamese-wght-normal.woff2 +0 -0
- package/dist/monitor-map/lungs.svg +0 -99
- package/dist/monitor-map/sjvair-air.avif +0 -0
- package/dist/monitor-map/sjvair-air.png +0 -0
- package/dist/monitor-map/sjvair-air.webp +0 -0
- package/dist/monitor-map/sjvair-air2.avif +0 -0
- package/dist/monitor-map/sjvair-air2.png +0 -0
- package/dist/monitor-map/sjvair-air2.webp +0 -0
- package/dist/monitor-map/sjvair-icon.avif +0 -0
- package/dist/monitor-map/sjvair-icon.png +0 -0
- package/dist/monitor-map/sjvair-icon.webp +0 -0
- package/dist/monitor-map/sjvair-icon2.avif +0 -0
- package/dist/monitor-map/sjvair-icon2.png +0 -0
- package/dist/monitor-map/sjvair-icon2.webp +0 -0
- package/dist/monitor-map/sjvair-sjv.avif +0 -0
- package/dist/monitor-map/sjvair-sjv.png +0 -0
- package/dist/monitor-map/sjvair-sjv.webp +0 -0
- package/dist/monitor-map/sjvair-sjv2.avif +0 -0
- package/dist/monitor-map/sjvair-sjv2.png +0 -0
- package/dist/monitor-map/sjvair-sjv2.webp +0 -0
- package/dist/monitor-map/sjvairMonitorMap.css +0 -1
- package/dist/monitor-map/sjvairMonitorMap.js +0 -4042
package/src/lib/index.ts
CHANGED
|
@@ -1,3 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage in a host app:
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* // router.ts
|
|
6
|
+
* import { createRouter } from "sv-router";
|
|
7
|
+
* import { monitorMapRoutes } from "@sjvair/monitor-map";
|
|
8
|
+
*
|
|
9
|
+
* export const { route, navigate, p, isActive, Router } = createRouter({
|
|
10
|
+
* "/my-map": monitorMapRoutes
|
|
11
|
+
* // ...the host app's own routes
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <!-- app root -->
|
|
17
|
+
* <script>
|
|
18
|
+
* import { provideMonitorMapRouter } from "@sjvair/monitor-map";
|
|
19
|
+
* import { route, navigate, p, isActive, Router } from "./router";
|
|
20
|
+
*
|
|
21
|
+
* provideMonitorMapRouter({ route, navigate, p, isActive, basePath: "/my-map" });
|
|
22
|
+
* </script>
|
|
23
|
+
*
|
|
24
|
+
* <Router />
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export { monitorMapRoutes } from "./routes";
|
|
28
|
+
export {
|
|
29
|
+
ROUTER_KEY,
|
|
30
|
+
provideMonitorMapRouter,
|
|
31
|
+
useMonitorMapRouter,
|
|
32
|
+
type MonitorMapRouterApi
|
|
33
|
+
} from "./router-context";
|
|
34
|
+
|
|
1
35
|
// State managers
|
|
2
36
|
export { monitorsManager } from "./monitors/monitors.svelte";
|
|
3
37
|
export type { MonitorsManager } from "./monitors/monitors.svelte";
|
package/src/lib/map/Map.svelte
CHANGED
package/src/lib/map/Menu.svelte
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Attachment } from "svelte/attachments";
|
|
3
|
+
import { SlidersHorizontalIcon } from "@lucide/svelte";
|
|
3
4
|
|
|
4
5
|
let { children } = $props();
|
|
5
6
|
let isOpen: boolean = $state(false);
|
|
@@ -37,20 +38,16 @@
|
|
|
37
38
|
data-trigger
|
|
38
39
|
class:rounded-full={!isOpen}
|
|
39
40
|
class:delay-100={!isOpen}
|
|
40
|
-
class="
|
|
41
|
+
class="relative top-0 left-0 flex items-center justify-center overflow-hidden rounded bg-white p-1 shadow transition-all duration-0 select-none"
|
|
41
42
|
>
|
|
42
43
|
<button
|
|
43
44
|
class:w-0={isOpen}
|
|
44
45
|
class:h-0={isOpen}
|
|
45
46
|
class:opacity-0={isOpen}
|
|
46
|
-
class="relative size-
|
|
47
|
+
class="relative size-10 transition-all flex justify-center items-center"
|
|
47
48
|
aria-label="Display Options"
|
|
48
49
|
>
|
|
49
|
-
<
|
|
50
|
-
src="$lib/assets/icons/adjustment-horizontal.svg"
|
|
51
|
-
alt="SJVAir Icon"
|
|
52
|
-
class="size-8"
|
|
53
|
-
/>
|
|
50
|
+
<SlidersHorizontalIcon size="24" />
|
|
54
51
|
</button>
|
|
55
52
|
<div
|
|
56
53
|
class:h-55={isOpen}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Map as MaptilerMap } from "@maptiler/sdk";
|
|
2
|
-
//import { XMap } from "@tstk/builtin-extensions";
|
|
3
2
|
import { mapManager } from "../map.svelte.ts";
|
|
4
3
|
import type { MapImageIcon } from "./types";
|
|
5
4
|
import { SvelteMap } from "svelte/reactivity";
|
|
@@ -13,6 +12,10 @@ export class MapIconManager {
|
|
|
13
12
|
}
|
|
14
13
|
}
|
|
15
14
|
|
|
15
|
+
get iter(): MapIterator<[string, MapImageIcon]> {
|
|
16
|
+
return this.icons.entries();
|
|
17
|
+
}
|
|
18
|
+
|
|
16
19
|
get(id: string): MapImageIcon | undefined {
|
|
17
20
|
return this.icons.get(id);
|
|
18
21
|
}
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
type MonitorsMeta,
|
|
8
8
|
type SJVAirEntryLevel
|
|
9
9
|
} from "@sjvair/sdk";
|
|
10
|
-
import { route } from "../../router.ts";
|
|
11
10
|
import { XMap } from "@tstk/builtin-extensions";
|
|
12
11
|
import { Interval } from "@tstk/utils";
|
|
13
12
|
|
|
@@ -26,12 +25,11 @@ class MonitorsManager {
|
|
|
26
25
|
this.meta?.entryType(this.pollutant ?? this.meta.default_pollutant).asIter.levels || null
|
|
27
26
|
);
|
|
28
27
|
|
|
29
|
-
async init(): Promise<void> {
|
|
28
|
+
async init(urlPollutant?: string | number | boolean): Promise<void> {
|
|
30
29
|
if (this.initialized) return;
|
|
31
30
|
|
|
32
31
|
[this.list, this.meta] = await Promise.all([getMonitors(), getMonitorsMeta()]);
|
|
33
32
|
|
|
34
|
-
const urlPollutant = route.search.pollutant;
|
|
35
33
|
this.pollutant =
|
|
36
34
|
urlPollutant === "pm25" || urlPollutant === "o3" ? urlPollutant : this.meta.default_pollutant;
|
|
37
35
|
this.latest = await getMonitorsLatestMap(this.pollutant);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getContext, setContext } from "svelte";
|
|
2
|
+
import type {
|
|
3
|
+
ConstructUrlOptions,
|
|
4
|
+
Navigation,
|
|
5
|
+
NavigateOptions,
|
|
6
|
+
RouterApi,
|
|
7
|
+
Routes
|
|
8
|
+
} from "sv-router";
|
|
9
|
+
|
|
10
|
+
export const ROUTER_KEY = Symbol("monitor-map-router");
|
|
11
|
+
|
|
12
|
+
export interface MonitorMapRouterApi {
|
|
13
|
+
route: RouterApi<Routes>["route"];
|
|
14
|
+
/**
|
|
15
|
+
* Loosely typed against `string` rather than sv-router's literal-checked `Path<T>` union:
|
|
16
|
+
* internal calls prefix paths with `basePath` (a runtime string), which can't satisfy a
|
|
17
|
+
* template literal type checked against the host app's full route tree.
|
|
18
|
+
*/
|
|
19
|
+
navigate(
|
|
20
|
+
path: string,
|
|
21
|
+
options?: NavigateOptions & { params?: Record<string, string> }
|
|
22
|
+
): Promise<Navigation>;
|
|
23
|
+
p(path: string, options?: ConstructUrlOptions & { params?: Record<string, string> }): string;
|
|
24
|
+
isActive: {
|
|
25
|
+
(path: string, params?: Record<string, string>): boolean;
|
|
26
|
+
startsWith(path: string, params?: Record<string, string>): boolean;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Prefix under which monitorMapRoutes is mounted in the host app's router,
|
|
30
|
+
* e.g. "" at the app root, or "/my-map" when nested under a host route.
|
|
31
|
+
*/
|
|
32
|
+
basePath: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function provideMonitorMapRouter(api: MonitorMapRouterApi) {
|
|
36
|
+
setContext(ROUTER_KEY, api);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function useMonitorMapRouter(): MonitorMapRouterApi {
|
|
40
|
+
const api = getContext<MonitorMapRouterApi>(ROUTER_KEY);
|
|
41
|
+
if (!api) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"monitor-map router context not found. Wrap your app root with " +
|
|
44
|
+
"provideMonitorMapRouter(...) before rendering monitor-map components."
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return api;
|
|
48
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from "svelte";
|
|
3
|
+
import { Marker } from "@maptiler/sdk";
|
|
4
|
+
import { SearchIcon } from "@lucide/svelte";
|
|
5
|
+
import { useMonitorMapRouter } from "../router-context";
|
|
6
|
+
import { mapManager } from "$lib/map/map.svelte";
|
|
7
|
+
import { debounce } from "$lib/utils";
|
|
8
|
+
import { monitorSearch, geocode, type SearchResult } from "./service";
|
|
9
|
+
import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte";
|
|
10
|
+
|
|
11
|
+
const { navigate, basePath } = useMonitorMapRouter();
|
|
12
|
+
|
|
13
|
+
let collapsed = $state(true);
|
|
14
|
+
let searchText = $state("");
|
|
15
|
+
let results: SearchResult[] = $state([]);
|
|
16
|
+
let marker: Marker | null = $state.raw(null);
|
|
17
|
+
let inputEl: HTMLInputElement | null = $state(null);
|
|
18
|
+
|
|
19
|
+
function clickOutside(node: HTMLElement) {
|
|
20
|
+
function handler(e: MouseEvent) {
|
|
21
|
+
if (!collapsed && !node.contains(e.target as Node)) {
|
|
22
|
+
collapsed = true;
|
|
23
|
+
searchText = "";
|
|
24
|
+
results = [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
document.addEventListener("click", handler, true);
|
|
28
|
+
return {
|
|
29
|
+
destroy() {
|
|
30
|
+
document.removeEventListener("click", handler, true);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function openSearch() {
|
|
36
|
+
if (collapsed) {
|
|
37
|
+
collapsed = false;
|
|
38
|
+
inputEl?.focus();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clearSearch() {
|
|
43
|
+
searchText = "";
|
|
44
|
+
results = [];
|
|
45
|
+
marker?.remove();
|
|
46
|
+
marker = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function handleInput() {
|
|
50
|
+
const query = searchText;
|
|
51
|
+
if (!query) {
|
|
52
|
+
results = [];
|
|
53
|
+
marker?.remove();
|
|
54
|
+
marker = null;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const monitorResults = await monitorSearch(query);
|
|
58
|
+
if (searchText !== query) return;
|
|
59
|
+
if (monitorResults.length) {
|
|
60
|
+
results = monitorResults;
|
|
61
|
+
} else if (query.length > 4) {
|
|
62
|
+
const geo = await geocode(query);
|
|
63
|
+
if (searchText !== query) return;
|
|
64
|
+
results = geo;
|
|
65
|
+
} else {
|
|
66
|
+
results = [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const debouncedSearch = debounce(handleInput, 500);
|
|
71
|
+
|
|
72
|
+
async function selectMonitor(result: Extract<SearchResult, { type: "monitor" }>) {
|
|
73
|
+
collapsed = true;
|
|
74
|
+
searchText = "";
|
|
75
|
+
results = [];
|
|
76
|
+
marker?.remove();
|
|
77
|
+
marker = null;
|
|
78
|
+
if (mapManager.map && result.monitor.position) {
|
|
79
|
+
const { coordinates } = result.monitor.position;
|
|
80
|
+
const center: [number, number] = [coordinates[0], coordinates[1]];
|
|
81
|
+
mapManager.map.flyTo({
|
|
82
|
+
center,
|
|
83
|
+
zoom: 12
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
await navigate(`${basePath}/monitor/:id`, { params: { id: result.monitor.id } });
|
|
87
|
+
monitorsMapIntegration.selectedMonitorId = result.monitor.id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function selectGeocode(result: Extract<SearchResult, { type: "geocode" }>) {
|
|
91
|
+
const { feature } = result;
|
|
92
|
+
marker?.remove();
|
|
93
|
+
marker = null;
|
|
94
|
+
if (mapManager.map) {
|
|
95
|
+
const center: [number, number] = [feature.center[0], feature.center[1]];
|
|
96
|
+
marker = new Marker().setLngLat(center).addTo(mapManager.map);
|
|
97
|
+
const isAddress = feature.place_type?.[0] === "address";
|
|
98
|
+
mapManager.map.flyTo({
|
|
99
|
+
center,
|
|
100
|
+
...(isAddress && { zoom: 16 })
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
results = [];
|
|
104
|
+
collapsed = true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
onDestroy(() => {
|
|
108
|
+
marker?.remove();
|
|
109
|
+
});
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<div
|
|
113
|
+
class={[
|
|
114
|
+
"flex flex-col bg-white shadow-md overflow-hidden transition-all duration-300 select-none",
|
|
115
|
+
collapsed ? "w-12 h-12 rounded-full" : "w-90 rounded-3xl",
|
|
116
|
+
!collapsed && results.length ? "max-h-90" : "max-h-12"
|
|
117
|
+
]}
|
|
118
|
+
use:clickOutside
|
|
119
|
+
>
|
|
120
|
+
<div class="flex items-center w-full h-12 shrink-0">
|
|
121
|
+
<button
|
|
122
|
+
class="w-12 h-12 shrink-0 flex items-center justify-center rounded-full bg-brand border-4 border-white"
|
|
123
|
+
onclick={openSearch}
|
|
124
|
+
aria-label="Open search"
|
|
125
|
+
>
|
|
126
|
+
<SearchIcon size="24" color="#FFFFFF" />
|
|
127
|
+
</button>
|
|
128
|
+
<div class="overflow-hidden w-full h-full flex items-center">
|
|
129
|
+
<input
|
|
130
|
+
bind:this={inputEl}
|
|
131
|
+
bind:value={searchText}
|
|
132
|
+
oninput={debouncedSearch}
|
|
133
|
+
type="text"
|
|
134
|
+
placeholder="Search monitors or locations..."
|
|
135
|
+
class="h-full px-2 text-sm bg-transparent border-none outline-none w-full"
|
|
136
|
+
tabindex={collapsed ? -1 : 0}
|
|
137
|
+
/>
|
|
138
|
+
{#if searchText && !collapsed}
|
|
139
|
+
<button
|
|
140
|
+
onclick={clearSearch}
|
|
141
|
+
class="px-2 text-gray-400 hover:text-gray-600 text-xl leading-none cursor-pointer"
|
|
142
|
+
aria-label="Clear search">×</button
|
|
143
|
+
>
|
|
144
|
+
{/if}
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div
|
|
148
|
+
class={[
|
|
149
|
+
"overflow-y-auto transition-all duration-300 divide-y divide-gray-100",
|
|
150
|
+
!collapsed ? "max-h-80" : "max-h-0"
|
|
151
|
+
]}
|
|
152
|
+
>
|
|
153
|
+
{#each results as result (result.type === "monitor" ? result.monitor.id : result.feature.place_name)}
|
|
154
|
+
{#if result.type === "monitor"}
|
|
155
|
+
<button
|
|
156
|
+
onclick={() => selectMonitor(result)}
|
|
157
|
+
class="flex items-center w-full h-12 px-4 hover:brightness-95 bg-white text-left"
|
|
158
|
+
>
|
|
159
|
+
{#if result.logo.url}
|
|
160
|
+
<img
|
|
161
|
+
src={result.logo.url}
|
|
162
|
+
alt={result.logo.alt}
|
|
163
|
+
class="w-10 h-10 object-contain mr-3 shrink-0"
|
|
164
|
+
/>
|
|
165
|
+
{/if}
|
|
166
|
+
<span class="text-sm truncate">{result.monitor.name}</span>
|
|
167
|
+
</button>
|
|
168
|
+
{:else}
|
|
169
|
+
{@const comma = result.feature.place_name.indexOf(",")}
|
|
170
|
+
<button
|
|
171
|
+
onclick={() => selectGeocode(result)}
|
|
172
|
+
class="flex items-center w-full h-12 px-4 hover:brightness-95 bg-white text-left"
|
|
173
|
+
>
|
|
174
|
+
<span class="svg-icon location-on bg-brand w-6 h-6 shrink-0 mr-3"></span>
|
|
175
|
+
<div class="min-w-0">
|
|
176
|
+
<p class="text-sm font-medium truncate">
|
|
177
|
+
{result.feature.place_name.substring(0, comma < 0 ? undefined : comma)}
|
|
178
|
+
</p>
|
|
179
|
+
{#if comma >= 0}
|
|
180
|
+
<p class="text-xs text-muted-foreground truncate">
|
|
181
|
+
{result.feature.place_name.substring(comma + 1).trim()}
|
|
182
|
+
</p>
|
|
183
|
+
{/if}
|
|
184
|
+
</div>
|
|
185
|
+
</button>
|
|
186
|
+
{/if}
|
|
187
|
+
{/each}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { geocoding, type GeocodingFeature } from "@maptiler/sdk";
|
|
2
|
+
import type { MonitorLatestType } from "@sjvair/sdk";
|
|
3
|
+
import { monitorsManager } from "$lib/monitors/monitors.svelte";
|
|
4
|
+
import AirGradientLogo from "../assets/logos/airgradient-blue.svg";
|
|
5
|
+
import AirNowLogo from "../assets/logos/airnow-compact.jpg";
|
|
6
|
+
import CarbLogo from "../assets/logos/carb-compact.jpg";
|
|
7
|
+
import PurpleairLogo from "../assets/logos/purpleair-compact.png";
|
|
8
|
+
import SJVAirLogo from "../assets/logos/sjvair.svg";
|
|
9
|
+
|
|
10
|
+
export type MonitorResult = {
|
|
11
|
+
type: "monitor";
|
|
12
|
+
monitor: MonitorLatestType<"pm25" | "o3">;
|
|
13
|
+
logo: { url: string; alt: string };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GeocodeResult = {
|
|
17
|
+
type: "geocode";
|
|
18
|
+
feature: GeocodingFeature;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type SearchResult = MonitorResult | GeocodeResult;
|
|
22
|
+
|
|
23
|
+
const SJV_CENTER: [number, number] = [-119.8, 36.76];
|
|
24
|
+
|
|
25
|
+
function getMonitorLogo(monitor: MonitorLatestType<"pm25" | "o3">): { url: string; alt: string } {
|
|
26
|
+
switch (monitor.data_source.name) {
|
|
27
|
+
case "AQview":
|
|
28
|
+
return { url: CarbLogo, alt: "AQview" };
|
|
29
|
+
case "AirNow.gov":
|
|
30
|
+
return { url: AirNowLogo, alt: "AirNow.gov" };
|
|
31
|
+
case "PurpleAir":
|
|
32
|
+
return { url: PurpleairLogo, alt: "PurpleAir" };
|
|
33
|
+
case "Central California Asthma Collaborative":
|
|
34
|
+
return { url: SJVAirLogo, alt: "Central California Asthma Collaborative" };
|
|
35
|
+
case "AirGradient":
|
|
36
|
+
return { url: AirGradientLogo, alt: "AirGradient" };
|
|
37
|
+
default:
|
|
38
|
+
console.warn(`[search] Unknown data source: ${monitor.data_source.name}`);
|
|
39
|
+
return { url: "", alt: "" };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let proximityCache: Promise<[number, number]> | null = null;
|
|
44
|
+
|
|
45
|
+
function getProximity(): Promise<[number, number]> {
|
|
46
|
+
return (proximityCache ??= resolveProximity());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveProximity(): Promise<[number, number]> {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
if (!navigator.geolocation) {
|
|
52
|
+
resolve(SJV_CENTER);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const timeout = setTimeout(() => resolve(SJV_CENTER), 3000);
|
|
56
|
+
navigator.geolocation.getCurrentPosition(
|
|
57
|
+
(pos) => {
|
|
58
|
+
clearTimeout(timeout);
|
|
59
|
+
resolve([pos.coords.longitude, pos.coords.latitude]);
|
|
60
|
+
},
|
|
61
|
+
() => {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
resolve(SJV_CENTER);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function monitorSearch(query: string): Promise<SearchResult[]> {
|
|
70
|
+
if (!monitorsManager.latest) return [];
|
|
71
|
+
const normalized = query.toLowerCase();
|
|
72
|
+
const exact: MonitorLatestType<"pm25" | "o3">[] = [];
|
|
73
|
+
const prefix: MonitorLatestType<"pm25" | "o3">[] = [];
|
|
74
|
+
const substr: MonitorLatestType<"pm25" | "o3">[] = [];
|
|
75
|
+
|
|
76
|
+
for (const monitor of monitorsManager.latest.values()) {
|
|
77
|
+
const name = monitor.name.toLowerCase();
|
|
78
|
+
if (name === normalized) exact.push(monitor);
|
|
79
|
+
else if (name.startsWith(normalized)) prefix.push(monitor);
|
|
80
|
+
else if (name.includes(normalized)) substr.push(monitor);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [...exact, ...prefix, ...substr]
|
|
84
|
+
.slice(0, 4)
|
|
85
|
+
.map((monitor) => ({ type: "monitor" as const, monitor, logo: getMonitorLogo(monitor) }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function geocode(query: string): Promise<SearchResult[]> {
|
|
89
|
+
const proximity = await getProximity();
|
|
90
|
+
try {
|
|
91
|
+
const result = await geocoding.forward(query, {
|
|
92
|
+
country: ["us"],
|
|
93
|
+
fuzzyMatch: true,
|
|
94
|
+
limit: 4,
|
|
95
|
+
proximity
|
|
96
|
+
});
|
|
97
|
+
return result.features.map((feature) => ({ type: "geocode" as const, feature }));
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
package/src/lib/utils.ts
CHANGED
|
@@ -11,3 +11,14 @@ export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
|
|
11
11
|
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
|
12
12
|
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
|
13
13
|
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
|
14
|
+
|
|
15
|
+
export function debounce<T extends (...args: any[]) => void>(
|
|
16
|
+
fn: T,
|
|
17
|
+
wait: number
|
|
18
|
+
): (...args: Parameters<T>) => void {
|
|
19
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
20
|
+
return (...args: Parameters<T>) => {
|
|
21
|
+
clearTimeout(timer);
|
|
22
|
+
timer = setTimeout(() => fn(...args), wait);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
//#region src/lib/assets/icons/adjustment-horizontal.svg
|
|
2
|
-
var e = "data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20fill='none'%20viewBox='0%200%2024%2024'%20stroke-width='1.5'%20stroke='currentColor'%20class='size-6'%3e%3cpath%20stroke-linecap='round'%20stroke-linejoin='round'%20d='M10.5%206h9.75M10.5%206a1.5%201.5%200%201%201-3%200m3%200a1.5%201.5%200%201%200-3%200M3.75%206H7.5m3%2012h9.75m-9.75%200a1.5%201.5%200%200%201-3%200m3%200a1.5%201.5%200%200%200-3%200m-3.75%200H7.5m9-6h3.75m-3.75%200a1.5%201.5%200%200%201-3%200m3%200a1.5%201.5%200%200%200-3%200m-9.75%200h9.75'%20/%3e%3c/svg%3e";
|
|
3
|
-
//#endregion
|
|
4
|
-
export { e as default };
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { $93635573935797de$export$80ee6245ec4f29ec as e } from "./calendars/GregorianCalendar.js";
|
|
2
|
-
import { $ad063034c8620db8$export$68781ddf31c0090f as t, $ad063034c8620db8$export$c19a80a9721b80f6 as n } from "./queries.js";
|
|
3
|
-
import { $d07e34cce18680fd$export$83aac07b4c37b25 as r, $d07e34cce18680fd$export$84c95a83c799e074 as i, $d07e34cce18680fd$export$b21e0b124e224484 as a, $d07e34cce18680fd$export$e67a095c620b86fe as o } from "./conversion.js";
|
|
4
|
-
import { $435a2ceaa8778ed8$export$31b5430eb18be4f8 as s, $435a2ceaa8778ed8$export$4e2d2ead65e5f7e3 as c, $435a2ceaa8778ed8$export$6814caac34ca03c7 as l, $435a2ceaa8778ed8$export$96b1d28349274637 as u, $435a2ceaa8778ed8$export$9a297d111fc86b79 as d, $435a2ceaa8778ed8$export$adaa4cf7ef1b65be as f, $435a2ceaa8778ed8$export$c4e2ecac49351ef2 as p, $435a2ceaa8778ed8$export$d52ced6badfb9a4c as m, $435a2ceaa8778ed8$export$dd02b3e0007dfe28 as h, $435a2ceaa8778ed8$export$e16d8520af44a096 as g, $435a2ceaa8778ed8$export$e5d5e1c1822b6e56 as _ } from "./manipulation.js";
|
|
5
|
-
import { $58246871e4652552$export$4223de14708adc63 as v, $58246871e4652552$export$60dfd74aa96791bd as y, $58246871e4652552$export$bf79f1ebf4b18792 as b } from "./string.js";
|
|
6
|
-
//#region node_modules/@internationalized/date/dist/private/CalendarDate.mjs
|
|
7
|
-
function x(t) {
|
|
8
|
-
let n = typeof t[0] == "object" ? t.shift() : new e(), r;
|
|
9
|
-
if (typeof t[0] == "string") r = t.shift();
|
|
10
|
-
else {
|
|
11
|
-
let e = n.getEras();
|
|
12
|
-
r = e[e.length - 1];
|
|
13
|
-
}
|
|
14
|
-
let i = t.shift(), a = t.shift(), o = t.shift();
|
|
15
|
-
return [
|
|
16
|
-
n,
|
|
17
|
-
r,
|
|
18
|
-
i,
|
|
19
|
-
a,
|
|
20
|
-
o
|
|
21
|
-
];
|
|
22
|
-
}
|
|
23
|
-
var S = class e {
|
|
24
|
-
constructor(...e) {
|
|
25
|
-
let [t, n, r, i, a] = x(e);
|
|
26
|
-
this.calendar = t, this.era = n, this.year = r, this.month = i, this.day = a, p(this);
|
|
27
|
-
}
|
|
28
|
-
copy() {
|
|
29
|
-
return this.era ? new e(this.calendar, this.era, this.year, this.month, this.day) : new e(this.calendar, this.year, this.month, this.day);
|
|
30
|
-
}
|
|
31
|
-
add(e) {
|
|
32
|
-
return g(this, e);
|
|
33
|
-
}
|
|
34
|
-
subtract(e) {
|
|
35
|
-
return c(this, e);
|
|
36
|
-
}
|
|
37
|
-
set(e) {
|
|
38
|
-
return f(this, e);
|
|
39
|
-
}
|
|
40
|
-
cycle(e, t, n) {
|
|
41
|
-
return m(this, e, t, n);
|
|
42
|
-
}
|
|
43
|
-
toDate(e) {
|
|
44
|
-
return o(this, e);
|
|
45
|
-
}
|
|
46
|
-
toString() {
|
|
47
|
-
return y(this);
|
|
48
|
-
}
|
|
49
|
-
compare(e) {
|
|
50
|
-
return t(this, e);
|
|
51
|
-
}
|
|
52
|
-
}, C = class e {
|
|
53
|
-
constructor(...e) {
|
|
54
|
-
let [t, n, r, i, a] = x(e);
|
|
55
|
-
this.calendar = t, this.era = n, this.year = r, this.month = i, this.day = a, this.hour = e.shift() || 0, this.minute = e.shift() || 0, this.second = e.shift() || 0, this.millisecond = e.shift() || 0, p(this);
|
|
56
|
-
}
|
|
57
|
-
copy() {
|
|
58
|
-
return this.era ? new e(this.calendar, this.era, this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond) : new e(this.calendar, this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond);
|
|
59
|
-
}
|
|
60
|
-
add(e) {
|
|
61
|
-
return g(this, e);
|
|
62
|
-
}
|
|
63
|
-
subtract(e) {
|
|
64
|
-
return c(this, e);
|
|
65
|
-
}
|
|
66
|
-
set(e) {
|
|
67
|
-
return f(_(this, e), e);
|
|
68
|
-
}
|
|
69
|
-
cycle(e, t, n) {
|
|
70
|
-
switch (e) {
|
|
71
|
-
case "era":
|
|
72
|
-
case "year":
|
|
73
|
-
case "month":
|
|
74
|
-
case "day": return m(this, e, t, n);
|
|
75
|
-
default: return h(this, e, t, n);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
toDate(e, t) {
|
|
79
|
-
return o(this, e, t);
|
|
80
|
-
}
|
|
81
|
-
toString() {
|
|
82
|
-
return v(this);
|
|
83
|
-
}
|
|
84
|
-
compare(e) {
|
|
85
|
-
let r = t(this, e);
|
|
86
|
-
return r === 0 ? n(this, a(e)) : r;
|
|
87
|
-
}
|
|
88
|
-
}, w = class e {
|
|
89
|
-
constructor(...e) {
|
|
90
|
-
let [t, n, r, i, a] = x(e), o = e.shift(), s = e.shift();
|
|
91
|
-
this.calendar = t, this.era = n, this.year = r, this.month = i, this.day = a, this.timeZone = o, this.offset = s, this.hour = e.shift() || 0, this.minute = e.shift() || 0, this.second = e.shift() || 0, this.millisecond = e.shift() || 0, p(this);
|
|
92
|
-
}
|
|
93
|
-
copy() {
|
|
94
|
-
return this.era ? new e(this.calendar, this.era, this.year, this.month, this.day, this.timeZone, this.offset, this.hour, this.minute, this.second, this.millisecond) : new e(this.calendar, this.year, this.month, this.day, this.timeZone, this.offset, this.hour, this.minute, this.second, this.millisecond);
|
|
95
|
-
}
|
|
96
|
-
add(e) {
|
|
97
|
-
return u(this, e);
|
|
98
|
-
}
|
|
99
|
-
subtract(e) {
|
|
100
|
-
return l(this, e);
|
|
101
|
-
}
|
|
102
|
-
set(e, t) {
|
|
103
|
-
return s(this, e, t);
|
|
104
|
-
}
|
|
105
|
-
cycle(e, t, n) {
|
|
106
|
-
return d(this, e, t, n);
|
|
107
|
-
}
|
|
108
|
-
toDate() {
|
|
109
|
-
return r(this);
|
|
110
|
-
}
|
|
111
|
-
toString() {
|
|
112
|
-
return b(this);
|
|
113
|
-
}
|
|
114
|
-
toAbsoluteString() {
|
|
115
|
-
return this.toDate().toISOString();
|
|
116
|
-
}
|
|
117
|
-
compare(e) {
|
|
118
|
-
return this.toDate().getTime() - i(e, this.timeZone).toDate().getTime();
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
//#endregion
|
|
122
|
-
export { S as $2aaf608024c21ca1$export$99faa760c7908e4f, C as $2aaf608024c21ca1$export$ca871e8dbb80966f, w as $2aaf608024c21ca1$export$d3b7288e7994edea };
|