@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.10

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +713 -2
  3. package/dist/core/basemap.svelte.d.ts +38 -0
  4. package/dist/core/basemap.svelte.js +163 -0
  5. package/dist/core/context.d.ts +5 -0
  6. package/dist/core/context.js +13 -0
  7. package/dist/core/create-map.svelte.d.ts +274 -0
  8. package/dist/core/create-map.svelte.js +591 -0
  9. package/dist/core/gl.d.ts +97 -0
  10. package/dist/core/gl.js +1 -0
  11. package/dist/core/interactions.svelte.d.ts +50 -0
  12. package/dist/core/interactions.svelte.js +330 -0
  13. package/dist/core/plugin-host.svelte.d.ts +71 -0
  14. package/dist/core/plugin-host.svelte.js +445 -0
  15. package/dist/core/plugin.d.ts +190 -0
  16. package/dist/core/plugin.js +1 -0
  17. package/dist/core/scoped-context.d.ts +7 -0
  18. package/dist/core/scoped-context.js +34 -0
  19. package/dist/core/shape-view.d.ts +27 -0
  20. package/dist/core/shape-view.js +323 -0
  21. package/dist/data/format.d.ts +10 -0
  22. package/dist/data/format.js +37 -0
  23. package/dist/data/index.d.ts +9 -0
  24. package/dist/data/index.js +9 -0
  25. package/dist/data/messages/en.json +61 -0
  26. package/dist/data/messages.d.ts +10 -0
  27. package/dist/data/messages.js +17 -0
  28. package/dist/data/metadata.d.ts +22 -0
  29. package/dist/data/metadata.js +110 -0
  30. package/dist/data/refs.d.ts +18 -0
  31. package/dist/data/refs.js +61 -0
  32. package/dist/data/scale-types.d.ts +7 -0
  33. package/dist/data/scale-types.js +1 -0
  34. package/dist/data/scale.d.ts +13 -0
  35. package/dist/data/scale.js +27 -0
  36. package/dist/data/selection.d.ts +13 -0
  37. package/dist/data/selection.js +52 -0
  38. package/dist/data/static-source.d.ts +4 -0
  39. package/dist/data/static-source.js +3 -0
  40. package/dist/data/types.d.ts +74 -0
  41. package/dist/data/types.js +1 -0
  42. package/dist/index.d.ts +59 -0
  43. package/dist/index.js +42 -0
  44. package/dist/plugins/icons.d.ts +21 -0
  45. package/dist/plugins/icons.js +73 -0
  46. package/dist/plugins/points-clusters.d.ts +60 -0
  47. package/dist/plugins/points-clusters.js +93 -0
  48. package/dist/plugins/points.svelte.d.ts +105 -0
  49. package/dist/plugins/points.svelte.js +502 -0
  50. package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
  51. package/dist/plugins/scale-resolution.svelte.js +111 -0
  52. package/dist/plugins/shapes.svelte.d.ts +34 -0
  53. package/dist/plugins/shapes.svelte.js +325 -0
  54. package/dist/plugins/shared.d.ts +6 -0
  55. package/dist/plugins/shared.js +18 -0
  56. package/dist/plugins/style.d.ts +57 -0
  57. package/dist/plugins/style.js +193 -0
  58. package/dist/presets/CollocationTooltip.svelte +132 -0
  59. package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
  60. package/dist/presets/FireTooltip.svelte +70 -0
  61. package/dist/presets/FireTooltip.svelte.d.ts +13 -0
  62. package/dist/presets/MonitorClusterTooltip.svelte +168 -0
  63. package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
  64. package/dist/presets/MonitorReadingBox.svelte +58 -0
  65. package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
  66. package/dist/presets/MonitorTooltip.svelte +88 -0
  67. package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
  68. package/dist/presets/calheatscore.d.ts +24 -0
  69. package/dist/presets/calheatscore.js +22 -0
  70. package/dist/presets/cluster-area.d.ts +13 -0
  71. package/dist/presets/cluster-area.js +34 -0
  72. package/dist/presets/collocation-sites.d.ts +18 -0
  73. package/dist/presets/collocation-sites.js +67 -0
  74. package/dist/presets/fire-groups.d.ts +26 -0
  75. package/dist/presets/fire-groups.js +51 -0
  76. package/dist/presets/forecast-zones.d.ts +25 -0
  77. package/dist/presets/forecast-zones.js +23 -0
  78. package/dist/presets/hms-fire.d.ts +30 -0
  79. package/dist/presets/hms-fire.js +86 -0
  80. package/dist/presets/hms-smoke.d.ts +20 -0
  81. package/dist/presets/hms-smoke.js +22 -0
  82. package/dist/presets/monitors.d.ts +48 -0
  83. package/dist/presets/monitors.js +243 -0
  84. package/dist/presets/reactive-props.svelte.d.ts +4 -0
  85. package/dist/presets/reactive-props.svelte.js +7 -0
  86. package/dist/presets/regions.d.ts +24 -0
  87. package/dist/presets/regions.js +22 -0
  88. package/dist/stores/abort.d.ts +16 -0
  89. package/dist/stores/abort.js +52 -0
  90. package/dist/stores/collocation-store.svelte.d.ts +25 -0
  91. package/dist/stores/collocation-store.svelte.js +72 -0
  92. package/dist/stores/hms-stores.svelte.d.ts +38 -0
  93. package/dist/stores/hms-stores.svelte.js +97 -0
  94. package/dist/stores/index.d.ts +4 -0
  95. package/dist/stores/index.js +4 -0
  96. package/dist/stores/load-once.svelte.d.ts +25 -0
  97. package/dist/stores/load-once.svelte.js +93 -0
  98. package/dist/stores/monitors-store.svelte.d.ts +85 -0
  99. package/dist/stores/monitors-store.svelte.js +340 -0
  100. package/dist/theme.css +34 -0
  101. package/dist/ui/FeatureList.svelte +501 -0
  102. package/dist/ui/FeatureList.svelte.d.ts +14 -0
  103. package/dist/ui/Legend.svelte +214 -0
  104. package/dist/ui/Legend.svelte.d.ts +22 -0
  105. package/dist/ui/LegendBar.svelte +161 -0
  106. package/dist/ui/LegendBar.svelte.d.ts +11 -0
  107. package/dist/ui/MapControls.svelte +135 -0
  108. package/dist/ui/MapControls.svelte.d.ts +11 -0
  109. package/dist/ui/MapTooltip.svelte +397 -0
  110. package/dist/ui/MapTooltip.svelte.d.ts +37 -0
  111. package/dist/ui/PickNotice.svelte +56 -0
  112. package/dist/ui/PickNotice.svelte.d.ts +7 -0
  113. package/dist/ui/SelectionBar.svelte +123 -0
  114. package/dist/ui/SelectionBar.svelte.d.ts +10 -0
  115. package/dist/ui/TooltipItem.svelte +66 -0
  116. package/dist/ui/TooltipItem.svelte.d.ts +11 -0
  117. package/dist/ui/ViewSummary.svelte +97 -0
  118. package/dist/ui/ViewSummary.svelte.d.ts +7 -0
  119. package/dist/ui/card.d.ts +29 -0
  120. package/dist/ui/card.js +1 -0
  121. package/dist/ui/controls.css +45 -0
  122. package/dist/ui/data-credits.svelte.d.ts +9 -0
  123. package/dist/ui/data-credits.svelte.js +52 -0
  124. package/dist/ui/feature-list.d.ts +46 -0
  125. package/dist/ui/feature-list.js +115 -0
  126. package/dist/ui/legend-data.d.ts +41 -0
  127. package/dist/ui/legend-data.js +40 -0
  128. package/dist/ui/level-chip.d.ts +11 -0
  129. package/dist/ui/level-chip.js +15 -0
  130. package/dist/ui/value-format.d.ts +8 -0
  131. package/dist/ui/value-format.js +20 -0
  132. package/dist/ui/view-summary.d.ts +11 -0
  133. package/dist/ui/view-summary.js +33 -0
  134. package/dist/view/MapView.svelte +443 -0
  135. package/dist/view/MapView.svelte.d.ts +51 -0
  136. package/dist/view/create-gl-map.d.ts +34 -0
  137. package/dist/view/create-gl-map.js +186 -0
  138. package/dist/view/list-placement.d.ts +21 -0
  139. package/dist/view/list-placement.js +13 -0
  140. package/package.json +104 -5
@@ -0,0 +1,193 @@
1
+ /** Theme token read at plugin setup (getComputedStyle on the container) for unmatched values. */
2
+ export const NEUTRAL_COLOR_VAR = "--muted";
3
+ /**
4
+ * The last resort when the host defines none of a color's tokens (or no DOM, as in unit tests).
5
+ * Presentation only, never data, and the only hardcoded colors in the plugins.
6
+ */
7
+ export const FALLBACK_THEME_COLORS = {
8
+ neutral: "#9ca3af",
9
+ highlight: "#2563eb",
10
+ highlightForeground: "#ffffff",
11
+ foreground: "#111827",
12
+ mutedForeground: "#6b7280"
13
+ };
14
+ const THEME_TOKENS = {
15
+ neutral: [NEUTRAL_COLOR_VAR],
16
+ highlight: ["--highlight", "--primary"],
17
+ highlightForeground: ["--highlight-foreground", "--primary-foreground"],
18
+ foreground: ["--foreground"],
19
+ mutedForeground: ["--muted-foreground"]
20
+ };
21
+ /**
22
+ * Resolves the theme tokens on `container` (the `[data-mapsdk-map]` element) to colors MapLibre
23
+ * and marker images can use. Token values may be any CSS color (shadcn uses `oklch()`, which
24
+ * MapLibre can't parse), so each is drawn to a 1×1 canvas and read back as `rgba()`.
25
+ */
26
+ export function themeColors(container) {
27
+ const style = container && typeof getComputedStyle === "function" ? getComputedStyle(container) : null;
28
+ const read = (names, fallback) => {
29
+ for (const name of names) {
30
+ const raw = style?.getPropertyValue(name).trim() ?? "";
31
+ if (raw && CSS.supports("color", raw))
32
+ return toRgba(raw, fallback);
33
+ }
34
+ return fallback;
35
+ };
36
+ const out = { ...FALLBACK_THEME_COLORS };
37
+ for (const key of Object.keys(THEME_TOKENS)) {
38
+ out[key] = read(THEME_TOKENS[key], FALLBACK_THEME_COLORS[key]);
39
+ }
40
+ return out;
41
+ }
42
+ /**
43
+ * The raw values of every token `themeColors` reads, as one string: when it changes, the colors
44
+ * plugins baked into paint and images are stale.
45
+ */
46
+ export function themeSignature(container) {
47
+ const style = getComputedStyle(container);
48
+ return Object.values(THEME_TOKENS)
49
+ .flat()
50
+ .map((name) => style.getPropertyValue(name).trim())
51
+ .join("|");
52
+ }
53
+ /** A color canvas leaves `fillStyle` at when it rejects a value. */
54
+ const SENTINEL = "#010203";
55
+ function toRgba(color, fallback) {
56
+ const ctx = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
57
+ if (!ctx)
58
+ return color;
59
+ ctx.fillStyle = SENTINEL;
60
+ ctx.fillStyle = color;
61
+ // Unchanged: canvas couldn't parse it (an assignment it rejects is ignored), so not black.
62
+ if (ctx.fillStyle === SENTINEL && color.toLowerCase() !== SENTINEL)
63
+ return fallback;
64
+ ctx.fillRect(0, 0, 1, 1);
65
+ const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
66
+ return `rgba(${r}, ${g}, ${b}, ${Math.round((a / 255) * 1000) / 1000})`;
67
+ }
68
+ /** Breakpoint input for null and non-numeric values: below every real level. */
69
+ export const MISSING_VALUE = -1;
70
+ const MISSING = MISSING_VALUE;
71
+ function read(input) {
72
+ return typeof input === "string" ? ["get", input] : input;
73
+ }
74
+ /**
75
+ * The value as a number for a range lookup, like `levelFor`: a number, or a non-blank numeric
76
+ * string; anything else (null, missing, blank or non-numeric strings, booleans, arrays) is
77
+ * MISSING. `to-number` alone would read null, "", " " and false as 0 (the first level) and
78
+ * true as 1. There is no trim in expressions, so a string converting to 0 counts as blank unless
79
+ * it contains a "0" (every non-blank string Number() reads as 0 does).
80
+ *
81
+ * Accepted gap: "Infinity" and "1e999" convert to Infinity and step to the top level, while
82
+ * `levelFor` says NO_LEVEL (expressions have no clean finiteness check).
83
+ */
84
+ function numeric(input) {
85
+ const v = ["var", "v"];
86
+ const n = ["var", "n"];
87
+ return [
88
+ "let",
89
+ "v",
90
+ read(input),
91
+ [
92
+ "case",
93
+ ["==", ["typeof", v], "number"],
94
+ ["to-number", v],
95
+ ["==", ["typeof", v], "string"],
96
+ [
97
+ "let",
98
+ "n",
99
+ ["to-number", v, MISSING],
100
+ ["case", ["all", ["==", n, 0], ["!", ["in", "0", ["to-string", v]]]], MISSING, n]
101
+ ],
102
+ MISSING
103
+ ]
104
+ ];
105
+ }
106
+ /**
107
+ * True where the input is a reading by `numericValue`'s rule: a number, or a non-blank numeric
108
+ * string (same blank-string test as `numeric`; "Infinity" is the same accepted gap). A string
109
+ * that two different `to-number` fallbacks both pass through converted.
110
+ */
111
+ export function hasNumericValue(input) {
112
+ const v = ["var", "v"];
113
+ const a = ["var", "a"];
114
+ return [
115
+ "let",
116
+ "v",
117
+ read(input),
118
+ [
119
+ "case",
120
+ ["==", ["typeof", v], "number"],
121
+ true,
122
+ ["==", ["typeof", v], "string"],
123
+ [
124
+ "let",
125
+ "a",
126
+ ["to-number", v, -1],
127
+ [
128
+ "all",
129
+ ["==", a, ["to-number", v, -2]],
130
+ ["!", ["all", ["==", a, 0], ["!", ["in", "0", v]]]]
131
+ ]
132
+ ],
133
+ false
134
+ ]
135
+ ];
136
+ }
137
+ /**
138
+ * Half-open range lookup matching `levelFor`: a level covers [min, max), the value `max` goes
139
+ * to the next level, and a gap or an unbounded-below input falls to `fallback`.
140
+ */
141
+ function rangeStep(scale, input, fallback, pick) {
142
+ const stops = [];
143
+ let last = null;
144
+ const levels = scale.levels
145
+ .filter((l) => l.min !== null)
146
+ .sort((a, b) => a.min - b.min);
147
+ for (const level of levels) {
148
+ const min = level.min;
149
+ // Close a gap left by the previous level's max.
150
+ if (last !== null && last < min)
151
+ stops.push(last, fallback);
152
+ stops.push(min, pick(level) ?? fallback);
153
+ last = level.max ?? null;
154
+ if (last === null)
155
+ break;
156
+ }
157
+ if (last !== null)
158
+ stops.push(last, fallback);
159
+ if (stops.length === 0)
160
+ return ["literal", fallback];
161
+ return ["step", numeric(input), fallback, ...stops];
162
+ }
163
+ function categoryMatch(scale, input, fallback, pick) {
164
+ const arms = [];
165
+ for (const level of scale.levels) {
166
+ if (level.value === null)
167
+ continue;
168
+ arms.push(level.value, pick(level) ?? fallback);
169
+ }
170
+ if (arms.length === 0)
171
+ return ["literal", fallback];
172
+ return ["match", read(input), ...arms, fallback];
173
+ }
174
+ /** Per-level output for the level `input` falls in; `fallback` where no level applies. */
175
+ export function levelLookup(scale, input, fallback, pick) {
176
+ return scale.kind === "range"
177
+ ? rangeStep(scale, input, fallback, pick)
178
+ : categoryMatch(scale, input, fallback, pick);
179
+ }
180
+ /** Fill color per level; unmatched values and levels with an empty color use `neutral`. */
181
+ export function colorExpression(scale, property, neutral) {
182
+ return levelLookup(scale, property, neutral, (level) => level.color || null);
183
+ }
184
+ /** Opacity per level, or the plain `fallback` number when no level sets one. */
185
+ export function opacityExpression(scale, property, fallback) {
186
+ if (!scale.levels.some((level) => level.opacity !== null))
187
+ return fallback;
188
+ return levelLookup(scale, property, fallback, (level) => level.opacity);
189
+ }
190
+ /** Level key per value ("" where no level applies), for filters and icon ids. */
191
+ export function levelKeyExpression(scale, input) {
192
+ return levelLookup(scale, input, "", (level) => level.key);
193
+ }
@@ -0,0 +1,132 @@
1
+ <script lang="ts">
2
+ import type { FormatProvider } from "../data/format.js";
3
+ import type { MessageProvider } from "../data/messages.js";
4
+ import { levelFor, NO_LEVEL, numericValue } from "../data/scale.js";
5
+ import type { Scale } from "../data/scale-types.js";
6
+ import type { CollocationFeatureProperties } from "../data/types.js";
7
+ import { formatValue } from "../ui/value-format.js";
8
+ import MonitorReadingBox from "./MonitorReadingBox.svelte";
9
+
10
+ // Ported from monitor-map's CollocationTooltip: the reference monitor's reading beside its
11
+ // details, then the collocated monitor's with the box on the other side. Readings are whole
12
+ // numbers cut off, not rounded, as monitor-map's. The reference monitor's own name is shown
13
+ // here (the site's name when it isn't loaded), not above.
14
+
15
+ interface Props {
16
+ site: CollocationFeatureProperties;
17
+ scale: Scale | null;
18
+ messages: MessageProvider;
19
+ format: FormatProvider;
20
+ /** How often each monitor's type reports (e.g. "1 hour"); null leaves its note out. */
21
+ referenceInterval: string | null;
22
+ colocatedInterval: string | null;
23
+ }
24
+
25
+ let { site, scale, messages, format, referenceInterval, colocatedInterval }: Props = $props();
26
+
27
+ function side(
28
+ name: string | null,
29
+ role: string,
30
+ value: number | null,
31
+ timestamp: string | null,
32
+ interval: string | null
33
+ ) {
34
+ const found = scale ? levelFor(value, scale) : NO_LEVEL;
35
+ const level = found !== NO_LEVEL ? found : null;
36
+ const n = numericValue(value);
37
+ const valid = !!timestamp && !Number.isNaN(Date.parse(timestamp));
38
+ const zone = format.zoneLabel();
39
+ const formatted = valid ? format.dateTime(timestamp!) : null;
40
+ return {
41
+ name,
42
+ role,
43
+ color: level?.color || null,
44
+ level: level ? level.label : messages("mapsdk_no_level"),
45
+ reading: n === null ? null : formatValue(Math.trunc(n), 0, format),
46
+ subheading: interval ? messages("mapsdk_collocation_interval_avg", { interval }) : undefined,
47
+ at:
48
+ formatted && zone
49
+ ? messages("mapsdk_time_with_zone", { time: formatted, zone })
50
+ : formatted,
51
+ ago: valid
52
+ ? messages("mapsdk_collocation_updated", { when: format.relativeTime(timestamp!) })
53
+ : null
54
+ };
55
+ }
56
+
57
+ const sides = $derived([
58
+ side(
59
+ site.referenceName ?? site.name,
60
+ messages("mapsdk_collocation_reference"),
61
+ numericValue(site.value),
62
+ site.timestamp ?? null,
63
+ referenceInterval
64
+ ),
65
+ side(
66
+ site.colocatedName,
67
+ messages("mapsdk_collocation_colocated"),
68
+ site.colocatedValue,
69
+ site.colocatedTimestamp,
70
+ colocatedInterval
71
+ )
72
+ ]);
73
+ </script>
74
+
75
+ <div class="mapsdk-collocation-tip mapsdk-tooltip-wide">
76
+ {#each sides as entry, i (i)}
77
+ <div class="mapsdk-collocation-row" class:reverse={i === 1}>
78
+ <MonitorReadingBox
79
+ color={entry.color}
80
+ header={scale?.label ?? ""}
81
+ value={entry.reading ?? messages("mapsdk_no_reading")}
82
+ large={entry.reading !== null}
83
+ subheading={entry.subheading}
84
+ />
85
+ <div class="mapsdk-collocation-detail">
86
+ {#if entry.at}
87
+ <span>{entry.at}</span>
88
+ {/if}
89
+ {#if entry.name}
90
+ <span class="mapsdk-collocation-name text-lg">{entry.name}</span>
91
+ {/if}
92
+ <span>{entry.role}</span>
93
+ <span>{entry.level}</span>
94
+ {#if entry.ago}
95
+ <span class="mapsdk-collocation-ago text-base">{entry.ago}</span>
96
+ {/if}
97
+ </div>
98
+ </div>
99
+ {/each}
100
+ </div>
101
+
102
+ <style>
103
+ .mapsdk-collocation-tip {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 1rem;
107
+ }
108
+
109
+ .mapsdk-collocation-row {
110
+ display: flex;
111
+ gap: 0.5rem;
112
+ }
113
+
114
+ .mapsdk-collocation-row.reverse {
115
+ flex-direction: row-reverse;
116
+ }
117
+
118
+ .mapsdk-collocation-detail {
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 0.25rem;
122
+ }
123
+
124
+ .mapsdk-collocation-name {
125
+ font-weight: 700;
126
+ text-decoration: underline;
127
+ }
128
+
129
+ .mapsdk-collocation-ago {
130
+ margin-top: auto;
131
+ }
132
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { FormatProvider } from "../data/format.js";
2
+ import type { MessageProvider } from "../data/messages.js";
3
+ import type { Scale } from "../data/scale-types.js";
4
+ import type { CollocationFeatureProperties } from "../data/types.js";
5
+ interface Props {
6
+ site: CollocationFeatureProperties;
7
+ scale: Scale | null;
8
+ messages: MessageProvider;
9
+ format: FormatProvider;
10
+ /** How often each monitor's type reports (e.g. "1 hour"); null leaves its note out. */
11
+ referenceInterval: string | null;
12
+ colocatedInterval: string | null;
13
+ }
14
+ declare const CollocationTooltip: import("svelte").Component<Props, {}, "">;
15
+ type CollocationTooltip = ReturnType<typeof CollocationTooltip>;
16
+ export default CollocationTooltip;
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import type { FeatureListItem } from "../core/plugin.js";
3
+ import type { FormatProvider } from "../data/format.js";
4
+ import type { MessageProvider } from "../data/messages.js";
5
+ import { NO_LEVEL, numericValue } from "../data/scale.js";
6
+ import { formatValue } from "../ui/value-format.js";
7
+ import MonitorReadingBox from "./MonitorReadingBox.svelte";
8
+
9
+ // A fire's power in the monitors' reading box, beside its name and level. The value is cut
10
+ // off, not rounded, so it never reads as the next level (9.96 MW shows 9.9 beside "<10").
11
+ // No detection time: a satellite pass isn't when the fire started or whether it still burns.
12
+
13
+ interface Props {
14
+ item: FeatureListItem;
15
+ /** How many detections a group averages; null for a single detection. */
16
+ count: number | null;
17
+ messages: MessageProvider;
18
+ format: FormatProvider;
19
+ }
20
+
21
+ let { item, count, messages, format }: Props = $props();
22
+
23
+ const level = $derived(item.level !== NO_LEVEL ? item.level : null);
24
+ const value = $derived.by(() => {
25
+ const n = numericValue(item.value);
26
+ return n === null ? null : formatValue(n, item.precision, format, true);
27
+ });
28
+ const header = $derived(
29
+ item.units
30
+ ? messages("mapsdk_hms_fire_box_header", { units: item.units })
31
+ : messages("mapsdk_hms_fire_box_header_no_units")
32
+ );
33
+ </script>
34
+
35
+ <div class="mapsdk-fire-tip mapsdk-tooltip-wide">
36
+ <MonitorReadingBox
37
+ color={level?.color || null}
38
+ {header}
39
+ value={value ?? messages("mapsdk_no_reading")}
40
+ large={value !== null}
41
+ subheading={count !== null && count >= 2 ? messages("mapsdk_hms_fire_average") : undefined}
42
+ />
43
+ <div class="mapsdk-fire-detail">
44
+ <span class="mapsdk-fire-name text-lg">{item.name}</span>
45
+ <span>
46
+ {level
47
+ ? item.units
48
+ ? messages("mapsdk_level_with_units", { level: level.label, units: item.units })
49
+ : level.label
50
+ : messages("mapsdk_no_level")}
51
+ </span>
52
+ </div>
53
+ </div>
54
+
55
+ <style>
56
+ .mapsdk-fire-tip {
57
+ display: flex;
58
+ gap: 0.5rem;
59
+ }
60
+
61
+ .mapsdk-fire-name {
62
+ font-weight: 700;
63
+ }
64
+
65
+ .mapsdk-fire-detail {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 0.25rem;
69
+ }
70
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { FeatureListItem } from "../core/plugin.js";
2
+ import type { FormatProvider } from "../data/format.js";
3
+ import type { MessageProvider } from "../data/messages.js";
4
+ interface Props {
5
+ item: FeatureListItem;
6
+ /** How many detections a group averages; null for a single detection. */
7
+ count: number | null;
8
+ messages: MessageProvider;
9
+ format: FormatProvider;
10
+ }
11
+ declare const FireTooltip: import("svelte").Component<Props, {}, "">;
12
+ type FireTooltip = ReturnType<typeof FireTooltip>;
13
+ export default FireTooltip;
@@ -0,0 +1,168 @@
1
+ <script lang="ts">
2
+ import type { FormatProvider } from "../data/format.js";
3
+ import type { MessageProvider } from "../data/messages.js";
4
+ import type { MetadataProvider } from "../data/metadata.js";
5
+ import { levelFor, NO_LEVEL, numericValue } from "../data/scale.js";
6
+ import type { Scale } from "../data/scale-types.js";
7
+ import type { MonitorFeatureProperties } from "../data/types.js";
8
+ import type { ClusterInfo } from "../plugins/points-clusters.js";
9
+ import { formatValue } from "../ui/value-format.js";
10
+ import { clusterAreaSqMiles } from "./cluster-area.js";
11
+ import MonitorReadingBox from "./MonitorReadingBox.svelte";
12
+
13
+ // Ported from monitor-map's MonitorClusterTooltip: the cluster's average in a level-colored
14
+ // box beside its monitor type, count, and highest and lowest readings. The count and average
15
+ // come from the bubble itself, so they show at once; the rest fills in from the cluster's
16
+ // monitors. The average is a whole number and max and min are as reported, as monitor-map's.
17
+ // The count line adds the area the monitors are within (their convex hull) once they load.
18
+
19
+ interface Props {
20
+ cluster: ClusterInfo<MonitorFeatureProperties>;
21
+ scale: Scale | null;
22
+ messages: MessageProvider;
23
+ format: FormatProvider;
24
+ metadata: MetadataProvider;
25
+ }
26
+
27
+ let { cluster, scale, messages, format, metadata }: Props = $props();
28
+
29
+ /** From the cluster's monitors, once they load. */
30
+ interface Leaves {
31
+ isSjvair: boolean;
32
+ /** Readings only; monitors without one are left out. */
33
+ values: number[];
34
+ /** Square miles of the monitors' convex hull, every monitor included. */
35
+ area: number;
36
+ }
37
+
38
+ let leaves = $state.raw<Leaves | null>(null);
39
+ /** True once the monitors loaded or failed to. */
40
+ let settled = $state(false);
41
+ let typeLabel = $state<string | null>(null);
42
+
43
+ $effect(() => {
44
+ const controller = new AbortController();
45
+ const { group } = cluster;
46
+ leaves = null;
47
+ settled = false;
48
+ typeLabel = null;
49
+ metadata.monitorTypeLabel(group, controller.signal).then(
50
+ (label) => {
51
+ if (!controller.signal.aborted) typeLabel = label === "unavailable" ? group : label;
52
+ },
53
+ () => {
54
+ if (!controller.signal.aborted) typeLabel = group;
55
+ }
56
+ );
57
+ cluster.leaves().then(
58
+ (found) => {
59
+ if (controller.signal.aborted) return;
60
+ settled = true;
61
+ if (found.length === 0) return;
62
+ leaves = {
63
+ isSjvair: found[0].props.isSjvair,
64
+ values: found.flatMap(({ props }) => {
65
+ const n = numericValue(props.value);
66
+ return n === null ? [] : [n];
67
+ }),
68
+ area: clusterAreaSqMiles(found.map(({ lngLat }) => lngLat))
69
+ };
70
+ },
71
+ () => {
72
+ if (!controller.signal.aborted) settled = true;
73
+ }
74
+ );
75
+ return () => controller.abort();
76
+ });
77
+
78
+ /**
79
+ * The bubble's own average (its readings' sum over their count), unrounded: the level comes
80
+ * from it, as the bubble's color does. Only the number shown is rounded.
81
+ */
82
+ const mean = $derived.by(() => {
83
+ const sum = cluster.properties.sumValues;
84
+ const count = cluster.properties.countValues;
85
+ if (typeof sum !== "number" || typeof count !== "number" || count <= 0) return null;
86
+ return sum / count;
87
+ });
88
+ const average = $derived(mean === null ? null : formatValue(mean, 0, format));
89
+ const level = $derived.by(() => {
90
+ if (mean === null || !scale) return null;
91
+ const found = levelFor(mean, scale);
92
+ return found !== NO_LEVEL ? found : null;
93
+ });
94
+ const units = $derived(scale?.units ?? "");
95
+ /** The area to 3 decimals, or null when there is none to show (it would read 0.000). */
96
+ const area = $derived.by(() => {
97
+ // Decided on the number, so a host's own number format can't change what's hidden.
98
+ if (!leaves || Math.round(leaves.area * 1000) === 0) return null;
99
+ return format.number(leaves.area, {
100
+ minimumFractionDigits: 3,
101
+ maximumFractionDigits: 3,
102
+ useGrouping: false
103
+ });
104
+ });
105
+ const max = $derived(leaves?.values.length ? Math.max(...leaves.values) : null);
106
+ const min = $derived(leaves?.values.length ? Math.min(...leaves.values) : null);
107
+ </script>
108
+
109
+ <div class="mapsdk-monitor-tip mapsdk-tooltip-wide">
110
+ <MonitorReadingBox
111
+ color={level?.color || null}
112
+ header={scale?.label ?? ""}
113
+ value={average ?? messages("mapsdk_no_reading")}
114
+ large={average !== null}
115
+ subheading={messages("mapsdk_monitor_cluster_average")}
116
+ />
117
+ <div class="mapsdk-monitor-detail">
118
+ {#if typeLabel !== null && settled}
119
+ <span class="mapsdk-monitor-title text-lg">
120
+ {messages(
121
+ leaves?.isSjvair ? "mapsdk_monitor_cluster_title_sjvair" : "mapsdk_monitor_cluster_title",
122
+ { type: typeLabel }
123
+ )}
124
+ </span>
125
+ {/if}
126
+ <span>
127
+ {area
128
+ ? messages("mapsdk_monitor_cluster_count_area", { count: cluster.pointCount, area })
129
+ : messages("mapsdk_monitor_cluster_count", { count: cluster.pointCount })}
130
+ </span>
131
+ <span>{level ? level.label : messages("mapsdk_no_level")}</span>
132
+ {#if max !== null && min !== null}
133
+ <span class="mapsdk-monitor-extremes text-base">
134
+ {messages("mapsdk_monitor_cluster_max", {
135
+ value: formatValue(max, undefined, format),
136
+ units
137
+ })}
138
+ </span>
139
+ <span class="text-base">
140
+ {messages("mapsdk_monitor_cluster_min", {
141
+ value: formatValue(min, undefined, format),
142
+ units
143
+ })}
144
+ </span>
145
+ {/if}
146
+ </div>
147
+ </div>
148
+
149
+ <style>
150
+ .mapsdk-monitor-tip {
151
+ display: flex;
152
+ gap: 0.5rem;
153
+ }
154
+
155
+ .mapsdk-monitor-title {
156
+ font-weight: 700;
157
+ }
158
+
159
+ .mapsdk-monitor-extremes {
160
+ margin-top: auto;
161
+ }
162
+
163
+ .mapsdk-monitor-detail {
164
+ display: flex;
165
+ flex-direction: column;
166
+ gap: 0.25rem;
167
+ }
168
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { FormatProvider } from "../data/format.js";
2
+ import type { MessageProvider } from "../data/messages.js";
3
+ import type { MetadataProvider } from "../data/metadata.js";
4
+ import type { Scale } from "../data/scale-types.js";
5
+ import type { MonitorFeatureProperties } from "../data/types.js";
6
+ import type { ClusterInfo } from "../plugins/points-clusters.js";
7
+ interface Props {
8
+ cluster: ClusterInfo<MonitorFeatureProperties>;
9
+ scale: Scale | null;
10
+ messages: MessageProvider;
11
+ format: FormatProvider;
12
+ metadata: MetadataProvider;
13
+ }
14
+ declare const MonitorClusterTooltip: import("svelte").Component<Props, {}, "">;
15
+ type MonitorClusterTooltip = ReturnType<typeof MonitorClusterTooltip>;
16
+ export default MonitorClusterTooltip;
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import { levelChip } from "../ui/level-chip.js";
3
+
4
+ // monitor-map's DataBox: a reading in a box filled with its level's color.
5
+
6
+ interface Props {
7
+ /** The level color; null or unparseable → the neutral box. */
8
+ color: string | null;
9
+ header: string;
10
+ value: string;
11
+ /** Shows `value` large (a reading rather than a "No reading" note). */
12
+ large: boolean;
13
+ subheading?: string;
14
+ }
15
+
16
+ let { color, header, value, large, subheading }: Props = $props();
17
+
18
+ const style = $derived.by(() => {
19
+ const chip = levelChip(color);
20
+ return chip
21
+ ? `background:${chip.background};border-color:${chip.border};color:${chip.text}`
22
+ : "";
23
+ });
24
+ </script>
25
+
26
+ <div class="mapsdk-reading-box" class:neutral={!style} {style}>
27
+ {#if header}
28
+ <span class="text-base">{header}</span>
29
+ {/if}
30
+ <span class="mapsdk-reading-value" class:text-3xl={large}>{value}</span>
31
+ {#if subheading}
32
+ <span>{subheading}</span>
33
+ {/if}
34
+ </div>
35
+
36
+ <style>
37
+ .mapsdk-reading-box {
38
+ display: flex;
39
+ flex-direction: column;
40
+ align-items: center;
41
+ justify-content: center;
42
+ padding: 0.25rem;
43
+ border: 4px solid;
44
+ border-radius: calc(var(--radius-control, var(--radius)) / 2);
45
+ text-align: center;
46
+ white-space: nowrap;
47
+ }
48
+
49
+ .mapsdk-reading-box.neutral {
50
+ border-color: var(--border);
51
+ background: var(--muted);
52
+ color: var(--muted-foreground);
53
+ }
54
+
55
+ .mapsdk-reading-value {
56
+ font-weight: 700;
57
+ }
58
+ </style>
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ /** The level color; null or unparseable → the neutral box. */
3
+ color: string | null;
4
+ header: string;
5
+ value: string;
6
+ /** Shows `value` large (a reading rather than a "No reading" note). */
7
+ large: boolean;
8
+ subheading?: string;
9
+ }
10
+ declare const MonitorReadingBox: import("svelte").Component<Props, {}, "">;
11
+ type MonitorReadingBox = ReturnType<typeof MonitorReadingBox>;
12
+ export default MonitorReadingBox;