@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.
Files changed (137) hide show
  1. package/dist/lib/LoadScreen.js +14 -14
  2. package/dist/lib/{src/MapView.js → MainMapView.js} +2 -2
  3. package/dist/lib/MapLegend.js +13 -0
  4. package/dist/lib/MonitorDetailPanel.js +169 -0
  5. package/dist/lib/MonitorMapLayout.js +91 -0
  6. package/dist/lib/_virtual/_rolldown/runtime.js +11 -0
  7. package/dist/lib/assets/logos/airgradient-blue.js +4 -0
  8. package/dist/lib/assets/logos/airnow-compact.js +4 -0
  9. package/dist/lib/assets/logos/carb-compact.js +4 -0
  10. package/dist/lib/assets/logos/purpleair-compact.js +4 -0
  11. package/dist/lib/assets/logos/sjvair.js +4 -0
  12. package/dist/lib/collocation-sites/CollocationTooltip.js +1 -1
  13. package/dist/lib/collocation-sites/collocations-map-integration.svelte.js +20 -20
  14. package/dist/lib/components/DataBox.js +1 -1
  15. package/dist/lib/components/DisplayOption.js +1 -1
  16. package/dist/lib/components/MapLayersDisplayOptions.js +44 -35
  17. package/dist/lib/components/SegmentedControl.js +4 -4
  18. package/dist/lib/components/ToggleSwitch.js +1 -1
  19. package/dist/lib/components/ui/button/button.js +4 -4
  20. package/dist/lib/components/ui/range-calendar/range-calendar-caption.js +57 -54
  21. package/dist/lib/components/ui/range-calendar/range-calendar-cell.js +19 -18
  22. package/dist/lib/components/ui/range-calendar/range-calendar-day.js +19 -18
  23. package/dist/lib/components/ui/range-calendar/range-calendar-grid-body.js +17 -16
  24. package/dist/lib/components/ui/range-calendar/range-calendar-grid-head.js +17 -16
  25. package/dist/lib/components/ui/range-calendar/range-calendar-grid-row.js +19 -18
  26. package/dist/lib/components/ui/range-calendar/range-calendar-grid.js +19 -18
  27. package/dist/lib/components/ui/range-calendar/range-calendar-head-cell.js +19 -18
  28. package/dist/lib/components/ui/range-calendar/range-calendar-header.js +19 -18
  29. package/dist/lib/components/ui/range-calendar/range-calendar-month-select.js +31 -31
  30. package/dist/lib/components/ui/range-calendar/range-calendar-month.js +19 -19
  31. package/dist/lib/components/ui/range-calendar/range-calendar-months.js +19 -19
  32. package/dist/lib/components/ui/range-calendar/range-calendar-nav.js +19 -19
  33. package/dist/lib/components/ui/range-calendar/range-calendar-next-button.js +27 -26
  34. package/dist/lib/components/ui/range-calendar/range-calendar-prev-button.js +27 -26
  35. package/dist/lib/components/ui/range-calendar/range-calendar-year-select.js +29 -29
  36. package/dist/lib/components/ui/range-calendar/range-calendar.js +154 -154
  37. package/dist/lib/data-chart/DataChart.js +76 -76
  38. package/dist/lib/ev-stations/components/EvStationClusterTooltip.js +16 -0
  39. package/dist/lib/ev-stations/components/EvStationTooltip.js +120 -0
  40. package/dist/lib/ev-stations/components/EvStationsDisplayOptions.js +41 -0
  41. package/dist/lib/ev-stations/data-maps.js +4 -0
  42. package/dist/lib/ev-stations/ev-station-icons.js +32 -0
  43. package/dist/lib/ev-stations/ev-stations-cluster-renderer.js +118 -0
  44. package/dist/lib/ev-stations/ev-stations-map-integration.svelte.js +181 -0
  45. package/dist/lib/ev-stations/ev-stations.svelte.js +33 -0
  46. package/dist/lib/ev-stations/zip-codes.js +136 -0
  47. package/dist/lib/hms/components/HMSFireLegend.js +35 -0
  48. package/dist/lib/index.js +19 -17
  49. package/dist/lib/map/Map.js +1 -1
  50. package/dist/lib/map/MapStyleDisplayOptions.js +16 -16
  51. package/dist/lib/map/Menu.js +23 -24
  52. package/dist/lib/map/integrations/map-icon-manager.js +3 -0
  53. package/dist/lib/map/utils.js +16 -13
  54. package/dist/lib/monitors/components/MonitorClusterTooltip.js +4 -4
  55. package/dist/lib/monitors/components/MonitorMarkersLegend.js +35 -0
  56. package/dist/lib/monitors/components/MonitorSubscriptionManager.js +1 -1
  57. package/dist/lib/monitors/components/MonitorTooltip.js +1 -1
  58. package/dist/lib/monitors/components/MonitorWidgetModal.js +1 -1
  59. package/dist/lib/monitors/components/MonitorsDisplayOptions.js +21 -21
  60. package/dist/lib/monitors/monitors-icon-manager.svelte.js +9 -9
  61. package/dist/lib/monitors/monitors-map-integration.svelte.js +36 -36
  62. package/dist/lib/monitors/monitors.svelte.js +38 -42
  63. package/dist/lib/router-context.js +13 -0
  64. package/dist/lib/routes.js +10 -0
  65. package/dist/lib/search/Search.js +124 -0
  66. package/dist/lib/search/service.js +90 -0
  67. package/dist/lib/sjvair-sjv.avif +0 -0
  68. package/dist/lib/sjvair-sjv.webp +0 -0
  69. package/dist/lib/sjvair-sjv2.avif +0 -0
  70. package/dist/lib/sjvair-sjv2.webp +0 -0
  71. package/dist/lib/utils.js +9 -3
  72. package/package.json +37 -36
  73. package/src/lib/LoadScreen.svelte +5 -5
  74. package/src/lib/MainMapView.svelte +1 -0
  75. package/src/lib/MapLegend.svelte +15 -0
  76. package/src/lib/MonitorDetailPanel.svelte +152 -0
  77. package/src/lib/MonitorMapLayout.svelte +193 -0
  78. package/src/lib/components/MapLayersDisplayOptions.svelte +6 -2
  79. package/src/lib/hms/components/HMSFireLegend.svelte +21 -0
  80. package/src/lib/index.ts +34 -0
  81. package/src/lib/map/Map.svelte +1 -0
  82. package/src/lib/map/Menu.svelte +4 -7
  83. package/src/lib/map/integrations/map-icon-manager.ts +4 -1
  84. package/src/lib/monitors/monitors.svelte.ts +1 -3
  85. package/src/lib/router-context.ts +48 -0
  86. package/src/lib/routes.ts +8 -0
  87. package/src/lib/search/Search.svelte +189 -0
  88. package/src/lib/search/service.ts +101 -0
  89. package/src/lib/utils.ts +11 -0
  90. package/dist/lib/assets/icons/adjustment-horizontal.js +0 -4
  91. package/dist/lib/node_modules/@internationalized/date/dist/private/CalendarDate.js +0 -122
  92. package/dist/lib/node_modules/@internationalized/date/dist/private/DateFormatter.js +0 -84
  93. package/dist/lib/node_modules/@internationalized/date/dist/private/calendars/GregorianCalendar.js +0 -91
  94. package/dist/lib/node_modules/@internationalized/date/dist/private/conversion.js +0 -104
  95. package/dist/lib/node_modules/@internationalized/date/dist/private/manipulation.js +0 -159
  96. package/dist/lib/node_modules/@internationalized/date/dist/private/queries.js +0 -32
  97. package/dist/lib/node_modules/@internationalized/date/dist/private/string.js +0 -37
  98. package/dist/lib/node_modules/@internationalized/date/dist/private/utils.js +0 -6
  99. package/dist/lib/node_modules/clsx/dist/clsx.js +0 -16
  100. package/dist/lib/node_modules/tailwind-merge/dist/bundle-mjs.js +0 -1723
  101. package/dist/lib/node_modules/tailwind-variants/dist/chunk-LQJYWU4O.js +0 -70
  102. package/dist/lib/node_modules/tailwind-variants/dist/chunk-RZF76H2U.js +0 -166
  103. package/dist/lib/node_modules/tailwind-variants/dist/index.js +0 -19
  104. package/dist/lib/node_modules/uplot/dist/uPlot.esm.js +0 -2147
  105. package/dist/lib/src/MonitorDetailPanel.js +0 -169
  106. package/dist/lib/src/router.js +0 -9
  107. package/dist/monitor-map/MonitorDetailPanel.css +0 -1
  108. package/dist/monitor-map/assets/MonitorDetailPanel-BU_pXZjp.js +0 -15
  109. package/dist/monitor-map/assets/validate-routes-CgbRa8fT.js +0 -1
  110. package/dist/monitor-map/inter-cyrillic-ext-wght-normal.woff2 +0 -0
  111. package/dist/monitor-map/inter-cyrillic-wght-normal.woff2 +0 -0
  112. package/dist/monitor-map/inter-greek-ext-wght-normal.woff2 +0 -0
  113. package/dist/monitor-map/inter-greek-wght-normal.woff2 +0 -0
  114. package/dist/monitor-map/inter-latin-ext-wght-normal.woff2 +0 -0
  115. package/dist/monitor-map/inter-latin-wght-normal.woff2 +0 -0
  116. package/dist/monitor-map/inter-vietnamese-wght-normal.woff2 +0 -0
  117. package/dist/monitor-map/lungs.svg +0 -99
  118. package/dist/monitor-map/sjvair-air.avif +0 -0
  119. package/dist/monitor-map/sjvair-air.png +0 -0
  120. package/dist/monitor-map/sjvair-air.webp +0 -0
  121. package/dist/monitor-map/sjvair-air2.avif +0 -0
  122. package/dist/monitor-map/sjvair-air2.png +0 -0
  123. package/dist/monitor-map/sjvair-air2.webp +0 -0
  124. package/dist/monitor-map/sjvair-icon.avif +0 -0
  125. package/dist/monitor-map/sjvair-icon.png +0 -0
  126. package/dist/monitor-map/sjvair-icon.webp +0 -0
  127. package/dist/monitor-map/sjvair-icon2.avif +0 -0
  128. package/dist/monitor-map/sjvair-icon2.png +0 -0
  129. package/dist/monitor-map/sjvair-icon2.webp +0 -0
  130. package/dist/monitor-map/sjvair-sjv.avif +0 -0
  131. package/dist/monitor-map/sjvair-sjv.png +0 -0
  132. package/dist/monitor-map/sjvair-sjv.webp +0 -0
  133. package/dist/monitor-map/sjvair-sjv2.avif +0 -0
  134. package/dist/monitor-map/sjvair-sjv2.png +0 -0
  135. package/dist/monitor-map/sjvair-sjv2.webp +0 -0
  136. package/dist/monitor-map/sjvairMonitorMap.css +0 -1
  137. 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";
@@ -6,6 +6,7 @@
6
6
 
7
7
  const { integrations }: { integrations: Array<SomeMapIntegration> } = $props();
8
8
 
9
+ // svelte-ignore state_referenced_locally (we only need the initial reference)
9
10
  integrationsManager.register(...integrations);
10
11
  </script>
11
12
 
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Attachment } from "svelte/attachments";
3
+ import { SlidersHorizontalIcon } from "@lucide/svelte";
3
4
 
4
5
  let { children } = $props();
5
6
  let isOpen: boolean = $state(false);
@@ -37,20 +38,16 @@
37
38
  data-trigger
38
39
  class:rounded-full={!isOpen}
39
40
  class:delay-100={!isOpen}
40
- class="absolute top-0 left-0 flex items-center justify-center overflow-hidden rounded bg-white p-1 shadow transition-all duration-0 select-none"
41
+ class="relative top-0 left-0 flex items-center justify-center overflow-hidden rounded bg-white p-1 shadow transition-all duration-0 select-none"
41
42
  >
42
43
  <button
43
44
  class:w-0={isOpen}
44
45
  class:h-0={isOpen}
45
46
  class:opacity-0={isOpen}
46
- class="relative size-8 transition-all"
47
+ class="relative size-10 transition-all flex justify-center items-center"
47
48
  aria-label="Display Options"
48
49
  >
49
- <enhanced:img
50
- src="$lib/assets/icons/adjustment-horizontal.svg"
51
- alt="SJVAir Icon"
52
- class="size-8"
53
- />
50
+ <SlidersHorizontalIcon size="24" />
54
51
  </button>
55
52
  <div
56
53
  class:h-55={isOpen}
@@ -1,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,8 @@
1
+ import MonitorMapLayout from "./MonitorMapLayout.svelte";
2
+ import MainMapView from "./MainMapView.svelte";
3
+
4
+ export const monitorMapRoutes = {
5
+ layout: MonitorMapLayout,
6
+ "/": MainMapView,
7
+ "/monitor/:id": () => import("./MonitorDetailPanel.svelte")
8
+ };
@@ -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 };