@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
@@ -0,0 +1,90 @@
1
+ import { monitorsManager as e } from "../monitors/monitors.svelte.js";
2
+ import t from "../assets/logos/airgradient-blue.js";
3
+ import n from "../assets/logos/airnow-compact.js";
4
+ import r from "../assets/logos/carb-compact.js";
5
+ import i from "../assets/logos/purpleair-compact.js";
6
+ import a from "../assets/logos/sjvair.js";
7
+ import { geocoding as o } from "@maptiler/sdk";
8
+ //#region src/lib/search/service.ts
9
+ var s = [-119.8, 36.76];
10
+ function c(e) {
11
+ switch (e.data_source.name) {
12
+ case "AQview": return {
13
+ url: r,
14
+ alt: "AQview"
15
+ };
16
+ case "AirNow.gov": return {
17
+ url: n,
18
+ alt: "AirNow.gov"
19
+ };
20
+ case "PurpleAir": return {
21
+ url: i,
22
+ alt: "PurpleAir"
23
+ };
24
+ case "Central California Asthma Collaborative": return {
25
+ url: a,
26
+ alt: "Central California Asthma Collaborative"
27
+ };
28
+ case "AirGradient": return {
29
+ url: t,
30
+ alt: "AirGradient"
31
+ };
32
+ default: return console.warn(`[search] Unknown data source: ${e.data_source.name}`), {
33
+ url: "",
34
+ alt: ""
35
+ };
36
+ }
37
+ }
38
+ var l = null;
39
+ function u() {
40
+ return l ??= d();
41
+ }
42
+ function d() {
43
+ return new Promise((e) => {
44
+ if (!navigator.geolocation) {
45
+ e(s);
46
+ return;
47
+ }
48
+ let t = setTimeout(() => e(s), 3e3);
49
+ navigator.geolocation.getCurrentPosition((n) => {
50
+ clearTimeout(t), e([n.coords.longitude, n.coords.latitude]);
51
+ }, () => {
52
+ clearTimeout(t), e(s);
53
+ });
54
+ });
55
+ }
56
+ async function f(t) {
57
+ if (!e.latest) return [];
58
+ let n = t.toLowerCase(), r = [], i = [], a = [];
59
+ for (let t of e.latest.values()) {
60
+ let e = t.name.toLowerCase();
61
+ e === n ? r.push(t) : e.startsWith(n) ? i.push(t) : e.includes(n) && a.push(t);
62
+ }
63
+ return [
64
+ ...r,
65
+ ...i,
66
+ ...a
67
+ ].slice(0, 4).map((e) => ({
68
+ type: "monitor",
69
+ monitor: e,
70
+ logo: c(e)
71
+ }));
72
+ }
73
+ async function p(e) {
74
+ let t = await u();
75
+ try {
76
+ return (await o.forward(e, {
77
+ country: ["us"],
78
+ fuzzyMatch: !0,
79
+ limit: 4,
80
+ proximity: t
81
+ })).features.map((e) => ({
82
+ type: "geocode",
83
+ feature: e
84
+ }));
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+ //#endregion
90
+ export { p as geocode, f as monitorSearch };
Binary file
Binary file
Binary file
Binary file
package/dist/lib/utils.js CHANGED
@@ -1,8 +1,14 @@
1
- import e from "./node_modules/clsx/dist/clsx.js";
2
- import { twMerge as t } from "./node_modules/tailwind-merge/dist/bundle-mjs.js";
1
+ import { clsx as e } from "clsx";
2
+ import { twMerge as t } from "tailwind-merge";
3
3
  //#region src/lib/utils.ts
4
4
  function n(...n) {
5
5
  return t(e(n));
6
6
  }
7
+ function r(e, t) {
8
+ let n;
9
+ return (...r) => {
10
+ clearTimeout(n), n = setTimeout(() => e(...r), t);
11
+ };
12
+ }
7
13
  //#endregion
8
- export { n as cn };
14
+ export { n as cn, r as debounce };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@sjvair/monitor-map",
3
3
  "description": "SJVAir's interactive monitor map",
4
- "version": "3.0.2",
4
+ "version": "3.0.3",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
7
- "build": "vite build",
7
+ "build:local": "PROD_MODE=local vite build",
8
+ "build": "PROD_MODE=production vite build",
8
9
  "build:lib": "vite build --config vite.config.lib.ts",
9
10
  "preview": "vite preview",
10
11
  "check": "svelte-check --tsconfig ./tsconfig.json",
@@ -24,64 +25,64 @@
24
25
  },
25
26
  "files": [
26
27
  "src/lib",
27
- "dist/lib",
28
- "dist/monitor-map"
28
+ "dist/lib"
29
29
  ],
30
30
  "sideEffects": [
31
31
  "**/*.css"
32
32
  ],
33
33
  "peerDependencies": {
34
+ "sv-router": "^0.16.2",
34
35
  "svelte": "^5.0.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@eslint/compat": "^2.1.0",
38
39
  "@eslint/js": "^10.0.1",
39
40
  "@fontsource-variable/inter": "^5.2.8",
40
- "@internationalized/date": "^3.12.1",
41
- "@lucide/svelte": "^1.16.0",
41
+ "@sveltejs/enhanced-img": "^0.11.0",
42
+ "@sveltejs/vite-plugin-svelte": "^7.2.0",
43
+ "@tailwindcss/vite": "^4.3.2",
44
+ "@types/node": "^26.1.1",
45
+ "eslint": "^10.6.0",
46
+ "eslint-config-prettier": "^10.1.8",
47
+ "eslint-plugin-svelte": "^3.20.0",
48
+ "globals": "^17.7.0",
49
+ "postcss": "^8.5.16",
50
+ "prettier": "^3.9.4",
51
+ "prettier-plugin-svelte": "^4.1.1",
52
+ "prettier-plugin-tailwindcss": "^0.8.0",
53
+ "shadcn-svelte": "^1.4.0",
54
+ "svelte": "^5.56.4",
55
+ "svelte-check": "^4.7.2",
56
+ "tailwindcss": "^4.3.2",
57
+ "tw-animate-css": "^1.4.0",
58
+ "typescript": "^6.0.3",
59
+ "typescript-eslint": "^8.63.0",
60
+ "vite": "^8.1.3"
61
+ },
62
+ "dependencies": {
63
+ "@internationalized/date": "^3.12.2",
64
+ "@lucide/svelte": "^1.23.0",
42
65
  "@maptiler/sdk": "^4.0.2",
43
66
  "@maptiler/weather": "^3.1.1",
44
67
  "@sjvair/sdk": "npm:@jsr/sjvair__sdk@^3.0.2",
45
- "@sveltejs/enhanced-img": "^0.10.4",
46
- "@sveltejs/vite-plugin-svelte": "^7.1.2",
47
- "@tailwindcss/vite": "^4.3.0",
48
68
  "@tstk/builtin-extensions": "npm:@jsr/tstk__builtin-extensions@^1.0.8",
49
- "@tstk/decorators": "npm:@jsr/tstk__decorators@^1.0.5",
50
- "@tstk/types": "npm:@jsr/tstk__types@^1.1.0",
51
69
  "@tstk/utils": "npm:@jsr/tstk__utils@^1.1.2",
52
70
  "@turf/area": "^7.3.5",
71
+ "@turf/clusters-dbscan": "^7.3.5",
53
72
  "@turf/helpers": "^7.3.5",
54
73
  "@types/geojson": "^7946.0.16",
55
- "@types/node": "^25.9.1",
56
74
  "bits-ui": "^2.18.1",
57
75
  "clsx": "^2.1.1",
58
- "color2k": "^2.0.3",
59
- "date-fns": "^4.3.0",
60
- "eslint": "^10.4.0",
61
- "eslint-config-prettier": "^10.1.8",
62
- "eslint-plugin-svelte": "^3.17.1",
63
- "globals": "^17.6.0",
64
- "postcss": "^8.5.15",
65
- "prettier": "^3.8.3",
66
- "prettier-plugin-svelte": "^4.0.1",
67
- "prettier-plugin-tailwindcss": "^0.8.0",
68
- "shadcn-svelte": "^1.2.7",
69
- "svelte": "^5.55.9",
70
- "svelte-check": "^4.4.8",
76
+ "color2k": "^2.0.4",
77
+ "date-fns": "^4.4.0",
71
78
  "tailwind-merge": "^3.6.0",
72
79
  "tailwind-variants": "^3.2.2",
73
- "tailwindcss": "^4.3.0",
74
- "tw-animate-css": "^1.4.0",
75
- "typescript": "^6.0.3",
76
- "typescript-eslint": "^8.60.0",
77
- "uplot": "^1.6.32",
78
- "vite": "^8.0.14"
79
- },
80
- "dependencies": {
81
- "@turf/clusters-dbscan": "^7.3.5",
82
- "sv-router": "^0.16.2"
80
+ "uplot": "^1.6.32"
83
81
  },
84
82
  "keywords": [
85
83
  "svelte"
86
- ]
84
+ ],
85
+ "allowScripts": {
86
+ "sharp@0.34.5": true
87
+ }
87
88
  }
@@ -32,14 +32,14 @@
32
32
 
33
33
  {#if loadScreenState.enabled}
34
34
  <div
35
- class="absolute top-0 left-0 z-50 flex h-full w-full flex-col items-center justify-center gap-4 overflow-hidden bg-white"
35
+ class="absolute top-0 left-0 z-50 flex h-full w-full items-center justify-center overflow-hidden bg-white"
36
36
  {@attach animationTrigger}
37
37
  >
38
- <div>
38
+ <div class="flex justify-around gap-4">
39
39
  <enhanced:img
40
40
  src="$lib/assets/logos/sjvair-parts/sjvair-icon.webp"
41
41
  alt="SJVAir Icon"
42
- class="icon inline-block h-24! w-auto! transition-all duration-500 ease-out"
42
+ class="icon inline-block h-24! w-auto! transition-all duration-500 ease-out object-contain"
43
43
  class:translate-x-0={loadScreenState.entered}
44
44
  class:-translate-x-full={!loadScreenState.entered}
45
45
  class:opacity-100={loadScreenState.entered}
@@ -48,7 +48,7 @@
48
48
  <enhanced:img
49
49
  src="$lib/assets/logos/sjvair-parts/sjvair-sjv.webp"
50
50
  alt="SJV"
51
- class="sjv inline-block h-24! w-auto! transition-all duration-500 ease-out"
51
+ class="sjv inline-block h-24! w-auto! transition-all duration-500 ease-out object-contain"
52
52
  class:translate-y-0={loadScreenState.entered}
53
53
  class:-translate-y-full={!loadScreenState.entered}
54
54
  class:opacity-100={loadScreenState.entered}
@@ -57,7 +57,7 @@
57
57
  <enhanced:img
58
58
  src="$lib/assets/logos/sjvair-parts/sjvair-air.webp"
59
59
  alt="Air"
60
- class="air inline-block h-24! w-auto! transition-all duration-500 ease-out"
60
+ class="air inline-block h-24! w-auto! transition-all duration-500 ease-out object-contain"
61
61
  class:translate-y-0={loadScreenState.entered}
62
62
  class:translate-y-full={!loadScreenState.entered}
63
63
  class:opacity-100={loadScreenState.entered}
@@ -0,0 +1 @@
1
+ <!-- Map view — the map renders in App.svelte, not in the router outlet -->
@@ -0,0 +1,15 @@
1
+ <script lang="ts">
2
+ import MonitorMarkersLegend from "$lib/monitors/components/MonitorMarkersLegend.svelte";
3
+ import HMSFireLegend from "$lib/hms/components/HMSFireLegend.svelte";
4
+ </script>
5
+
6
+ <!--
7
+ <div class="z-10 absolute bottom-6 left-6 w-1/2 lg:w-1/5">
8
+ <MonitorMarkersLegend />
9
+ <HMSFireLegend />
10
+ </div>
11
+ -->
12
+ <div class="flex flex-col gap-1 pointer-events-none pl-4 pb-4">
13
+ <HMSFireLegend />
14
+ <MonitorMarkersLegend />
15
+ </div>
@@ -0,0 +1,152 @@
1
+ <script lang="ts">
2
+ import type { ComponentProps } from "svelte";
3
+ import type { SJVAirEntryLevel } from "@sjvair/sdk";
4
+ import DataBox from "$lib/components/DataBox.svelte";
5
+ import MonitorWidgetModal from "$lib/monitors/components/MonitorWidgetModal.svelte";
6
+ import MonitorSubscriptionManager from "$lib/monitors/components/MonitorSubscriptionManager.svelte";
7
+ import DataChart from "$lib/data-chart/DataChart.svelte";
8
+ import { monitorsManager } from "$lib";
9
+ import { useMonitorMapRouter } from "./router-context";
10
+ import { fetchTempByCoords } from "$lib/weather.ts";
11
+ import { Colors, valueToColor } from "$lib/colors";
12
+ import { getCurrentLevel } from "$lib/monitors/monitor-utils.ts";
13
+
14
+ type TempDataBox = Pick<ComponentProps<typeof DataBox>, "color" | "value">;
15
+
16
+ interface BadgeOpts {
17
+ bgClass?: string;
18
+ iconColorClass?: string;
19
+ labelColorClass?: string;
20
+ href?: string;
21
+ }
22
+ const { route, navigate, basePath } = useMonitorMapRouter();
23
+
24
+ const monitor = $derived(monitorsManager.latest?.get(route.params.id ?? ""));
25
+ let tempData: TempDataBox | undefined = $state();
26
+ const entryData: ComponentProps<typeof DataBox> | undefined = $derived.by(() => {
27
+ if (!monitor || !monitorsManager.meta) return;
28
+
29
+ const entryType = monitorsManager.meta.entryType(monitor.latest.entry_type);
30
+ const level: SJVAirEntryLevel | undefined = getCurrentLevel(
31
+ monitor.latest.value,
32
+ entryType.asIter.levels!
33
+ );
34
+
35
+ const updateDuration = monitorsManager.meta.monitorType(monitor.type).expected_interval;
36
+
37
+ return {
38
+ color: level?.color ?? "",
39
+ header: entryType.label,
40
+ subheading: `(${updateDuration} avg)`,
41
+ value: Math.round(parseInt(monitor.latest.value, 10)).toString()
42
+ };
43
+ });
44
+
45
+ const tempColors = [
46
+ { min: -Infinity, color: Colors.blue },
47
+ { min: 65, color: Colors.green },
48
+ { min: 78, color: Colors.yellow },
49
+ { min: 95, color: Colors.red }
50
+ ];
51
+
52
+ $effect(() => {
53
+ if (monitor && monitor.position) {
54
+ fetchTempByCoords(monitor.position.coordinates).then((temperature) => {
55
+ tempData = {
56
+ value: `${temperature}°F`,
57
+ color: valueToColor(temperature, tempColors)
58
+ };
59
+ });
60
+ }
61
+ });
62
+ </script>
63
+
64
+ {#snippet badge(icon: string, label: string, opts: BadgeOpts = {})}
65
+ <span
66
+ class="flex items-center justify-center gap-1 rounded-sm border px-1 py-0.5
67
+ {opts.bgClass ?? 'bg-neutral-100'}"
68
+ >
69
+ <span class={["svg-icon", icon, opts.iconColorClass ?? "bg-black/70"]}></span>
70
+ {#if opts.href}
71
+ <a class="text-link text-xs" href={opts.href} target="_blank" translate="no">{label}</a>
72
+ {:else}
73
+ <span
74
+ class={["text-xs whitespace-nowrap", opts.labelColorClass ?? "text-black/70"]}
75
+ translate="no">{label}</span
76
+ >
77
+ {/if}
78
+ </span>
79
+ {/snippet}
80
+
81
+ <div class="flex h-full w-full flex-col overflow-y-auto bg-white">
82
+ <div
83
+ class="flex flex-col items-center justify-center gap-8 border-b border-gray-200 p-4 shadow-lg"
84
+ >
85
+ <button
86
+ class="cursor-pointer self-end rounded border border-gray-300 px-2 py-1 text-sm hover:bg-gray-50"
87
+ onclick={() => navigate(`${basePath}/`)}
88
+ >
89
+ ✕ Close
90
+ </button>
91
+ {#if monitor}
92
+ <h2 class="text-center text-3xl font-semibold">{monitor.name}</h2>
93
+ <div class="flex flex-wrap items-center justify-center gap-4">
94
+ {#if monitor.is_sjvair}
95
+ {@render badge("lungs", "SJVAir", {
96
+ bgClass: "bg-aqua-haze",
97
+ iconColorClass: "bg-sky-600",
98
+ labelColorClass: "text-sky-600"
99
+ })}
100
+ {/if}
101
+
102
+ {@render badge("rss-feed", monitor.data_source.name, { href: monitor.data_source.url })}
103
+ {@render badge("router", monitor.device)}
104
+ {@render badge("location-on", monitor.county)}
105
+ {@render badge("location-searching", monitor.location)}
106
+ </div>
107
+ {/if}
108
+ </div>
109
+
110
+ <div class="flex flex-col items-center gap-12 overflow-y-auto p-5 pt-10">
111
+ {#if monitor}
112
+ <div class="flex w-full justify-around">
113
+ {#if tempData}
114
+ <DataBox color={tempData.color} header="Temperature" value={tempData.value} />
115
+ {/if}
116
+ {#if entryData}
117
+ <DataBox
118
+ color={entryData.color}
119
+ header={entryData.header}
120
+ subheading={entryData.subheading}
121
+ value={entryData.value}
122
+ />
123
+ {/if}
124
+ </div>
125
+
126
+ <MonitorWidgetModal monitorId={monitor.id} />
127
+ <!-- eslint-disable-next-line @typescript-eslint/no-explicit-any -->
128
+ {#if (globalThis as any).USER?.is_authenticated}
129
+ <MonitorSubscriptionManager monitorId={monitor.id} />
130
+ {/if}
131
+
132
+ <div class="w-full">
133
+ <DataChart {monitor} />
134
+ </div>
135
+
136
+ <div class="w-full">
137
+ <h3 class="font-semibold">Data Provided Courtesy Of:</h3>
138
+ <ul>
139
+ {#each monitor.data_providers as provider, idx (idx)}
140
+ <li>
141
+ {#if provider.url}
142
+ <a class="text-link" href={provider.url} target="_blank">{provider.name}</a>
143
+ {:else}
144
+ <span>{provider.name}</span>
145
+ {/if}
146
+ </li>
147
+ {/each}
148
+ </ul>
149
+ </div>
150
+ {/if}
151
+ </div>
152
+ </div>
@@ -0,0 +1,193 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import { onDestroy } from "svelte";
4
+ import LoadScreen, { disable as disableLoadScreen } from "$lib/LoadScreen.svelte";
5
+ import Map from "$lib/map/Map.svelte";
6
+ import Menu from "$lib/map/Menu.svelte";
7
+ import MonitorsDisplayOptions from "$lib/monitors/components/MonitorsDisplayOptions.svelte";
8
+ import MapLayersDisplayOptions from "$lib/components/MapLayersDisplayOptions.svelte";
9
+ import MapStyleDisplayOptions from "$lib/map/MapStyleDisplayOptions.svelte";
10
+ import { mapManager } from "$lib/map/map.svelte";
11
+ import { monitorsManager } from "$lib/monitors/monitors.svelte";
12
+ import { monitorsMapIntegration } from "$lib/monitors/monitors-map-integration.svelte.ts";
13
+ import { windMapIntegration } from "$lib/wind/wind.svelte";
14
+ import { baseLayerSeperator } from "$lib/map/integrations/base-layer-seperator";
15
+ import type { SomeMapIntegration } from "$lib/map/integrations/types";
16
+ import { collocationSitesManager } from "$lib/collocation-sites/collocations.svelte";
17
+ import { collocationSitesMapIntegration } from "$lib/collocation-sites/collocations-map-integration.svelte";
18
+ import EvStationsDisplayOptions from "$lib/ev-stations/components/EvStationsDisplayOptions.svelte";
19
+ import { evStationsMapIntegration } from "$lib/ev-stations/ev-stations-map-integration.svelte.ts";
20
+ import { hmsManager } from "$lib/hms/hms.svelte.ts";
21
+ import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte.ts";
22
+ import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte.ts";
23
+ import MapLegend from "$lib/MapLegend.svelte";
24
+ import Search from "$lib/search/Search.svelte";
25
+ import { useMonitorMapRouter } from "./router-context";
26
+
27
+ interface Props {
28
+ children: Snippet;
29
+ }
30
+
31
+ let { children }: Props = $props();
32
+
33
+ const { route, navigate, basePath } = useMonitorMapRouter();
34
+
35
+ const TRANSITION_MS = 300;
36
+
37
+ const integrations: Array<SomeMapIntegration> = [
38
+ baseLayerSeperator,
39
+ collocationSitesMapIntegration,
40
+ windMapIntegration,
41
+ hmsSmokeMapIntegration,
42
+ hmsFireMapIntegration,
43
+ monitorsMapIntegration,
44
+ evStationsMapIntegration
45
+ ];
46
+
47
+ monitorsManager.init(route.search.pollutant);
48
+ collocationSitesManager.init();
49
+ hmsManager.init();
50
+ monitorsMapIntegration.onMonitorClick = (id: string) => {
51
+ navigate(`${basePath}/monitor/:id`, { params: { id } }).catch(console.error);
52
+ };
53
+
54
+ let panelOpen = $derived(route.pathname.startsWith(`${basePath}/monitor/`));
55
+
56
+ $effect(() => {
57
+ if (mapManager.map && monitorsManager.initialized) {
58
+ mapManager.map.once("idle", () => disableLoadScreen());
59
+ }
60
+ });
61
+
62
+ $effect(() => {
63
+ if (panelOpen) {
64
+ // Container width snapped to open; resize after layout settles
65
+ requestAnimationFrame(() => mapManager.map?.resize());
66
+ } else {
67
+ // Container width snaps back after the transform transition ends
68
+ setTimeout(() => mapManager.map?.resize(), TRANSITION_MS);
69
+ }
70
+ });
71
+
72
+ // Clear selected icon scale when the detail panel closes
73
+ $effect(() => {
74
+ if (panelOpen) return;
75
+ monitorsMapIntegration.selectedMonitorId = null;
76
+ });
77
+
78
+ onDestroy(() => {
79
+ monitorsManager.autoUpdate.stop();
80
+ });
81
+
82
+ /**
83
+ * HACK: Fix for escaping sv-router and allowing navigation to other pages,
84
+ * as well as navigating back
85
+ */
86
+ const rootPath = basePath || "/";
87
+ const knownRoutes = [`${basePath}/monitor/`];
88
+
89
+ window.addEventListener("pageshow", (e) => {
90
+ if (e.persisted) window.location.reload();
91
+ });
92
+
93
+ document.addEventListener(
94
+ "click",
95
+ (e) => {
96
+ const anchor = e
97
+ .composedPath()
98
+ .find((el) => el instanceof HTMLAnchorElement) as HTMLAnchorElement;
99
+ if (!anchor) return;
100
+ const { pathname } = new URL(anchor.href);
101
+ const isKnown = pathname === rootPath || knownRoutes.some((r) => pathname.startsWith(r));
102
+ if (!isKnown) {
103
+ e.stopImmediatePropagation();
104
+ window.location.href = anchor.href;
105
+ }
106
+ },
107
+ { capture: true }
108
+ );
109
+ </script>
110
+
111
+ <div class="shell" class:panel-open={panelOpen}>
112
+ <LoadScreen />
113
+ <div class="relative flex-1 overflow-hidden">
114
+ <Map {integrations} />
115
+ <div class="z-10 absolute bottom-0 left-0 pointer-events-none">
116
+ <MapLegend />
117
+ </div>
118
+ <div class="absolute top-4 left-4 z-10">
119
+ <div class="absolute z-10">
120
+ <Menu>
121
+ <MonitorsDisplayOptions />
122
+ <EvStationsDisplayOptions />
123
+ <MapLayersDisplayOptions />
124
+ <MapStyleDisplayOptions />
125
+ </Menu>
126
+ </div>
127
+ <div class="absolute left-16">
128
+ <Search />
129
+ </div>
130
+ </div>
131
+ </div>
132
+ <div class="panel-container">
133
+ <div class="panel-content">
134
+ {@render children()}
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <style>
140
+ .shell {
141
+ display: flex;
142
+ position: relative;
143
+ width: 100%;
144
+ height: 100%;
145
+ }
146
+
147
+ .panel-container {
148
+ flex-shrink: 0;
149
+ overflow: hidden;
150
+ width: 0;
151
+ }
152
+
153
+ .shell.panel-open .panel-container {
154
+ width: 33.333vw;
155
+ }
156
+
157
+ .panel-content {
158
+ width: 33.333vw;
159
+ height: 100%;
160
+ transform: translateX(100%);
161
+ transition: transform 300ms ease-in-out;
162
+ }
163
+
164
+ .shell.panel-open .panel-content {
165
+ transform: translateX(0);
166
+ }
167
+
168
+ @media (max-width: 768px) {
169
+ .shell {
170
+ flex-direction: column;
171
+ }
172
+
173
+ .panel-container {
174
+ width: 100vw;
175
+ height: 0;
176
+ }
177
+
178
+ .shell.panel-open .panel-container {
179
+ width: 100vw;
180
+ height: 50vh;
181
+ }
182
+
183
+ .panel-content {
184
+ width: 100vw;
185
+ height: 50vh;
186
+ transform: translateY(100%);
187
+ }
188
+
189
+ .shell.panel-open .panel-content {
190
+ transform: translateY(0);
191
+ }
192
+ }
193
+ </style>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { WindIcon, CrosshairIcon, FlameIcon, WavesVerticalIcon } from "@lucide/svelte";
2
+ import { WindIcon, CrosshairIcon, WavesVerticalIcon } from "@lucide/svelte";
3
3
  import DisplayOption from "$lib/components/DisplayOption.svelte";
4
4
  import ToggleSwitch from "$lib/components/ToggleSwitch.svelte";
5
5
  import { windMapIntegration } from "$lib/wind/wind.svelte";
@@ -7,6 +7,8 @@
7
7
  import { monitorsManager } from "$lib/monitors/monitors.svelte";
8
8
  import { hmsFireMapIntegration } from "$lib/hms/hms-fire-map-integration.svelte";
9
9
  import { hmsSmokeMapIntegration } from "$lib/hms/hms-smoke-map-integration.svelte";
10
+
11
+ const fireIcon = hmsFireMapIntegration.icons.get("hms-fire-xxl");
10
12
  </script>
11
13
 
12
14
  <DisplayOption>
@@ -26,7 +28,9 @@
26
28
  </div>
27
29
  {/if}
28
30
  <div class="flex gap-1 items-center">
29
- <FlameIcon size="16" />
31
+ {#if fireIcon}
32
+ <img class="w-4" src={fireIcon.image.src} alt={fireIcon.image.alt} />
33
+ {/if}
30
34
  <ToggleSwitch id="hms-fire" label="HMS Fire" bind:value={hmsFireMapIntegration.enabled}
31
35
  ></ToggleSwitch>
32
36
  </div>
@@ -0,0 +1,21 @@
1
+ <script lang="ts">
2
+ import { hmsFireMapIntegration } from "../hms-fire-map-integration.svelte.ts";
3
+
4
+ const levels = ["<10", "10-49", "50-149", "150-349", "≥350"];
5
+ </script>
6
+
7
+ {#if hmsFireMapIntegration.enabled && hmsFireMapIntegration.features.length > 0}
8
+ <div class="flex flex-col bg-white rounded shadow w-fit p-1">
9
+ <p class="text-center whitespace-nowrap font-semibold">Fire Points</p>
10
+ <div class="flex flex-col">
11
+ {#each hmsFireMapIntegration.icons.iter as [_id, icon], idx (idx)}
12
+ <div class="flex gap-1 items-center">
13
+ <div class="size-9 flex justify-center items-center">
14
+ <img src={icon.image.src} alt={icon.image.alt} />
15
+ </div>
16
+ <span>{levels[idx]} MW</span>
17
+ </div>
18
+ {/each}
19
+ </div>
20
+ </div>
21
+ {/if}