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

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 (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +487 -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 +207 -0
  8. package/dist/core/create-map.svelte.js +376 -0
  9. package/dist/core/gl.d.ts +92 -0
  10. package/dist/core/gl.js +1 -0
  11. package/dist/core/interactions.svelte.d.ts +42 -0
  12. package/dist/core/interactions.svelte.js +213 -0
  13. package/dist/core/plugin-host.svelte.d.ts +46 -0
  14. package/dist/core/plugin-host.svelte.js +366 -0
  15. package/dist/core/plugin.d.ts +148 -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/data/format.d.ts +10 -0
  20. package/dist/data/format.js +24 -0
  21. package/dist/data/index.d.ts +9 -0
  22. package/dist/data/index.js +9 -0
  23. package/dist/data/messages/en.json +51 -0
  24. package/dist/data/messages.d.ts +10 -0
  25. package/dist/data/messages.js +17 -0
  26. package/dist/data/metadata.d.ts +22 -0
  27. package/dist/data/metadata.js +110 -0
  28. package/dist/data/refs.d.ts +18 -0
  29. package/dist/data/refs.js +61 -0
  30. package/dist/data/scale-types.d.ts +7 -0
  31. package/dist/data/scale-types.js +1 -0
  32. package/dist/data/scale.d.ts +13 -0
  33. package/dist/data/scale.js +27 -0
  34. package/dist/data/selection.d.ts +7 -0
  35. package/dist/data/selection.js +31 -0
  36. package/dist/data/static-source.d.ts +4 -0
  37. package/dist/data/static-source.js +3 -0
  38. package/dist/data/types.d.ts +69 -0
  39. package/dist/data/types.js +1 -0
  40. package/dist/index.d.ts +56 -0
  41. package/dist/index.js +41 -0
  42. package/dist/plugins/icons.d.ts +21 -0
  43. package/dist/plugins/icons.js +73 -0
  44. package/dist/plugins/points-clusters.d.ts +60 -0
  45. package/dist/plugins/points-clusters.js +93 -0
  46. package/dist/plugins/points.svelte.d.ts +93 -0
  47. package/dist/plugins/points.svelte.js +473 -0
  48. package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
  49. package/dist/plugins/scale-resolution.svelte.js +111 -0
  50. package/dist/plugins/shapes.svelte.d.ts +29 -0
  51. package/dist/plugins/shapes.svelte.js +307 -0
  52. package/dist/plugins/shared.d.ts +6 -0
  53. package/dist/plugins/shared.js +18 -0
  54. package/dist/plugins/style.d.ts +57 -0
  55. package/dist/plugins/style.js +193 -0
  56. package/dist/presets/CollocationTooltip.svelte +132 -0
  57. package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
  58. package/dist/presets/MonitorClusterTooltip.svelte +149 -0
  59. package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
  60. package/dist/presets/MonitorReadingBox.svelte +58 -0
  61. package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
  62. package/dist/presets/MonitorTooltip.svelte +88 -0
  63. package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
  64. package/dist/presets/calheatscore.d.ts +24 -0
  65. package/dist/presets/calheatscore.js +22 -0
  66. package/dist/presets/collocation-sites.d.ts +18 -0
  67. package/dist/presets/collocation-sites.js +67 -0
  68. package/dist/presets/fire-groups.d.ts +26 -0
  69. package/dist/presets/fire-groups.js +51 -0
  70. package/dist/presets/forecast-zones.d.ts +25 -0
  71. package/dist/presets/forecast-zones.js +23 -0
  72. package/dist/presets/hms-fire.d.ts +26 -0
  73. package/dist/presets/hms-fire.js +87 -0
  74. package/dist/presets/hms-smoke.d.ts +19 -0
  75. package/dist/presets/hms-smoke.js +20 -0
  76. package/dist/presets/monitors.d.ts +48 -0
  77. package/dist/presets/monitors.js +243 -0
  78. package/dist/presets/reactive-props.svelte.d.ts +4 -0
  79. package/dist/presets/reactive-props.svelte.js +7 -0
  80. package/dist/presets/regions.d.ts +24 -0
  81. package/dist/presets/regions.js +22 -0
  82. package/dist/stores/abort.d.ts +16 -0
  83. package/dist/stores/abort.js +52 -0
  84. package/dist/stores/collocation-store.svelte.d.ts +25 -0
  85. package/dist/stores/collocation-store.svelte.js +72 -0
  86. package/dist/stores/hms-stores.svelte.d.ts +35 -0
  87. package/dist/stores/hms-stores.svelte.js +97 -0
  88. package/dist/stores/index.d.ts +4 -0
  89. package/dist/stores/index.js +4 -0
  90. package/dist/stores/load-once.svelte.d.ts +25 -0
  91. package/dist/stores/load-once.svelte.js +93 -0
  92. package/dist/stores/monitors-store.svelte.d.ts +85 -0
  93. package/dist/stores/monitors-store.svelte.js +340 -0
  94. package/dist/theme.css +34 -0
  95. package/dist/ui/FeatureList.svelte +501 -0
  96. package/dist/ui/FeatureList.svelte.d.ts +14 -0
  97. package/dist/ui/Legend.svelte +172 -0
  98. package/dist/ui/Legend.svelte.d.ts +13 -0
  99. package/dist/ui/LegendBar.svelte +161 -0
  100. package/dist/ui/LegendBar.svelte.d.ts +11 -0
  101. package/dist/ui/MapControls.svelte +135 -0
  102. package/dist/ui/MapControls.svelte.d.ts +11 -0
  103. package/dist/ui/MapTooltip.svelte +158 -0
  104. package/dist/ui/MapTooltip.svelte.d.ts +28 -0
  105. package/dist/ui/SelectionBar.svelte +123 -0
  106. package/dist/ui/SelectionBar.svelte.d.ts +10 -0
  107. package/dist/ui/TooltipItem.svelte +66 -0
  108. package/dist/ui/TooltipItem.svelte.d.ts +11 -0
  109. package/dist/ui/ViewSummary.svelte +97 -0
  110. package/dist/ui/ViewSummary.svelte.d.ts +7 -0
  111. package/dist/ui/controls.css +45 -0
  112. package/dist/ui/data-credits.svelte.d.ts +9 -0
  113. package/dist/ui/data-credits.svelte.js +52 -0
  114. package/dist/ui/feature-list.d.ts +31 -0
  115. package/dist/ui/feature-list.js +80 -0
  116. package/dist/ui/legend-data.d.ts +41 -0
  117. package/dist/ui/legend-data.js +40 -0
  118. package/dist/ui/level-chip.d.ts +11 -0
  119. package/dist/ui/level-chip.js +15 -0
  120. package/dist/ui/value-format.d.ts +7 -0
  121. package/dist/ui/value-format.js +13 -0
  122. package/dist/ui/view-summary.d.ts +11 -0
  123. package/dist/ui/view-summary.js +33 -0
  124. package/dist/view/MapView.svelte +373 -0
  125. package/dist/view/MapView.svelte.d.ts +43 -0
  126. package/dist/view/create-gl-map.d.ts +34 -0
  127. package/dist/view/create-gl-map.js +182 -0
  128. package/dist/view/list-placement.d.ts +21 -0
  129. package/dist/view/list-placement.js +13 -0
  130. package/package.json +103 -5
@@ -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,149 @@
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 MonitorReadingBox from "./MonitorReadingBox.svelte";
11
+
12
+ // Ported from monitor-map's MonitorClusterTooltip: the cluster's average in a level-colored
13
+ // box beside its monitor type, count, and highest and lowest readings. The count and average
14
+ // come from the bubble itself, so they show at once; the rest fills in from the cluster's
15
+ // monitors. The average is a whole number and max and min are as reported, as monitor-map's.
16
+
17
+ interface Props {
18
+ cluster: ClusterInfo<MonitorFeatureProperties>;
19
+ scale: Scale | null;
20
+ messages: MessageProvider;
21
+ format: FormatProvider;
22
+ metadata: MetadataProvider;
23
+ }
24
+
25
+ let { cluster, scale, messages, format, metadata }: Props = $props();
26
+
27
+ /** From the cluster's monitors, once they load. */
28
+ interface Leaves {
29
+ isSjvair: boolean;
30
+ /** Readings only; monitors without one are left out. */
31
+ values: number[];
32
+ }
33
+
34
+ let leaves = $state.raw<Leaves | null>(null);
35
+ /** True once the monitors loaded or failed to. */
36
+ let settled = $state(false);
37
+ let typeLabel = $state<string | null>(null);
38
+
39
+ $effect(() => {
40
+ const controller = new AbortController();
41
+ const { group } = cluster;
42
+ leaves = null;
43
+ settled = false;
44
+ typeLabel = null;
45
+ metadata.monitorTypeLabel(group, controller.signal).then(
46
+ (label) => {
47
+ if (!controller.signal.aborted) typeLabel = label === "unavailable" ? group : label;
48
+ },
49
+ () => {
50
+ if (!controller.signal.aborted) typeLabel = group;
51
+ }
52
+ );
53
+ cluster.leaves().then(
54
+ (found) => {
55
+ if (controller.signal.aborted) return;
56
+ settled = true;
57
+ if (found.length === 0) return;
58
+ leaves = {
59
+ isSjvair: found[0].props.isSjvair,
60
+ values: found.flatMap(({ props }) => {
61
+ const n = numericValue(props.value);
62
+ return n === null ? [] : [n];
63
+ })
64
+ };
65
+ },
66
+ () => {
67
+ if (!controller.signal.aborted) settled = true;
68
+ }
69
+ );
70
+ return () => controller.abort();
71
+ });
72
+
73
+ /**
74
+ * The bubble's own average (its readings' sum over their count), unrounded: the level comes
75
+ * from it, as the bubble's color does. Only the number shown is rounded.
76
+ */
77
+ const mean = $derived.by(() => {
78
+ const sum = cluster.properties.sumValues;
79
+ const count = cluster.properties.countValues;
80
+ if (typeof sum !== "number" || typeof count !== "number" || count <= 0) return null;
81
+ return sum / count;
82
+ });
83
+ const average = $derived(mean === null ? null : formatValue(mean, 0, format));
84
+ const level = $derived.by(() => {
85
+ if (mean === null || !scale) return null;
86
+ const found = levelFor(mean, scale);
87
+ return found !== NO_LEVEL ? found : null;
88
+ });
89
+ const units = $derived(scale?.units ?? "");
90
+ const max = $derived(leaves?.values.length ? Math.max(...leaves.values) : null);
91
+ const min = $derived(leaves?.values.length ? Math.min(...leaves.values) : null);
92
+ </script>
93
+
94
+ <div class="mapsdk-monitor-tip mapsdk-tooltip-wide">
95
+ <MonitorReadingBox
96
+ color={level?.color || null}
97
+ header={scale?.label ?? ""}
98
+ value={average ?? messages("mapsdk_no_reading")}
99
+ large={average !== null}
100
+ subheading={messages("mapsdk_monitor_cluster_average")}
101
+ />
102
+ <div class="mapsdk-monitor-detail">
103
+ {#if typeLabel !== null && settled}
104
+ <span class="mapsdk-monitor-title text-lg">
105
+ {messages(
106
+ leaves?.isSjvair ? "mapsdk_monitor_cluster_title_sjvair" : "mapsdk_monitor_cluster_title",
107
+ { type: typeLabel }
108
+ )}
109
+ </span>
110
+ {/if}
111
+ <span>{messages("mapsdk_monitor_cluster_count", { count: cluster.pointCount })}</span>
112
+ <span>{level ? level.label : messages("mapsdk_no_level")}</span>
113
+ {#if max !== null && min !== null}
114
+ <span class="mapsdk-monitor-extremes text-base">
115
+ {messages("mapsdk_monitor_cluster_max", {
116
+ value: formatValue(max, undefined, format),
117
+ units
118
+ })}
119
+ </span>
120
+ <span class="text-base">
121
+ {messages("mapsdk_monitor_cluster_min", {
122
+ value: formatValue(min, undefined, format),
123
+ units
124
+ })}
125
+ </span>
126
+ {/if}
127
+ </div>
128
+ </div>
129
+
130
+ <style>
131
+ .mapsdk-monitor-tip {
132
+ display: flex;
133
+ gap: 0.5rem;
134
+ }
135
+
136
+ .mapsdk-monitor-title {
137
+ font-weight: 700;
138
+ }
139
+
140
+ .mapsdk-monitor-extremes {
141
+ margin-top: auto;
142
+ }
143
+
144
+ .mapsdk-monitor-detail {
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: 0.25rem;
148
+ }
149
+ </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;
@@ -0,0 +1,88 @@
1
+ <script lang="ts">
2
+ import { formatValue } from "../ui/value-format.js";
3
+ import type { FeatureListItem } from "../core/plugin.js";
4
+ import type { FormatProvider } from "../data/format.js";
5
+ import type { MessageProvider } from "../data/messages.js";
6
+ import { NO_LEVEL, numericValue } from "../data/scale.js";
7
+ import type { Scale } from "../data/scale-types.js";
8
+ import MonitorReadingBox from "./MonitorReadingBox.svelte";
9
+
10
+ // Ported from monitor-map's MonitorTooltip: the reading in a level-colored box beside when it
11
+ // was taken, the monitor's name, and how long ago. The whole number is cut off, not rounded, as
12
+ // monitor-map's is; the level label is added for accessibility. The map tooltip leaves its own
13
+ // name header out.
14
+
15
+ interface Props {
16
+ item: FeatureListItem;
17
+ /** ISO 8601, or null when unknown. */
18
+ timestamp: string | null;
19
+ scale: Scale | null;
20
+ messages: MessageProvider;
21
+ format: FormatProvider;
22
+ /**
23
+ * How often the monitor's type reports (e.g. "1 hour"); null leaves the "(N avg)" note
24
+ * out.
25
+ */
26
+ interval: string | null;
27
+ }
28
+
29
+ let { item, timestamp, scale, messages, format, interval }: Props = $props();
30
+
31
+ // A level with an empty color shows its label in the neutral box.
32
+ const level = $derived(item.level !== NO_LEVEL ? item.level : null);
33
+ const reading = $derived.by(() => {
34
+ const n = numericValue(item.value);
35
+ return n === null ? null : formatValue(Math.trunc(n), 0, format);
36
+ });
37
+ const time = $derived.by(() => {
38
+ if (!timestamp || Number.isNaN(Date.parse(timestamp))) return null;
39
+ const formatted = format.dateTime(timestamp);
40
+ const zone = format.zoneLabel();
41
+ return {
42
+ at: zone ? messages("mapsdk_time_with_zone", { time: formatted, zone }) : formatted,
43
+ ago: messages("mapsdk_monitor_updated", { when: format.relativeTime(timestamp) })
44
+ };
45
+ });
46
+ </script>
47
+
48
+ <div class="mapsdk-monitor-tip mapsdk-tooltip-wide">
49
+ <MonitorReadingBox
50
+ color={level?.color || null}
51
+ header={scale?.label ?? ""}
52
+ value={reading ?? messages("mapsdk_no_reading")}
53
+ large={reading !== null}
54
+ subheading={interval ? messages("mapsdk_monitor_interval_avg", { interval }) : undefined}
55
+ />
56
+ <div class="mapsdk-monitor-detail">
57
+ {#if time}
58
+ <span>{time.at}</span>
59
+ {/if}
60
+ <span class="mapsdk-monitor-name text-lg">{item.name}</span>
61
+ <span>{level ? level.label : messages("mapsdk_no_level")}</span>
62
+ {#if time}
63
+ <span class="mapsdk-monitor-ago text-base">{time.ago}</span>
64
+ {/if}
65
+ </div>
66
+ </div>
67
+
68
+ <style>
69
+ .mapsdk-monitor-tip {
70
+ display: flex;
71
+ gap: 0.5rem;
72
+ }
73
+
74
+ .mapsdk-monitor-name {
75
+ font-weight: 700;
76
+ text-decoration: underline;
77
+ }
78
+
79
+ .mapsdk-monitor-ago {
80
+ margin-top: auto;
81
+ }
82
+
83
+ .mapsdk-monitor-detail {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 0.25rem;
87
+ }
88
+ </style>
@@ -0,0 +1,20 @@
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
+ import type { Scale } from "../data/scale-types.js";
5
+ interface Props {
6
+ item: FeatureListItem;
7
+ /** ISO 8601, or null when unknown. */
8
+ timestamp: string | null;
9
+ scale: Scale | null;
10
+ messages: MessageProvider;
11
+ format: FormatProvider;
12
+ /**
13
+ * How often the monitor's type reports (e.g. "1 hour"); null leaves the "(N avg)" note
14
+ * out.
15
+ */
16
+ interval: string | null;
17
+ }
18
+ declare const MonitorTooltip: import("svelte").Component<Props, {}, "">;
19
+ type MonitorTooltip = ReturnType<typeof MonitorTooltip>;
20
+ export default MonitorTooltip;
@@ -0,0 +1,24 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { DataSource } from "../data/types.js";
3
+ export interface CalHeatScorePluginOptions {
4
+ /** Default "calheatscore". */
5
+ id?: string;
6
+ source: DataSource;
7
+ /** Already-translated layer name for the list and summary. */
8
+ layerLabel: string;
9
+ /** Default true. */
10
+ selectable?: boolean;
11
+ }
12
+ /**
13
+ * CalHeatScore ZIP code areas filled by their heat score.
14
+ *
15
+ * The source's `scale` is expected to be the ScaleRef
16
+ * `{ dataset: "calheatscore", entryType: "heat-score" }`.
17
+ *
18
+ * This preset does not fetch. The host's data source supplies the features, and each feature's
19
+ * `refKey` must already be `region:<type>:<external_id>`: a ZIP, `region:zipcode:<zip>`, e.g.
20
+ * `region:zipcode:93721` (`zipcode` is the server's region type name). Use the region's
21
+ * `external_id` (its code), never the server's `id` (a sqid), which is not a ref. A picked
22
+ * feature selects `{ kind: "regions", ids: ["<type>:<code>"] }`.
23
+ */
24
+ export declare function calHeatScorePlugin(options: CalHeatScorePluginOptions): MapPlugin;
@@ -0,0 +1,22 @@
1
+ import { shapesPlugin } from "../plugins/shapes.svelte.js";
2
+ /**
3
+ * CalHeatScore ZIP code areas filled by their heat score.
4
+ *
5
+ * The source's `scale` is expected to be the ScaleRef
6
+ * `{ dataset: "calheatscore", entryType: "heat-score" }`.
7
+ *
8
+ * This preset does not fetch. The host's data source supplies the features, and each feature's
9
+ * `refKey` must already be `region:<type>:<external_id>`: a ZIP, `region:zipcode:<zip>`, e.g.
10
+ * `region:zipcode:93721` (`zipcode` is the server's region type name). Use the region's
11
+ * `external_id` (its code), never the server's `id` (a sqid), which is not a ref. A picked
12
+ * feature selects `{ kind: "regions", ids: ["<type>:<code>"] }`.
13
+ */
14
+ export function calHeatScorePlugin(options) {
15
+ return shapesPlugin({
16
+ id: options.id ?? "calheatscore",
17
+ source: options.source,
18
+ layerLabel: options.layerLabel,
19
+ mode: "choropleth",
20
+ selectable: options.selectable ?? true
21
+ });
22
+ }
@@ -0,0 +1,18 @@
1
+ import type { MapPlugin } from "../core/plugin.js";
2
+ import type { CollocationFeatureProperties, DataSource } from "../data/types.js";
3
+ export declare const COLLOCATION_CROSSHAIR_COLOR = "#4A5FC6";
4
+ export interface CollocationSitesPluginOptions {
5
+ /** Default "collocation-sites". */
6
+ id?: string;
7
+ /** Usually `createCollocationSitesStore(…).dataSource`. */
8
+ source: DataSource<CollocationFeatureProperties>;
9
+ /** Already-translated layer name for the list and summary. */
10
+ layerLabel: string;
11
+ }
12
+ /**
13
+ * Collocation sites as on today's live map: the reference monitor's marker in a crosshair, with
14
+ * a tooltip of the reference and collocated monitors' readings side by side. Register it before
15
+ * `monitorsPlugin` so it draws under the monitors; its tooltip still wins the hover. Not
16
+ * selectable.
17
+ */
18
+ export declare function collocationSitesPlugin(options: CollocationSitesPluginOptions): MapPlugin;
@@ -0,0 +1,67 @@
1
+ import { CROSSHAIR_ICON_SIZE, withCrosshair } from "../plugins/icons.js";
2
+ import { pointsPlugin } from "../plugins/points.svelte.js";
3
+ import CollocationTooltip from "./CollocationTooltip.svelte";
4
+ import { componentSnippet, intervalLookup, MONITOR_INACTIVE_COLOR, MONITOR_INSIDE_OUTLINE, monitorShape, SHAPES } from "./monitors.js";
5
+ // Ported from monitor-map 4.3 (src/lib/collocation-sites/): each site drawn at its reference
6
+ // monitor, in that monitor's shape and level color, wrapped in a crosshair.
7
+ // The user's decision (2026-11-04), an exception to the theme-token rule: the crosshair is this
8
+ // blue on every map and theme, as on sjvair.com.
9
+ export const COLLOCATION_CROSSHAIR_COLOR = "#4A5FC6";
10
+ const BORDER_WIDTH = 2;
11
+ /** `shape:location`, e.g. "triangle:inside", as the monitors preset's icons. */
12
+ function category(props) {
13
+ return `${monitorShape(props.type)}:${props.location === "inside" ? "inside" : "outside"}`;
14
+ }
15
+ /**
16
+ * Collocation sites as on today's live map: the reference monitor's marker in a crosshair, with
17
+ * a tooltip of the reference and collocated monitors' readings side by side. Register it before
18
+ * `monitorsPlugin` so it draws under the monitors; its tooltip still wins the hover. Not
19
+ * selectable.
20
+ */
21
+ export function collocationSitesPlugin(options) {
22
+ const intervalFor = intervalLookup();
23
+ const plugin = pointsPlugin({
24
+ id: options.id ?? "collocation-sites",
25
+ source: options.source,
26
+ layerLabel: options.layerLabel,
27
+ selectable: false,
28
+ icon: {
29
+ category,
30
+ draw(cat, color) {
31
+ const [shape, location] = cat.split(":");
32
+ const marker = SHAPES[shape](color, BORDER_WIDTH, location === "inside" ? MONITOR_INSIDE_OUTLINE : undefined);
33
+ return withCrosshair(marker, COLLOCATION_CROSSHAIR_COLOR);
34
+ },
35
+ size: CROSSHAIR_ICON_SIZE,
36
+ muted: (props) => !props.isActive,
37
+ mutedColor: MONITOR_INACTIVE_COLOR
38
+ },
39
+ tooltipShowsName: true,
40
+ tooltip: (_item, props, env) => componentSnippet(CollocationTooltip, {
41
+ site: props,
42
+ scale: env.scale,
43
+ messages: env.messages,
44
+ format: env.format,
45
+ referenceInterval: null,
46
+ colocatedInterval: null
47
+ }, Promise.all([
48
+ intervalFor(props.type, env.metadata),
49
+ props.colocatedType
50
+ ? intervalFor(props.colocatedType, env.metadata)
51
+ : Promise.resolve(null)
52
+ ]).then(([referenceInterval, colocatedInterval]) => ({
53
+ referenceInterval,
54
+ colocatedInterval
55
+ })))
56
+ });
57
+ // monitor-map registers the collocation tooltip at the highest priority, over the monitors.
58
+ plugin.hoverPriority = Number.MAX_SAFE_INTEGER;
59
+ // The sites use the monitors' scale, which the monitors' own section shows; only a loading
60
+ // or failure status still reports here.
61
+ const legend = plugin.legend;
62
+ plugin.legend = () => {
63
+ const data = legend?.() ?? null;
64
+ return data && "scale" in data ? null : data;
65
+ };
66
+ return plugin;
67
+ }
@@ -0,0 +1,26 @@
1
+ export interface FireGroup {
2
+ id: string;
3
+ /** Mean [lng, lat] of the group's fires. */
4
+ coordinates: [number, number];
5
+ /** Mean fire radiative power of the group's fires. */
6
+ avgFrp: number;
7
+ count: number;
8
+ }
9
+ export interface FireDetection {
10
+ id: string;
11
+ coordinates: [number, number];
12
+ /** Fire radiative power; a fire without a finite one is skipped. */
13
+ frp: number | null;
14
+ /** ISO 8601. Of several fires at the same coordinates, the latest is kept (else the first). */
15
+ timestamp?: string;
16
+ }
17
+ /**
18
+ * Drops fires without a finite FRP, then keeps one fire per exact coordinates: the latest by
19
+ * timestamp, else the first listed. Dropping first means a newer fire with no FRP hides nothing.
20
+ */
21
+ export declare function cleanFires<F extends FireDetection>(fires: F[]): F[];
22
+ /**
23
+ * Groups nearby fires (DBSCAN, 2 km, every fire its own seed) into one point per group, after
24
+ * skipping fires without a finite FRP and keeping one fire per coordinates, as monitor-map does.
25
+ */
26
+ export declare function groupFires(fires: FireDetection[]): FireGroup[];