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

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 +506 -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 +43 -0
  12. package/dist/core/interactions.svelte.js +230 -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 +155 -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 +52 -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 +100 -0
  47. package/dist/plugins/points.svelte.js +501 -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 +505 -0
  96. package/dist/ui/FeatureList.svelte.d.ts +14 -0
  97. package/dist/ui/Legend.svelte +213 -0
  98. package/dist/ui/Legend.svelte.d.ts +22 -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 +72 -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 +38 -0
  115. package/dist/ui/feature-list.js +95 -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,155 @@
1
+ import type { CustomLayerInterface, LayerSpecification, SourceSpecification } from "@maptiler/sdk";
2
+ import type { Geometry } from "geojson";
3
+ import type { Snippet } from "svelte";
4
+ import type { FormatProvider } from "../data/format.js";
5
+ import type { MessageProvider } from "../data/messages.js";
6
+ import type { MetadataProvider } from "../data/metadata.js";
7
+ import type { NO_LEVEL } from "../data/scale.js";
8
+ import type { Level, Scale } from "../data/scale-types.js";
9
+ import type { DataStatus, FeatureRef } from "../data/types.js";
10
+ import type { GlMapLike } from "./gl.js";
11
+ export type Providers = {
12
+ metadata: MetadataProvider;
13
+ messages: MessageProvider;
14
+ format: FormatProvider;
15
+ };
16
+ /**
17
+ * A layer spec without its id (the host assigns it). Distributive: `Omit` over the
18
+ * `LayerSpecification` union would keep only the keys every layer type shares, losing `source`.
19
+ */
20
+ export type LayerSpecWithoutId = LayerSpecification extends infer L ? L extends unknown ? Omit<L, "id"> : never : never;
21
+ export interface FeatureHit {
22
+ pluginId: string;
23
+ ref: FeatureRef;
24
+ refKey: string;
25
+ name: string;
26
+ /**
27
+ * The rendered feature's properties, for hover content of a hit no plugin lists (e.g. a
28
+ * cluster bubble's count and aggregates). Unset for ordinary hits.
29
+ */
30
+ properties?: Readonly<Record<string, unknown>>;
31
+ }
32
+ /** The topmost feature of a blocking layer that was clicked. */
33
+ export interface BlockingClick {
34
+ properties: Record<string, unknown>;
35
+ geometry: Geometry;
36
+ }
37
+ export interface FeatureListItem {
38
+ refKey: string;
39
+ ref: FeatureRef;
40
+ name: string;
41
+ value: number | string | null;
42
+ level: Level | typeof NO_LEVEL;
43
+ units: string;
44
+ /**
45
+ * Decimal places to show a numeric `value` with (the built-in plugins take it from the
46
+ * metadata's display hint). Without it the value is formatted as given.
47
+ */
48
+ precision?: number;
49
+ lngLat: [number, number];
50
+ selectable: boolean;
51
+ /**
52
+ * A short text about a feature with no level (e.g. "Level 2 charger"), shown where its level
53
+ * would be: in the feature list, its row's accessible name, and the generic tooltip. Ignored
54
+ * when the item has a level.
55
+ */
56
+ detail?: string;
57
+ }
58
+ export type LegendData = {
59
+ pluginId: string;
60
+ title: string;
61
+ units: string;
62
+ scale: Scale;
63
+ /**
64
+ * `"gradient"` or `"blocks"` (labeled swatches with the levels' own labels). Absent: a
65
+ * gradient for range scales and blocks for category scales.
66
+ */
67
+ display?: "gradient" | "blocks";
68
+ } | {
69
+ pluginId: string;
70
+ status: DataStatus;
71
+ };
72
+ export interface ClickEvent {
73
+ hits: FeatureHit[];
74
+ additive: boolean;
75
+ lngLat: [number, number];
76
+ }
77
+ /**
78
+ * What a plugin's `setup` gets. Everything added through it is removed automatically when the
79
+ * plugin is torn down, whether or not the plugin's own teardown cleans up.
80
+ */
81
+ export interface PluginContext {
82
+ /**
83
+ * The GL map. Don't use it after teardown: do late async work (an image that finishes
84
+ * loading, a fetch) through the context's methods, which do nothing once torn down.
85
+ */
86
+ readonly gl: GlMapLike;
87
+ /** `${mapUid}:${pluginId}:${local}` */
88
+ id(local: string): string;
89
+ readonly metadata: MetadataProvider;
90
+ readonly messages: MessageProvider;
91
+ readonly format: FormatProvider;
92
+ /** Returns the namespaced id; removed on teardown. */
93
+ addSource(local: string, spec: SourceSpecification): string;
94
+ /** Returns the namespaced id; removed on teardown, before sources. */
95
+ addLayer(local: string, spec: LayerSpecWithoutId): string;
96
+ /**
97
+ * Adds a MapLibre custom layer (e.g. a wind layer from a library the host adds) in the
98
+ * plugin's draw-order slot, with the same lifecycle as `addLayer`. `create` gets the
99
+ * namespaced id, which the layer must use; the object it returns is added as is, never copied,
100
+ * and `create` runs again on each restyle, so build a fresh layer every time. Returns the id.
101
+ */
102
+ addCustomLayer(local: string, create: (id: string) => CustomLayerInterface): string;
103
+ /** Adds the image as `id(local)`; removed on teardown. */
104
+ addImage(local: string, image: HTMLImageElement | ImageData): void;
105
+ /** Registers layers for click, hover, tooltip, and `featuresAt`. */
106
+ interactive(layerIds: string[], toHit: (props: Record<string, unknown>) => FeatureHit | null): void;
107
+ /**
108
+ * Layers that hide what is beneath them (e.g. cluster bubbles): where one is topmost, a click
109
+ * picks nothing and calls `onClick` with that feature, and the hover is `toHover`'s hit for it
110
+ * (for its tooltip), or nothing. `toHover` must have no side effects (lookups call it too).
111
+ */
112
+ blocking(layerIds: string[], toHover?: (props: Record<string, unknown>) => FeatureHit | null, onClick?: (feature: BlockingClick) => void): void;
113
+ /** A layer event listener (e.g. a cluster click), removed on teardown. */
114
+ on(type: string, layerId: string, listener: (e: any) => void): void;
115
+ /** Reactive. */
116
+ readonly selectedKeys: ReadonlySet<string>;
117
+ /** An `$effect.root` torn down with the plugin. */
118
+ effect(fn: () => void | (() => void)): void;
119
+ }
120
+ /**
121
+ * A layer of data on a map. A plugin object keeps per-map state, so it belongs to one map: create
122
+ * one plugin per map (adding an object that is set up on another map throws).
123
+ */
124
+ export interface MapPlugin {
125
+ id: string;
126
+ /** Already-translated layer name, for the selection bar and view summary. */
127
+ label?: string;
128
+ /** The metadata dataset the plugin's data comes from (for attribution); may change with its source. */
129
+ readonly dataset?: string;
130
+ /** Adds sources, layers, and icons; returns its teardown. Runs only once a style is loaded. */
131
+ setup(ctx: PluginContext): () => void;
132
+ legend?(): LegendData | null;
133
+ features?(): FeatureListItem[];
134
+ selectable?: boolean;
135
+ /**
136
+ * Which plugin's feature the hover tooltip shows where several are under the pointer: the
137
+ * highest wins, the topmost on a tie. Default 0. Over a blocking layer (a cluster bubble), a
138
+ * feature beneath shows only if its plugin's priority is higher than the blocking plugin's.
139
+ * Clicks ignore this (topmost first), and a blocking layer on top still blocks them.
140
+ */
141
+ hoverPriority?: number;
142
+ displayOptions?: Snippet;
143
+ /** Richer tooltip content for one of this plugin's features, below its name. */
144
+ tooltip?(item: FeatureListItem): Snippet | undefined;
145
+ /**
146
+ * Set when this plugin's `tooltip` content shows the feature's name itself, so the generic
147
+ * tooltip leaves its name header out. Default false.
148
+ */
149
+ tooltipShowsName?: boolean;
150
+ /**
151
+ * Tooltip content for a hovered hit that isn't a listed feature (e.g. a cluster bubble's hover
152
+ * hit from `PluginContext.blocking`).
153
+ */
154
+ hoverTooltip?(hit: FeatureHit): Snippet | undefined;
155
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { PluginContext } from "./plugin.js";
2
+ /**
3
+ * A view of `ctx` whose local ids all start with `prefix`, so several plugins can be set up
4
+ * inside one (one id, one teardown) without their sources, layers, and images colliding.
5
+ * Everything else is the parent's, read live (`selectedKeys` stays reactive).
6
+ */
7
+ export declare function scopedContext(ctx: PluginContext, prefix: string): PluginContext;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * A view of `ctx` whose local ids all start with `prefix`, so several plugins can be set up
3
+ * inside one (one id, one teardown) without their sources, layers, and images colliding.
4
+ * Everything else is the parent's, read live (`selectedKeys` stays reactive).
5
+ */
6
+ export function scopedContext(ctx, prefix) {
7
+ const local = (name) => `${prefix}${name}`;
8
+ return {
9
+ get gl() {
10
+ return ctx.gl;
11
+ },
12
+ get metadata() {
13
+ return ctx.metadata;
14
+ },
15
+ get messages() {
16
+ return ctx.messages;
17
+ },
18
+ get format() {
19
+ return ctx.format;
20
+ },
21
+ get selectedKeys() {
22
+ return ctx.selectedKeys;
23
+ },
24
+ id: (name) => ctx.id(local(name)),
25
+ addSource: (name, spec) => ctx.addSource(local(name), spec),
26
+ addLayer: (name, spec) => ctx.addLayer(local(name), spec),
27
+ addCustomLayer: (name, create) => ctx.addCustomLayer(local(name), create),
28
+ addImage: (name, image) => ctx.addImage(local(name), image),
29
+ interactive: (layerIds, toHit) => ctx.interactive(layerIds, toHit),
30
+ blocking: (layerIds, toHover, onClick) => ctx.blocking(layerIds, toHover, onClick),
31
+ on: (type, layerId, listener) => ctx.on(type, layerId, listener),
32
+ effect: (fn) => ctx.effect(fn)
33
+ };
34
+ }
@@ -0,0 +1,10 @@
1
+ export interface FormatProvider {
2
+ number(value: number, options?: Intl.NumberFormatOptions): string;
3
+ /** Formats in Pacific time. */
4
+ dateTime(value: Date | string): string;
5
+ /** "PT" only when the device zone isn't Pacific. */
6
+ zoneLabel(): string | null;
7
+ /** How long ago (or until) `value` is from `now` (default: the current time), e.g. "5 minutes ago". */
8
+ relativeTime(value: Date | string, now?: Date): string;
9
+ }
10
+ export declare function createDefaultFormat(deviceZone?: string): FormatProvider;
@@ -0,0 +1,24 @@
1
+ const PACIFIC = "America/Los_Angeles";
2
+ export function createDefaultFormat(deviceZone = Intl.DateTimeFormat().resolvedOptions().timeZone) {
3
+ const dateFormat = new Intl.DateTimeFormat("en-US", {
4
+ timeZone: PACIFIC,
5
+ dateStyle: "medium",
6
+ timeStyle: "short"
7
+ });
8
+ const relative = new Intl.RelativeTimeFormat("en-US", { numeric: "always" });
9
+ return {
10
+ number: (value, options) => new Intl.NumberFormat("en-US", options).format(value),
11
+ dateTime: (value) => dateFormat.format(typeof value === "string" ? new Date(value) : value),
12
+ zoneLabel: () => (deviceZone === PACIFIC ? null : "PT"),
13
+ relativeTime: (value, now = new Date()) => {
14
+ const ms = (typeof value === "string" ? new Date(value) : value).getTime() - now.getTime();
15
+ const minutes = Math.round(ms / 60_000);
16
+ if (Math.abs(minutes) < 60)
17
+ return relative.format(minutes, "minute");
18
+ const hours = Math.round(ms / 3_600_000);
19
+ if (Math.abs(hours) < 24)
20
+ return relative.format(hours, "hour");
21
+ return relative.format(Math.round(ms / 86_400_000), "day");
22
+ }
23
+ };
24
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./types.js";
2
+ export * from "./scale-types.js";
3
+ export * from "./refs.js";
4
+ export * from "./scale.js";
5
+ export * from "./selection.js";
6
+ export * from "./static-source.js";
7
+ export * from "./messages.js";
8
+ export * from "./format.js";
9
+ export * from "./metadata.js";
@@ -0,0 +1,9 @@
1
+ export * from "./types.js";
2
+ export * from "./scale-types.js";
3
+ export * from "./refs.js";
4
+ export * from "./scale.js";
5
+ export * from "./selection.js";
6
+ export * from "./static-source.js";
7
+ export * from "./messages.js";
8
+ export * from "./format.js";
9
+ export * from "./metadata.js";
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "https://inlang.com/schema/inlang-message-format",
3
+ "mapsdk_list_button": "List",
4
+ "mapsdk_list_button_label": "List of features on the map",
5
+ "mapsdk_list_label": "Features on the map",
6
+ "mapsdk_list_sort_name": "Name",
7
+ "mapsdk_list_sort_value": "Value",
8
+ "mapsdk_list_row": "{name}, {value} {units}, {level}",
9
+ "mapsdk_list_row_no_units": "{name}, {value}, {level}",
10
+ "mapsdk_list_row_no_value": "{name}, no reading",
11
+ "mapsdk_list_row_detail": "{name}, {detail}",
12
+ "mapsdk_list_empty": "Nothing on the map in this view",
13
+ "mapsdk_selected_count_one": "{count} selected",
14
+ "mapsdk_selected_count_other": "{count} selected",
15
+ "mapsdk_selection_clear": "Clear",
16
+ "mapsdk_selection_clear_layer": "Clear selection: {layer}",
17
+ "mapsdk_selected_in_layer": "{layer}: {selected}",
18
+ "mapsdk_selection_cleared": "Selection cleared",
19
+ "mapsdk_summary_one": "{count} {layer}",
20
+ "mapsdk_summary_other": "{count} {layer}",
21
+ "mapsdk_summary_level": "{count} {level}",
22
+ "mapsdk_summary_with_levels": "{layer}: {levels}",
23
+ "mapsdk_no_level": "No level",
24
+ "mapsdk_zoom_in": "Zoom in",
25
+ "mapsdk_zoom_out": "Zoom out",
26
+ "mapsdk_map_label": "Map. Use arrow keys to pan, plus and minus to zoom.",
27
+ "mapsdk_unavailable": "{layer} isn't available right now",
28
+ "mapsdk_loading": "Loading",
29
+ "mapsdk_error_retry": "Try again",
30
+ "mapsdk_legend_label": "Legend",
31
+ "mapsdk_legend_threshold": "from {value}",
32
+ "mapsdk_controls_label": "Map controls",
33
+ "mapsdk_selection_label": "Selection",
34
+ "mapsdk_no_reading": "No reading",
35
+ "mapsdk_time_with_zone": "{time} {zone}",
36
+ "mapsdk_monitor_cluster_average": "(cluster avg)",
37
+ "mapsdk_monitor_cluster_title": "{type} Monitors",
38
+ "mapsdk_monitor_cluster_title_sjvair": "SJVAir {type} Monitors",
39
+ "mapsdk_monitor_cluster_count_one": "{count} monitor",
40
+ "mapsdk_monitor_cluster_count_other": "{count} monitors",
41
+ "mapsdk_monitor_cluster_max": "Max: {value} {units}",
42
+ "mapsdk_monitor_cluster_min": "Min: {value} {units}",
43
+ "mapsdk_monitor_interval_avg": "({interval} avg)",
44
+ "mapsdk_monitor_updated": "Last updated {when}",
45
+ "mapsdk_hms_smoke_name": "Smoke",
46
+ "mapsdk_hms_fire_detection": "Fire detection",
47
+ "mapsdk_hms_fire_detections": "{count} fire detections",
48
+ "mapsdk_collocation_reference": "Reference monitor",
49
+ "mapsdk_collocation_colocated": "Collocated monitor",
50
+ "mapsdk_collocation_interval_avg": "({interval} avg)",
51
+ "mapsdk_collocation_updated": "Last updated {when}"
52
+ }
@@ -0,0 +1,10 @@
1
+ import en from "./messages/en.json";
2
+ type Catalog = Omit<typeof en, "$schema">;
3
+ type Literal = keyof Catalog;
4
+ type PluralBase<K> = K extends `${infer B}_one` ? B : never;
5
+ /** Every catalog key, plus the base key of each `_one`/`_other` plural message. */
6
+ export type MessageKey = Literal | PluralBase<Literal>;
7
+ export type MessageParams = Record<string, string | number>;
8
+ export type MessageProvider = (key: MessageKey, params?: MessageParams) => string;
9
+ export declare const defaultMessages: MessageProvider;
10
+ export {};
@@ -0,0 +1,17 @@
1
+ import en from "./messages/en.json";
2
+ const catalog = en;
3
+ const plurals = new Intl.PluralRules("en-US");
4
+ function interpolate(template, params) {
5
+ if (!params)
6
+ return template;
7
+ return template.replace(/\{(\w+)\}/g, (whole, name) => Object.hasOwn(params, name) ? String(params[name]) : whole);
8
+ }
9
+ export const defaultMessages = (key, params) => {
10
+ let resolved = key;
11
+ if (!Object.hasOwn(catalog, key) && typeof params?.count === "number") {
12
+ const variant = `${key}_${plurals.select(params.count) === "one" ? "one" : "other"}`;
13
+ if (Object.hasOwn(catalog, variant))
14
+ resolved = variant;
15
+ }
16
+ return interpolate(catalog[resolved] ?? key, params);
17
+ };
@@ -0,0 +1,22 @@
1
+ import type { Scale, ScaleRef } from "./scale-types.js";
2
+ export interface EntryTypeInfo {
3
+ label: string;
4
+ units: string;
5
+ /**
6
+ * Decimal places to show values with (the catalog's display hint), or null when the metadata
7
+ * gives none.
8
+ */
9
+ precision: number | null;
10
+ }
11
+ export interface MetadataProvider {
12
+ scale(ref: ScaleRef, signal?: AbortSignal): Promise<Scale | "unavailable">;
13
+ entryType(ref: ScaleRef, signal?: AbortSignal): Promise<EntryTypeInfo | "unavailable">;
14
+ monitorTypeLabel(type: string, signal?: AbortSignal): Promise<string | "unavailable">;
15
+ /**
16
+ * How often a monitor type reports (e.g. "2 min"), for the tooltip's "(N avg)" note. Optional:
17
+ * the note is left out when a host's provider lacks it.
18
+ */
19
+ monitorTypeInterval?(type: string, signal?: AbortSignal): Promise<string | "unavailable">;
20
+ attribution(dataset: string, signal?: AbortSignal): Promise<string | "unavailable">;
21
+ }
22
+ export declare function createDefaultMetadata(): MetadataProvider;
@@ -0,0 +1,110 @@
1
+ import { getCalHeatScoreMeta, getDatasetCatalog, getForecastsMeta, getHMSMeta, getMonitorsMeta } from "@sjvair/sdk";
2
+ const AIR_QUALITY = "air-quality";
3
+ /** Datasets whose meta carries `scales` keyed by entry-type id. */
4
+ const SCALE_DATASETS = ["hms-smoke", "hms-fire", "calheatscore", "forecasts"];
5
+ function hasScales(dataset) {
6
+ return SCALE_DATASETS.includes(dataset);
7
+ }
8
+ function abortError() {
9
+ return new DOMException("The operation was aborted.", "AbortError");
10
+ }
11
+ /** Resolve with `promise`, or reject early if `signal` aborts (without touching `promise`). */
12
+ function untilAborted(promise, signal) {
13
+ if (!signal)
14
+ return promise;
15
+ if (signal.aborted)
16
+ return Promise.reject(abortError());
17
+ return new Promise((resolve, reject) => {
18
+ const onAbort = () => reject(abortError());
19
+ signal.addEventListener("abort", onAbort, { once: true });
20
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
21
+ });
22
+ }
23
+ /**
24
+ * Fetch once, share the result, and forget failures so the next call retries. The shared fetch
25
+ * carries no caller signal, so one caller's abort cannot reject it for the others.
26
+ */
27
+ function memoize(fetch) {
28
+ let cached;
29
+ return (signal) => {
30
+ if (!cached) {
31
+ const p = fetch();
32
+ cached = p;
33
+ p.catch(() => {
34
+ if (cached === p)
35
+ cached = undefined;
36
+ });
37
+ }
38
+ return untilAborted(cached, signal);
39
+ };
40
+ }
41
+ export function createDefaultMetadata() {
42
+ const monitorsMeta = memoize(() => getMonitorsMeta());
43
+ const catalog = memoize(() => getDatasetCatalog());
44
+ // The catalog splits HMS into two datasets; one meta carries both scales.
45
+ const hmsMeta = memoize(() => getHMSMeta());
46
+ const datasetScales = {
47
+ "hms-smoke": hmsMeta,
48
+ "hms-fire": hmsMeta,
49
+ calheatscore: memoize(() => getCalHeatScoreMeta()),
50
+ forecasts: memoize(() => getForecastsMeta())
51
+ };
52
+ /** The value, `"unavailable"` when `read` finds nothing or the fetch fails; only the caller's abort rejects. */
53
+ async function guarded(signal, read) {
54
+ try {
55
+ return (await read()) ?? "unavailable";
56
+ }
57
+ catch (error) {
58
+ if (signal?.aborted)
59
+ throw error;
60
+ return "unavailable";
61
+ }
62
+ }
63
+ /** The catalog's display precision for an entry type, or null. Only the caller's abort rejects. */
64
+ async function precisionOf(ref, signal) {
65
+ try {
66
+ const { datasets } = await catalog(signal);
67
+ const entry = datasets
68
+ .find((d) => d.id === ref.dataset)
69
+ ?.entry_types.find((e) => e.id === ref.entryType);
70
+ const precision = entry?.display?.precision;
71
+ return typeof precision === "number" ? precision : null;
72
+ }
73
+ catch (error) {
74
+ if (signal?.aborted)
75
+ throw error;
76
+ return null;
77
+ }
78
+ }
79
+ async function entryMeta(ref, signal) {
80
+ if (ref.dataset !== AIR_QUALITY)
81
+ return undefined;
82
+ return (await monitorsMeta(signal)).entries[ref.entryType];
83
+ }
84
+ return {
85
+ scale: (ref, signal) => guarded(signal, async () => {
86
+ if (hasScales(ref.dataset)) {
87
+ const { scales } = await datasetScales[ref.dataset](signal);
88
+ return Object.hasOwn(scales, ref.entryType) ? scales[ref.entryType] : undefined;
89
+ }
90
+ const entry = await entryMeta(ref, signal);
91
+ return entry?.scale ?? undefined;
92
+ }),
93
+ entryType: (ref, signal) => guarded(signal, async () => {
94
+ let info;
95
+ if (hasScales(ref.dataset)) {
96
+ const { scales } = await datasetScales[ref.dataset](signal);
97
+ const scale = Object.hasOwn(scales, ref.entryType) ? scales[ref.entryType] : undefined;
98
+ info = scale && { label: scale.label, units: scale.units };
99
+ }
100
+ else {
101
+ const entry = await entryMeta(ref, signal);
102
+ info = entry && { label: entry.label, units: entry.units ?? "" };
103
+ }
104
+ return info && { ...info, precision: await precisionOf(ref, signal) };
105
+ }),
106
+ monitorTypeLabel: (type, signal) => guarded(signal, async () => (await monitorsMeta(signal)).monitors[type]?.label),
107
+ monitorTypeInterval: (type, signal) => guarded(signal, async () => (await monitorsMeta(signal)).monitors[type]?.expected_interval),
108
+ attribution: (dataset, signal) => guarded(signal, async () => (await catalog(signal)).datasets.find((d) => d.id === dataset)?.source || undefined)
109
+ };
110
+ }
@@ -0,0 +1,18 @@
1
+ import type { FeatureRef } from "./types.js";
2
+ /** A place as `@sjvair/sdk` and the dashboard name it: a monitor, or a region by type and code. */
3
+ export type PlaceRef = {
4
+ kind: "monitor";
5
+ id: string;
6
+ } | {
7
+ kind: "region";
8
+ type: string;
9
+ code: string;
10
+ };
11
+ export declare function encodeRef(ref: FeatureRef): string;
12
+ export declare function decodeRef(key: string): FeatureRef | null;
13
+ export declare function selectionId(ref: FeatureRef): string | null;
14
+ export declare function toPlaceRef(ref: FeatureRef): PlaceRef | null;
15
+ export declare function fromPlaceRef(place: {
16
+ kind: string;
17
+ [k: string]: unknown;
18
+ }): FeatureRef | null;
@@ -0,0 +1,61 @@
1
+ export function encodeRef(ref) {
2
+ switch (ref.kind) {
3
+ case "monitor":
4
+ return `monitor:${ref.id}`;
5
+ case "region":
6
+ return `region:${ref.type}:${ref.code}`;
7
+ case "feature":
8
+ return `feature:${ref.id}`;
9
+ }
10
+ }
11
+ export function decodeRef(key) {
12
+ const first = key.indexOf(":");
13
+ if (first < 0)
14
+ return null;
15
+ const kind = key.slice(0, first);
16
+ const rest = key.slice(first + 1);
17
+ if (kind === "monitor" || kind === "feature") {
18
+ return rest ? { kind, id: rest } : null;
19
+ }
20
+ if (kind === "region") {
21
+ const second = rest.indexOf(":");
22
+ if (second <= 0)
23
+ return null;
24
+ const code = rest.slice(second + 1);
25
+ return code ? { kind, type: rest.slice(0, second), code } : null;
26
+ }
27
+ return null;
28
+ }
29
+ export function selectionId(ref) {
30
+ switch (ref.kind) {
31
+ case "monitor":
32
+ return ref.id;
33
+ case "region":
34
+ return `${ref.type}:${ref.code}`;
35
+ case "feature":
36
+ return null;
37
+ }
38
+ }
39
+ export function toPlaceRef(ref) {
40
+ switch (ref.kind) {
41
+ case "monitor":
42
+ return { kind: "monitor", id: ref.id };
43
+ case "region":
44
+ return { kind: "region", type: ref.type, code: ref.code };
45
+ case "feature":
46
+ return null;
47
+ }
48
+ }
49
+ export function fromPlaceRef(place) {
50
+ if (place.kind === "monitor" && typeof place.id === "string" && place.id) {
51
+ return { kind: "monitor", id: place.id };
52
+ }
53
+ if (place.kind === "region" &&
54
+ typeof place.type === "string" &&
55
+ typeof place.code === "string" &&
56
+ place.type &&
57
+ place.code) {
58
+ return { kind: "region", type: place.type, code: place.code };
59
+ }
60
+ return null;
61
+ }
@@ -0,0 +1,7 @@
1
+ import type { Scale, ScaleLevel, ScaleSource } from "@sjvair/sdk/meta/types";
2
+ export type { Scale, ScaleSource };
3
+ export type Level = ScaleLevel;
4
+ export interface ScaleRef {
5
+ dataset: string;
6
+ entryType: string;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { Level, Scale } from "./scale-types.js";
2
+ /** Returned by `levelFor` when no level covers the value. */
3
+ export declare const NO_LEVEL: unique symbol;
4
+ /**
5
+ * A reading as a number: a finite number, or a non-blank string that reads as one; else null.
6
+ * The one rule for what counts as a reading (levels, averages, tooltips).
7
+ */
8
+ export declare function numericValue(value: number | string | null | undefined): number | null;
9
+ /**
10
+ * Find the level a value falls in. Range levels are half-open, [min, max), and a null max
11
+ * is unbounded. Numeric strings are coerced for range scales only.
12
+ */
13
+ export declare function levelFor(value: number | string | null | undefined, scale: Scale): Level | typeof NO_LEVEL;
@@ -0,0 +1,27 @@
1
+ /** Returned by `levelFor` when no level covers the value. */
2
+ export const NO_LEVEL = Symbol("no-level");
3
+ /**
4
+ * A reading as a number: a finite number, or a non-blank string that reads as one; else null.
5
+ * The one rule for what counts as a reading (levels, averages, tooltips).
6
+ */
7
+ export function numericValue(value) {
8
+ if (typeof value !== "number" && (typeof value !== "string" || value.trim() === ""))
9
+ return null;
10
+ const n = typeof value === "number" ? value : Number(value);
11
+ return Number.isFinite(n) ? n : null;
12
+ }
13
+ /**
14
+ * Find the level a value falls in. Range levels are half-open, [min, max), and a null max
15
+ * is unbounded. Numeric strings are coerced for range scales only.
16
+ */
17
+ export function levelFor(value, scale) {
18
+ if (value === null || value === undefined)
19
+ return NO_LEVEL;
20
+ if (scale.kind === "category") {
21
+ return scale.levels.find((level) => level.value === value) ?? NO_LEVEL;
22
+ }
23
+ const n = numericValue(value);
24
+ if (n === null)
25
+ return NO_LEVEL;
26
+ return (scale.levels.find((level) => level.min !== null && n >= level.min && (level.max == null || n < level.max)) ?? NO_LEVEL);
27
+ }
@@ -0,0 +1,7 @@
1
+ import type { FeatureRef, SelectionMode, SpatialSelection } from "./types.js";
2
+ export interface PickGesture {
3
+ additive: boolean;
4
+ }
5
+ export declare function selectionKind(ref: FeatureRef): "monitors" | "regions" | null;
6
+ export declare function applyPick(current: SpatialSelection | null, ref: FeatureRef, mode: SelectionMode, gesture: PickGesture): SpatialSelection | null;
7
+ export declare function selectionKeys(sel: SpatialSelection | null): Set<string>;
@@ -0,0 +1,31 @@
1
+ import { selectionId } from "./refs.js";
2
+ export function selectionKind(ref) {
3
+ switch (ref.kind) {
4
+ case "monitor":
5
+ return "monitors";
6
+ case "region":
7
+ return "regions";
8
+ case "feature":
9
+ return null;
10
+ }
11
+ }
12
+ export function applyPick(current, ref, mode, gesture) {
13
+ const kind = selectionKind(ref);
14
+ const id = selectionId(ref);
15
+ if (mode === "none" || kind === null || id === null)
16
+ return current;
17
+ if (mode === "single" || !gesture.additive || !current || current.kind !== kind) {
18
+ return { kind, ids: [id] };
19
+ }
20
+ if (current.ids.includes(id)) {
21
+ const ids = current.ids.filter((x) => x !== id);
22
+ return ids.length ? { kind, ids } : null;
23
+ }
24
+ return { kind, ids: [...current.ids, id] };
25
+ }
26
+ export function selectionKeys(sel) {
27
+ if (!sel)
28
+ return new Set();
29
+ const prefix = sel.kind === "monitors" ? "monitor:" : "region:";
30
+ return new Set(sel.ids.map((id) => prefix + id));
31
+ }
@@ -0,0 +1,4 @@
1
+ import type { FeatureCollection, Geometry } from "geojson";
2
+ import type { Scale, ScaleRef } from "./scale-types.js";
3
+ import type { DataSource, MapFeatureProperties } from "./types.js";
4
+ export declare function staticSource<P extends MapFeatureProperties>(features: FeatureCollection<Geometry, P>, scale: ScaleRef | Scale | null): DataSource<P>;