isard 1.10.0 → 1.11.0

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 (46) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/dist/basemap/vectortiles.d.ts +13 -8
  4. package/dist/basemap/vectortiles.js.map +1 -1
  5. package/dist/bind/layers.js +7 -2
  6. package/dist/bind/layers.js.map +1 -1
  7. package/dist/decoder.worker.js +3 -3
  8. package/dist/decoder.worker.js.map +3 -3
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +1 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/isard_bg.wasm +0 -0
  13. package/dist/isard_core_bg.wasm +0 -0
  14. package/dist/isard_tess_bg.wasm +0 -0
  15. package/dist/layers/features.d.ts +7 -0
  16. package/dist/layers/features.js +21 -5
  17. package/dist/layers/features.js.map +1 -1
  18. package/dist/layers/layers.js +2 -0
  19. package/dist/layers/layers.js.map +1 -1
  20. package/dist/layers/tiles3d.d.ts +1 -0
  21. package/dist/layers/tiles3d.js +42 -2
  22. package/dist/layers/tiles3d.js.map +1 -1
  23. package/dist/map/events.js +1 -0
  24. package/dist/map/events.js.map +1 -1
  25. package/dist/map/quality.d.ts +33 -0
  26. package/dist/map/quality.js +61 -0
  27. package/dist/map/quality.js.map +1 -0
  28. package/dist/map.d.ts +12 -1
  29. package/dist/map.js +64 -6
  30. package/dist/map.js.map +1 -1
  31. package/dist/react/IsardMap.d.ts +1 -0
  32. package/dist/react/IsardMap.js +2 -0
  33. package/dist/react/IsardMap.js.map +1 -1
  34. package/dist/react/VectorTiles.d.ts +5 -1
  35. package/dist/react/VectorTiles.js +8 -0
  36. package/dist/react/VectorTiles.js.map +1 -1
  37. package/dist/react/index.d.ts +1 -1
  38. package/dist/react/index.js +1 -1
  39. package/dist/react/index.js.map +1 -1
  40. package/dist/types.d.ts +20 -0
  41. package/dist/types.js.map +1 -1
  42. package/dist/wasm/isard.d.ts +12 -5
  43. package/dist/wasm/isard.js +33 -5
  44. package/dist/wasm/isard_core.d.ts +7 -5
  45. package/dist/wasm/isard_core.js +21 -5
  46. package/package.json +1 -1
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The map's quality: what it draws less of on a device that cannot keep up. Each level a ceiling on the pixels drawn
3
+ * (the device's pixel ratio capped: a high-density screen draws four times the pixels of its CSS size), the sun's
4
+ * shadows (the solid scene drawn twice more for them: the sun's map, the camera's depth) and how far the 3D Tiles are
5
+ * refined (their screen-space error multiplied: fewer, coarser tiles far away). 'auto' starts high and steps down
6
+ * when the frames stay slow while the map moves.
7
+ */
8
+ export const QUALITY = {
9
+ high: { pixelRatio: Number.POSITIVE_INFINITY, shadows: true, errorScale: 1 },
10
+ medium: { pixelRatio: 1.5, shadows: true, errorScale: 1.5 },
11
+ low: { pixelRatio: 1, shadows: false, errorScale: 2.5 },
12
+ };
13
+ /** The level under each, for 'auto' */
14
+ const LOWER = { high: 'medium', medium: 'low', low: undefined };
15
+ /** A move measured before it is judged: `WINDOW` frames, or `WINDOW_MS` of them on a slow device (at least
16
+ * `MIN_FRAMES`); a gap past `GAP_MS` between two frames is no move (the map was still) */
17
+ const WINDOW = 60;
18
+ const WINDOW_MS = 2000;
19
+ const MIN_FRAMES = 10;
20
+ const GAP_MS = 500;
21
+ /** The median interval between the frames of a move past which the map steps down (some 40 frames a second) */
22
+ const SLOW_MS = 25;
23
+ /** After a step down, the time the new level is given before it is judged */
24
+ const SETTLE_MS = 3000;
25
+ /**
26
+ * 'auto': the intervals between the frames drawn while the map moves; once a window of them (`WINDOW`) has a median
27
+ * past `SLOW_MS`, `step` is called with the level under the current one. It never steps up: at the display's rate a fast
28
+ * frame tells nothing of what a finer level would cost
29
+ */
30
+ export class QualityWatch {
31
+ level;
32
+ step;
33
+ intervals = [];
34
+ last = 0;
35
+ settled = 0;
36
+ constructor(level, step) {
37
+ this.level = level;
38
+ this.step = step;
39
+ }
40
+ /** A frame drawn at `now` (ms), the map moving or not */
41
+ frame(now, moving) {
42
+ const gap = now - this.last;
43
+ this.last = now;
44
+ if (!moving || gap > GAP_MS || now < this.settled)
45
+ return;
46
+ this.intervals.push(gap);
47
+ const spent = this.intervals.reduce((sum, i) => sum + i, 0);
48
+ if (this.intervals.length < WINDOW && (spent < WINDOW_MS || this.intervals.length < MIN_FRAMES))
49
+ return;
50
+ const sorted = [...this.intervals].sort((a, b) => a - b);
51
+ const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
52
+ const lower = LOWER[this.level];
53
+ this.intervals.length = 0;
54
+ if (median <= SLOW_MS || !lower)
55
+ return;
56
+ this.level = lower;
57
+ this.settled = now + SETTLE_MS;
58
+ this.step(lower);
59
+ }
60
+ }
61
+ //# sourceMappingURL=quality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality.js","sourceRoot":"","sources":["../../src/map/quality.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAcH,MAAM,CAAC,MAAM,OAAO,GAA+C;IACjE,IAAI,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE;IAC5E,MAAM,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE;IAC3D,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE;CACxD,CAAC;AAEF,uCAAuC;AACvC,MAAM,KAAK,GAAmD,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;AAEhH;0FAC0F;AAC1F,MAAM,MAAM,GAAG,EAAE,CAAC;AAClB,MAAM,SAAS,GAAG,IAAI,CAAC;AACvB,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,MAAM,GAAG,GAAG,CAAC;AAEnB,+GAA+G;AAC/G,MAAM,OAAO,GAAG,EAAE,CAAC;AAEnB,6EAA6E;AAC7E,MAAM,SAAS,GAAG,IAAI,CAAC;AAEvB;;;;GAIG;AACH,MAAM,OAAO,YAAY;IAMb;IACS;IANF,SAAS,GAAa,EAAE,CAAC;IAClC,IAAI,GAAG,CAAC,CAAC;IACT,OAAO,GAAG,CAAC,CAAC;IAEpB,YACU,KAAc,EACL,IAA8B;QADvC,UAAK,GAAL,KAAK,CAAS;QACL,SAAI,GAAJ,IAAI,CAA0B;IAC9C,CAAC;IAEJ,yDAAyD;IACzD,KAAK,CAAC,GAAW,EAAE,MAAe;QAChC,MAAM,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC;QAE5B,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,MAAM,IAAI,GAAG,GAAG,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO;YAAE,OAAO;QAC1D,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEzB,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;QAE5D,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,GAAG,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC;YAAE,OAAO;QAExG,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC1D,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAEhC,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC;QAC1B,IAAI,MAAM,IAAI,OAAO,IAAI,CAAC,KAAK;YAAE,OAAO;QACxC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,GAAG,GAAG,SAAS,CAAC;QAC/B,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,CAAC;CACF","sourcesContent":["/**\n * The map's quality: what it draws less of on a device that cannot keep up. Each level a ceiling on the pixels drawn\n * (the device's pixel ratio capped: a high-density screen draws four times the pixels of its CSS size), the sun's\n * shadows (the solid scene drawn twice more for them: the sun's map, the camera's depth) and how far the 3D Tiles are\n * refined (their screen-space error multiplied: fewer, coarser tiles far away). 'auto' starts high and steps down\n * when the frames stay slow while the map moves.\n */\n\nimport type { Quality } from '../types.ts';\n\n/** What a level draws */\nexport interface QualitySettings {\n /** The highest pixel ratio drawn */\n pixelRatio: number;\n /** The sun casts its shadows (its light and the night stay without) */\n shadows: boolean;\n /** Multiplies the 3D Tiles' screen-space error */\n errorScale: number;\n}\n\nexport const QUALITY: Readonly<Record<Quality, QualitySettings>> = {\n high: { pixelRatio: Number.POSITIVE_INFINITY, shadows: true, errorScale: 1 },\n medium: { pixelRatio: 1.5, shadows: true, errorScale: 1.5 },\n low: { pixelRatio: 1, shadows: false, errorScale: 2.5 },\n};\n\n/** The level under each, for 'auto' */\nconst LOWER: Readonly<Record<Quality, Quality | undefined>> = { high: 'medium', medium: 'low', low: undefined };\n\n/** A move measured before it is judged: `WINDOW` frames, or `WINDOW_MS` of them on a slow device (at least\n * `MIN_FRAMES`); a gap past `GAP_MS` between two frames is no move (the map was still) */\nconst WINDOW = 60;\nconst WINDOW_MS = 2000;\nconst MIN_FRAMES = 10;\nconst GAP_MS = 500;\n\n/** The median interval between the frames of a move past which the map steps down (some 40 frames a second) */\nconst SLOW_MS = 25;\n\n/** After a step down, the time the new level is given before it is judged */\nconst SETTLE_MS = 3000;\n\n/**\n * 'auto': the intervals between the frames drawn while the map moves; once a window of them (`WINDOW`) has a median\n * past `SLOW_MS`, `step` is called with the level under the current one. It never steps up: at the display's rate a fast\n * frame tells nothing of what a finer level would cost\n */\nexport class QualityWatch {\n private readonly intervals: number[] = [];\n private last = 0;\n private settled = 0;\n\n constructor(\n private level: Quality,\n private readonly step: (level: Quality) => void,\n ) {}\n\n /** A frame drawn at `now` (ms), the map moving or not */\n frame(now: number, moving: boolean): void {\n const gap = now - this.last;\n\n this.last = now;\n if (!moving || gap > GAP_MS || now < this.settled) return;\n this.intervals.push(gap);\n\n const spent = this.intervals.reduce((sum, i) => sum + i, 0);\n\n if (this.intervals.length < WINDOW && (spent < WINDOW_MS || this.intervals.length < MIN_FRAMES)) return;\n\n const sorted = [...this.intervals].sort((a, b) => a - b);\n const median = sorted[Math.floor(sorted.length / 2)] ?? 0;\n const lower = LOWER[this.level];\n\n this.intervals.length = 0;\n if (median <= SLOW_MS || !lower) return;\n this.level = lower;\n this.settled = now + SETTLE_MS;\n this.step(lower);\n }\n}\n"]}
package/dist/map.d.ts CHANGED
@@ -26,7 +26,7 @@ import { Tiles3dLayer } from './layers/tiles3d.ts';
26
26
  import { type TreeOptions } from './layers/trees.ts';
27
27
  import { type HoverPicking } from './map/hover-pick.ts';
28
28
  import { type LayerSpec } from './map/layer-spec.ts';
29
- import type { AnimationOptions, AtmosphereOptions, Basemap, FeatureHit, FirstPersonView, FlyToOptions, GeoMapOptions, LoadingMode, LonLat, MapEvents, NetworkOptions, PickOptions, Point, Projection, RasterOptions, SunOptions, SwipeOptions, TerrainModel, TerrainOptions, Tiles3dOptions, ViewshedOptions } from './types.ts';
29
+ import type { AnimationOptions, AtmosphereOptions, Basemap, FeatureHit, FirstPersonView, FlyToOptions, GeoMapOptions, LoadingMode, LonLat, MapEvents, NetworkOptions, PickOptions, Point, Projection, Quality, RasterOptions, SunOptions, SwipeOptions, TerrainModel, TerrainOptions, Tiles3dOptions, ViewshedOptions } from './types.ts';
30
30
  import { type Control, type ControlPosition } from './ui/controls.ts';
31
31
  import type { Camera, PickHit, RasterLayerStats } from './wasm/isard.js';
32
32
  export type { LayerSpec } from './map/layer-spec.ts';
@@ -119,8 +119,19 @@ export declare class GeoMap {
119
119
  setSwipe(options: SwipeOptions | undefined): void;
120
120
  private applySwipe;
121
121
  private addLayer;
122
+ /** Whether the canvas's WebGL context is lost, its event come or not */
123
+ private contextLost;
122
124
  /** Listens to an event; returns the function that stops listening */
123
125
  on<K extends keyof MapEvents>(type: K, listener: (event: MapEvents[K]) => void): () => void;
126
+ /** 'auto': the frames watched, the map stepped down while they stay slow */
127
+ private qualityWatch;
128
+ private movingNow;
129
+ /** The map's quality: `'high'`, `'medium'` or `'low'` (as `GeoMapOptions.quality` says; `'auto'` the level it
130
+ * stepped down to) */
131
+ getQuality(): Quality;
132
+ /** Sets the map's quality (`GeoMapOptions.quality`); `'auto'` starts again from `'high'` */
133
+ setQuality(quality: Quality | 'auto'): void;
134
+ private applyQuality;
124
135
  getCamera(): Camera;
125
136
  setCamera(camera: Partial<Camera>): void;
126
137
  /** Animated move to a camera: over `duration` milliseconds (or the options), ease-out by default */
package/dist/map.js CHANGED
@@ -39,6 +39,7 @@ import { addSpec } from "./map/layer-spec.js";
39
39
  import { LayerStack } from "./map/layer-stack.js";
40
40
  import { OverlayHost } from "./map/overlay-host.js";
41
41
  import { panoramaHost } from "./map/panorama-host.js";
42
+ import { QUALITY, QualityWatch } from "./map/quality.js";
42
43
  import { rasterSource } from "./map/raster-source.js";
43
44
  import { SymbolAtlases } from "./map/symbol-atlases.js";
44
45
  import { applyViewshed } from "./map/viewshed.js";
@@ -158,6 +159,7 @@ export class GeoMap {
158
159
  upgrading: () => this.engines.upgrading !== undefined,
159
160
  working: () => this.#working > 0,
160
161
  drawn: (hovered) => {
162
+ this.qualityWatch?.frame(performance.now(), this.movingNow);
161
163
  // The picking pass sees the frame just drawn; its answer comes on a later frame
162
164
  if (hovered && this.listeners.has('hoverfeature'))
163
165
  this.hoverPick.request(hovered, this.hoverPicking);
@@ -181,6 +183,17 @@ export class GeoMap {
181
183
  this.setSun(options.sun);
182
184
  this.resizeObserver = new ResizeObserver(() => this.resize());
183
185
  this.resizeObserver.observe(container);
186
+ this.#quality = options.quality === 'auto' || options.quality === undefined ? 'high' : options.quality;
187
+ if (options.quality === 'auto') {
188
+ this.qualityWatch = new QualityWatch('high', (level) => this.applyQuality(level));
189
+ // The frames measured are those of a move
190
+ this.on('movestart', () => {
191
+ this.movingNow = true;
192
+ });
193
+ this.on('moveend', () => {
194
+ this.movingNow = false;
195
+ });
196
+ }
184
197
  this.unwatchRatio = watchPixelRatio(() => this.resize());
185
198
  this.resize();
186
199
  const interactive = options.interactive ?? true;
@@ -345,13 +358,26 @@ export class GeoMap {
345
358
  // others on it
346
359
  if (layer.needsFullEngine && !this.engines.tiles3d)
347
360
  this.upgrade();
348
- else
349
- layer.attach(this.engine);
361
+ else {
362
+ try {
363
+ layer.attach(this.engine);
364
+ }
365
+ catch (error) {
366
+ // A context lost before the browser says so (its event comes later): nothing could be made on it, the layer is
367
+ // made with the others on the next engine (`restore`). Any other failure is the caller's
368
+ if (!this.contextLost())
369
+ throw error;
370
+ }
371
+ }
350
372
  // Under the layers kept on top (the basemap's labels)
351
373
  this.stack.add(layer);
352
374
  this.requestRender();
353
375
  return layer;
354
376
  }
377
+ /** Whether the canvas's WebGL context is lost, its event come or not */
378
+ contextLost() {
379
+ return this.lost || this.canvas.getContext('webgl2')?.isContextLost() === true;
380
+ }
355
381
  /** @internal Keeps a layer above the layers added after it (they go under it), as the labels of a basemap */
356
382
  keepOnTop(layer) {
357
383
  this.onTop.add(layer);
@@ -375,6 +401,38 @@ export class GeoMap {
375
401
  this.listeners.emit(type, event);
376
402
  }
377
403
  // ── Camera ──
404
+ #quality = 'high';
405
+ /** 'auto': the frames watched, the map stepped down while they stay slow */
406
+ qualityWatch;
407
+ movingNow = false;
408
+ /** The map's quality: `'high'`, `'medium'` or `'low'` (as `GeoMapOptions.quality` says; `'auto'` the level it
409
+ * stepped down to) */
410
+ getQuality() {
411
+ return this.#quality;
412
+ }
413
+ /** @internal What its quality draws */
414
+ get quality() {
415
+ return QUALITY[this.#quality];
416
+ }
417
+ /** Sets the map's quality (`GeoMapOptions.quality`); `'auto'` starts again from `'high'` */
418
+ setQuality(quality) {
419
+ this.qualityWatch = quality === 'auto' ? new QualityWatch('high', (level) => this.applyQuality(level)) : undefined;
420
+ this.applyQuality(quality === 'auto' ? 'high' : quality);
421
+ }
422
+ applyQuality(quality) {
423
+ if (quality === this.#quality)
424
+ return;
425
+ this.#quality = quality;
426
+ // The canvas at its pixel ratio, the sun's shadows, the layers' detail
427
+ this.resize();
428
+ if (this.sun)
429
+ this.setSun(this.sun);
430
+ this.layers.forEach((layer) => {
431
+ layer.qualityChanged();
432
+ });
433
+ this.emit('quality', { quality });
434
+ this.requestRender();
435
+ }
378
436
  getCamera() {
379
437
  return this.frames.camera ?? this.engine.camera();
380
438
  }
@@ -473,8 +531,8 @@ export class GeoMap {
473
531
  // The twilight: from 6° above the horizon to 6° under it, smoothly
474
532
  const dusk = Math.min(1, Math.max(0, (6 - altitude) / 12));
475
533
  const night = Math.min(1, Math.max(0, options.night ?? dusk * dusk * (3 - 2 * dusk)));
476
- // The shadows fade with the sun's light
477
- const opacity = Math.min(1, Math.max(0, options.opacity ?? 0.35)) * (1 - night);
534
+ // The shadows fade with the sun's light; none at a low quality (its light and the night stay)
535
+ const opacity = this.quality.shadows ? Math.min(1, Math.max(0, options.opacity ?? 0.35)) * (1 - night) : 0;
478
536
  this.engine.setSun(azimuth, altitude, opacity, night);
479
537
  this.requestRender();
480
538
  }
@@ -728,7 +786,7 @@ export class GeoMap {
728
786
  }
729
787
  /** Lines from GeoJSON (line strings, and polygon outlines), styled per feature */
730
788
  addLines(options) {
731
- return this.place(new LineLayer(this, options.data, lineStyle(options)), options.before);
789
+ return this.place(new LineLayer(this, options.data, lineStyle(options), options.altitudeReference), options.before);
732
790
  }
733
791
  /** Discs from GeoJSON points, styled per feature */
734
792
  addCircles(options) {
@@ -888,7 +946,7 @@ export class GeoMap {
888
946
  if (this.lost)
889
947
  return;
890
948
  const { clientWidth: width, clientHeight: height } = this.canvas;
891
- const ratio = window.devicePixelRatio || 1;
949
+ const ratio = Math.min(window.devicePixelRatio || 1, this.quality.pixelRatio);
892
950
  this.canvas.width = Math.round(width * ratio);
893
951
  this.canvas.height = Math.round(height * ratio);
894
952
  this.engine.resize(width, height, ratio);