@sjvair/map-sdk 0.1.0-beta.5 → 0.1.0-beta.7

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.
package/README.md CHANGED
@@ -135,7 +135,13 @@ default MapTiler Streets v4 variants (build on `defaultBasemaps()`, which `creat
135
135
  sjvair.com setup is `{ ...defaultBasemaps(), excludedLayers: [] }`); `config.initialView` and
136
136
  `config.cooperativeGestures` set the camera and gesture handling. `config.projection` (`"mercator"`
137
137
  by default, or `"globe"`) and `config.space` (`false` by default, or `{ preset: "milkyway" }` for a
138
- space background behind the globe) are passed to MapTiler only when set.
138
+ space background behind the globe) are passed to MapTiler only when set. `config.telemetry: false`
139
+ turns off MapTiler's telemetry (a usage report of the SDK version, API key, and session id, sent
140
+ about 2 s after each map is created) before the map is created. It is a page-wide setting in
141
+ MapTiler's SDK: once one map turns it off, it stays off for every map on the page. MapTiler's
142
+ session id (`mtsid`) stays on either way, since MapTiler bills SDK maps by session (without one,
143
+ every tile request counts on its own) and its tile cache needs it. By default (`true`) MapTiler's
144
+ own default applies.
139
145
 
140
146
  Without `createMonitorsStore`, build a `DataSource` yourself (below).
141
147
 
@@ -207,7 +213,9 @@ the legend section's title: `layerLabel` (default) or the entry type's label fro
207
213
  provider (`monitorsPlugin` uses the latter, so its section reads "PM2.5"; `layerLabel` still names
208
214
  the layer in the list, selection bar and summary). `Legend` takes `order` (`"bottom-first"`,
209
215
  default, or `"top-first"`) and `framed` (`true`, default, draws its own card; `false` draws no
210
- border, background, padding or fixed width, so a host's own card frames it). Without a frame,
216
+ border, background, padding or fixed width, so a host's own card frames it). The framed legend
217
+ and the feature list panel are 21rem wide at most, and never wider than the map less 2rem (the
218
+ window less 2rem outside a `MapView`), so they fit a small map on a wide screen. Without a frame,
211
219
  translucent swatches blend over the nearest ancestor with an opaque background (a see-through
212
220
  card is skipped), else `--background`; that color is read on mount, when `framed` changes, and on
213
221
  a theme change, but not when the card changes color through its own class or style.
@@ -254,7 +262,9 @@ source loads, fails, or is unavailable, the legend says so (a failure offers "Tr
254
262
  calls `status.retry()`). Values are shown with the metadata's display precision, except in the
255
263
  monitor, cluster, and collocation tooltips, which match monitor-map: the reading and a cluster's
256
264
  average are whole numbers, and a cluster's max and min are as reported. Fire values are cut off
257
- rather than rounded (see `truncateValues`). Times in the tooltips read like sjvair.com's map, in
265
+ rather than rounded (see `truncateValues`). On a category scale, a value that is its level's code
266
+ (smoke's `"light"`, the heat score's `2`) isn't shown: the list row, the tooltip, and the row's
267
+ accessible name give only the level label. Times in the tooltips read like sjvair.com's map, in
258
268
  Pacific time: "4:00 pm Friday Nov 06, 2026", with " PT" after them on a device in another zone.
259
269
  The monitor cluster tooltip's count line gives the area the monitors are within, "3 monitors
260
270
  within a 22.619 mi² area": the area of their convex hull, left out when it would read 0.000 (two
@@ -548,6 +558,29 @@ local sjvair.com stack at `http://localhost:8000`.
548
558
 
549
559
  ## Changes
550
560
 
561
+ ### 0.1.0-beta.7
562
+
563
+ - **Plugins come back at once after a basemap switch.** After picking another basemap, monitors,
564
+ fire, smoke and the other layers were missing until every tile of the new basemap had loaded
565
+ and drawn (4–10 s on a slow connection). They are now added as soon as the new style loads, as
566
+ sjvair.com's map does. A theme change while tiles are loading no longer waits either.
567
+ - **`config.telemetry: false`** turns off MapTiler's telemetry (its usage report) for the page,
568
+ before the map is created. Sessions stay on, so billing by session and the tile cache are
569
+ unchanged. By default nothing changes.
570
+ - **The feature list panel and the framed legend fit the map.** They were capped at the window's
571
+ width less 2rem; inside a `MapView` they are now capped at the map's width less 2rem, so a small
572
+ map on a wide screen doesn't overflow. MapView sets `--mapsdk-map-width` for this.
573
+
574
+ ### 0.1.0-beta.6
575
+
576
+ - **A category reading is shown once.** When a feature's value is its category level's code, the
577
+ list row, the tooltip, and the row's accessible name show only the level label. Smoke rows read
578
+ "Smoke, Light" (was "light Light"), burn status "Burning Discouraged", and heat score
579
+ "Moderate" (the 0–4 number is no longer shown). Number scales such as PM2.5 and fire power are
580
+ unchanged, and a category value that matches no level is still shown.
581
+ - **New message key for hosts with their own catalog:** `mapsdk_list_row_level`
582
+ (`"{name}, {level}"`).
583
+
551
584
  ### 0.1.0-beta.5
552
585
 
553
586
  - **Shapes that cover the view are in view.** The feature list and the view summary count a shape
@@ -32,6 +32,14 @@ export interface MapConfig {
32
32
  space?: false | {
33
33
  preset: string;
34
34
  };
35
+ /**
36
+ * Default `true`: MapTiler's own default. `false` turns off MapTiler's telemetry (a usage
37
+ * report of the SDK version, the API key, and the session id, sent about 2 s after each map is
38
+ * created) before the GL map is created. It is a page-wide setting in MapTiler's SDK, so once
39
+ * one map turns it off it stays off for every map on the page. MapTiler's session id (`mtsid`)
40
+ * stays on: MapTiler bills SDK maps by session, and its tile cache needs sessions.
41
+ */
42
+ telemetry?: boolean;
35
43
  }
36
44
  export interface CreateMapOptions {
37
45
  config: MapConfig;
@@ -381,7 +381,9 @@ class MapContextImpl {
381
381
  this.#host.attach(gl);
382
382
  this.#router.attach(gl);
383
383
  gl.on("moveend", this.#onMoveEnd);
384
- this.basemap.attach(gl, () => this.#host.restyle(), () => {
384
+ this.basemap.attach(gl,
385
+ // Called from style.load: the style is loaded, though its tiles may not be.
386
+ () => this.#host.restyle(true), () => {
385
387
  this.#ready = false;
386
388
  this.#readyRequested = false;
387
389
  this.#host.suspend();
@@ -57,11 +57,15 @@ export declare class PluginHost {
57
57
  */
58
58
  suspend(): void;
59
59
  /**
60
- * Tears down and sets up every plugin again (after a basemap change), once the style loads.
61
- * On a loaded style the setup is synchronous, so only map events and theme watching call this,
62
- * never code inside an effect: roots created there can stop (see the class comment).
60
+ * Tears down and sets up every plugin again (after a basemap or theme change), once the style
61
+ * loads. Pass `styleLoaded` from a `style.load` listener: `isStyleLoaded()` stays false there
62
+ * until every tile has loaded, and `style.load` has already fired, so waiting would last until
63
+ * `idle`. A live host's style is loaded too (`suspend` comes before every `setStyle`), so a theme
64
+ * change sets up at once as well. On a loaded style the setup is synchronous, so only map events
65
+ * and theme watching call this, never code inside an effect: roots created there can stop (see
66
+ * the class comment).
63
67
  */
64
- restyle(): void;
68
+ restyle(styleLoaded?: boolean): void;
65
69
  /** Tears down every plugin. The gl is not touched after this returns. */
66
70
  detach(): void;
67
71
  }
@@ -161,15 +161,26 @@ export class PluginHost {
161
161
  this.#teardownAll();
162
162
  }
163
163
  /**
164
- * Tears down and sets up every plugin again (after a basemap change), once the style loads.
165
- * On a loaded style the setup is synchronous, so only map events and theme watching call this,
166
- * never code inside an effect: roots created there can stop (see the class comment).
164
+ * Tears down and sets up every plugin again (after a basemap or theme change), once the style
165
+ * loads. Pass `styleLoaded` from a `style.load` listener: `isStyleLoaded()` stays false there
166
+ * until every tile has loaded, and `style.load` has already fired, so waiting would last until
167
+ * `idle`. A live host's style is loaded too (`suspend` comes before every `setStyle`), so a theme
168
+ * change sets up at once as well. On a loaded style the setup is synchronous, so only map events
169
+ * and theme watching call this, never code inside an effect: roots created there can stop (see
170
+ * the class comment).
167
171
  */
168
- restyle() {
172
+ restyle(styleLoaded = false) {
169
173
  if (!this.#gl)
170
174
  return;
175
+ const loaded = styleLoaded || this.#live;
171
176
  this.#teardownAll();
172
- this.#whenStyleLoaded(true);
177
+ if (loaded) {
178
+ this.#cancelStyleWait();
179
+ this.#setupAll();
180
+ }
181
+ else {
182
+ this.#whenStyleLoaded(true);
183
+ }
173
184
  }
174
185
  /** Tears down every plugin. The gl is not touched after this returns. */
175
186
  detach() {
@@ -8,6 +8,7 @@
8
8
  "mapsdk_list_row": "{name}, {value} {units}, {level}",
9
9
  "mapsdk_list_row_no_units": "{name}, {value}, {level}",
10
10
  "mapsdk_list_row_no_value": "{name}, no reading",
11
+ "mapsdk_list_row_level": "{name}, {level}",
11
12
  "mapsdk_list_row_detail": "{name}, {detail}",
12
13
  "mapsdk_list_empty": "Nothing on the map in this view",
13
14
  "mapsdk_selected_count_one": "{count} selected",
@@ -11,11 +11,11 @@
11
11
  rowLabel,
12
12
  levelText,
13
13
  shownLevel,
14
+ shownValue,
14
15
  sortItems,
15
16
  typeaheadIndex,
16
17
  type SortBy
17
18
  } from "./feature-list.js";
18
- import { formatValue as formatNumber } from "./value-format.js";
19
19
 
20
20
  interface Props {
21
21
  map: MapContext;
@@ -267,13 +267,6 @@
267
267
  pick(rows[index], e.shiftKey || e.metaKey || e.ctrlKey);
268
268
  }
269
269
 
270
- function formatValue(row: Row): string {
271
- if (row.value === null) return "";
272
- return typeof row.value === "number"
273
- ? formatNumber(row.value, row.precision, map.format, row.truncate)
274
- : row.value;
275
- }
276
-
277
270
  onDestroy(() => {
278
271
  clearTimeout(typeaheadTimer);
279
272
  clearTimeout(menuSentTimer);
@@ -332,6 +325,7 @@
332
325
  {@const index = range.start + i}
333
326
  {@const level = shownLevel(row)}
334
327
  {@const levelPlace = levelText(row, map.messages)}
328
+ {@const value = shownValue(row, map.format)}
335
329
  {@const isOn = isSelected(row)}
336
330
  <!-- Options aren't focused: the listbox holds focus and handles keys (aria-activedescendant). -->
337
331
  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_interactive_supports_focus -->
@@ -359,7 +353,7 @@
359
353
  </span>
360
354
  <span class="mapsdk-list-name">{row.name}</span>
361
355
  <span class="mapsdk-list-value">
362
- {formatValue(row)}{#if row.units && row.value !== null}&nbsp;{row.units}{/if}
356
+ {value ?? ""}{#if row.units && value !== null}&nbsp;{row.units}{/if}
363
357
  </span>
364
358
  <span class="mapsdk-list-level text-xs">
365
359
  {#if levelPlace.kind !== "detail"}
@@ -387,7 +381,8 @@
387
381
  flex-direction: column;
388
382
  position: relative;
389
383
  width: 21rem;
390
- max-width: calc(100vw - 2rem);
384
+ /* The map's width inside a MapView (set there), else the window's. */
385
+ max-width: calc(var(--mapsdk-map-width, 100vw) - 2rem);
391
386
  max-height: 100%;
392
387
  box-sizing: border-box;
393
388
  border: 1px solid var(--card-border, var(--border));
@@ -143,7 +143,8 @@
143
143
  flex-direction: column;
144
144
  gap: 0.75rem;
145
145
  width: 21rem;
146
- max-width: calc(100vw - 2rem);
146
+ /* The map's width inside a MapView (set there), else the window's. */
147
+ max-width: calc(var(--mapsdk-map-width, 100vw) - 2rem);
147
148
  box-sizing: border-box;
148
149
  padding: 0.75rem 1rem;
149
150
  border: 1px solid var(--card-border, var(--border));
@@ -3,9 +3,8 @@
3
3
  import type { MessageProvider } from "../data/messages.js";
4
4
  import type { FeatureListItem } from "../core/plugin.js";
5
5
  import { NO_LEVEL } from "../data/scale.js";
6
- import { levelText } from "./feature-list.js";
6
+ import { levelText, shownValue } from "./feature-list.js";
7
7
  import { levelChip } from "./level-chip.js";
8
- import { formatValue } from "./value-format.js";
9
8
 
10
9
  // The generic tooltip body under a feature's name: its value and a chip with its level. Shared
11
10
  // by MapTooltip and by plugins that show it for features they don't list.
@@ -26,13 +25,8 @@
26
25
  const chip = levelChip(level?.color);
27
26
  return chip ? `background:${chip.background};color:${chip.text}` : "";
28
27
  });
29
- const valueText = $derived(
30
- item.value == null
31
- ? null
32
- : typeof item.value === "number"
33
- ? formatValue(item.value, item.precision, format, item.truncate)
34
- : item.value
35
- );
28
+ // A category reading shows only its level chip: its value is the level's code.
29
+ const valueText = $derived(shownValue(item, format));
36
30
  </script>
37
31
 
38
32
  <div class="mapsdk-tooltip-item">
@@ -26,6 +26,11 @@ export declare function inView(items: FeatureListItem[], bounds: Bounds): Featur
26
26
  * label on a neutral swatch.
27
27
  */
28
28
  export declare function shownLevel(item: FeatureListItem): Level | null;
29
+ /**
30
+ * The value as shown, or `null` for none. A category reading, whose value is its level's code
31
+ * (smoke's "light", the heat score's 2), shows no value: its level label already says it.
32
+ */
33
+ export declare function shownValue(item: FeatureListItem, format: FormatProvider): string | null;
29
34
  /** A sorted copy. `value`: highest first, null values and no level last; ties and `name` by name. */
30
35
  export declare function sortItems(items: FeatureListItem[], by: SortBy): FeatureListItem[];
31
36
  /** The first index at or after `from` (wrapping) whose name starts with `query`, any case; -1 if none. */
@@ -29,6 +29,20 @@ export function inView(items, bounds) {
29
29
  export function shownLevel(item) {
30
30
  return item.level !== NO_LEVEL ? item.level : null;
31
31
  }
32
+ /**
33
+ * The value as shown, or `null` for none. A category reading, whose value is its level's code
34
+ * (smoke's "light", the heat score's 2), shows no value: its level label already says it.
35
+ */
36
+ export function shownValue(item, format) {
37
+ if (item.value === null)
38
+ return null;
39
+ const level = shownLevel(item);
40
+ if (level && level.value !== null && level.value === item.value)
41
+ return null;
42
+ return typeof item.value === "number"
43
+ ? formatValue(item.value, item.precision, format, item.truncate)
44
+ : item.value;
45
+ }
32
46
  const collator = new Intl.Collator("en-US");
33
47
  /** A value's sort weight; `null` sorts last. Category values sort by their level's tier. */
34
48
  function weight(item) {
@@ -82,16 +96,18 @@ export function levelText(item, messages) {
82
96
  /** A row's accessible name: name, value, units, and level label (or detail). */
83
97
  export function rowLabel(item, messages, format) {
84
98
  const level = levelText(item, messages);
85
- if (item.value === null) {
99
+ const value = shownValue(item, format);
100
+ if (value === null) {
101
+ // A category reading: its level is its value.
102
+ if (item.value !== null && level.kind === "level")
103
+ return messages("mapsdk_list_row_level", { name: item.name, level: level.text });
86
104
  return level.kind === "detail"
87
105
  ? messages("mapsdk_list_row_detail", { name: item.name, detail: level.text })
88
106
  : messages("mapsdk_list_row_no_value", { name: item.name });
89
107
  }
90
108
  const params = {
91
109
  name: item.name,
92
- value: typeof item.value === "number"
93
- ? formatValue(item.value, item.precision, format, item.truncate)
94
- : item.value,
110
+ value,
95
111
  units: item.units,
96
112
  level: level.text
97
113
  };
@@ -250,6 +250,7 @@
250
250
  role="region"
251
251
  aria-label={label ?? map.messages("mapsdk_map_label")}
252
252
  data-mapsdk-map={map.uid}
253
+ style:--mapsdk-map-width={width > 0 ? `${width}px` : undefined}
253
254
  >
254
255
  <div class="mapsdk-gl" bind:this={glContainer}></div>
255
256
  <MapTooltip {map} {project} {width} {height} mode={tooltip} gl={projector} />
@@ -128,6 +128,10 @@ function recordView(gl, ctx) {
128
128
  */
129
129
  export function createGlMap(container, ctx, engineControls = false) {
130
130
  const { camera, style } = untrack(() => ({ camera: ctx.camera, style: styleOf(ctx) }));
131
+ // Before the map exists, so its telemetry timer finds it off. Sessions stay on: MapTiler
132
+ // bills SDK maps by session, and without one every tile request counts on its own.
133
+ if (ctx.config.telemetry === false)
134
+ maptilerConfig.telemetry = false;
131
135
  const gl = new GlMap({
132
136
  container,
133
137
  style,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sjvair/map-sdk",
3
3
  "description": "Svelte 5 map components for SJVAir air-quality data",
4
- "version": "0.1.0-beta.5",
4
+ "version": "0.1.0-beta.7",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",