@navaramap/three-plugins 0.0.4 → 0.0.5

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/dist/index.d.ts CHANGED
@@ -388,8 +388,28 @@ export declare type TileJson = {
388
388
  maxzoom?: number;
389
389
  /** Tiling scheme. `"tms"` flips the Y axis (raster sources only). @defaultValue "xyz" */
390
390
  scheme?: "xyz" | "tms";
391
+ /**
392
+ * Tile size in pixels. Not part of the TileJSON spec, but emitted by
393
+ * MapLibre-oriented tile servers; applied to `raster-dem` sources only.
394
+ * @defaultValue 512
395
+ */
396
+ tileSize?: number;
397
+ /**
398
+ * Elevation encoding of DEM tiles. Not part of the TileJSON spec, but
399
+ * emitted by MapLibre-oriented tile servers; applied to `raster-dem` sources
400
+ * only.
401
+ * @defaultValue "mapbox"
402
+ */
403
+ encoding?: TileJsonDemEncoding;
391
404
  };
392
405
 
406
+ /**
407
+ * How a `raster-dem` document's RGB tiles encode elevation, matching MapLibre's
408
+ * `encoding` values. MapLibre's `"custom"` (free-form decode factors) is not
409
+ * supported.
410
+ */
411
+ export declare type TileJsonDemEncoding = "mapbox" | "terrarium";
412
+
393
413
  /** Detail passed to the {@link TileJsonPlugin} `loaded` event. */
394
414
  export declare type TileJsonLoadedEvent = {
395
415
  /** The Navara source created for the document. */
@@ -420,9 +440,13 @@ export declare class TileJsonPlugin extends Plugin_2<View_4, ViewContext> {
420
440
  *
421
441
  * `desc.url` is the TileJSON document URL; the plugin fetches it, then creates
422
442
  * one source of the requested `desc.type` using the document's first tile
423
- * endpoint, `minzoom`/`maxzoom`, and `scheme`. The document's `attribution` is
424
- * surfaced through the view's built-in attribution UI (`view.attribution`), and
425
- * a `loaded` event is emitted carrying the source, document, and attribution.
443
+ * endpoint, `minzoom`/`maxzoom`, and `scheme` — plus, for `raster-dem`, the
444
+ * MapLibre-compatible `tileSize` and `encoding` fields (defaulting to
445
+ * MapLibre's 512 and `"mapbox"`). Each of those fields can also be set on
446
+ * `desc` itself, taking precedence over the document. The document's
447
+ * `attribution` is surfaced through the view's built-in attribution UI
448
+ * (`view.attribution`), and a `loaded` event is emitted carrying the source,
449
+ * document, and attribution.
426
450
  *
427
451
  * A TileJSON `tiles` array lists mirror endpoints for the same tileset (as in
428
452
  * MapLibre, which shards requests across them). Navara sources take a single
@@ -456,15 +480,36 @@ export declare type TileJsonPluginEventMap = {
456
480
  loaded: (event: TileJsonLoadedEvent) => void;
457
481
  };
458
482
 
483
+ /** Registers a TileJSON document as a `raster-dem` (elevation) source. */
484
+ export declare type TileJsonRasterDemSourceDescription = TileJsonSourceDescriptionBase & {
485
+ type: "raster-dem";
486
+ /** Overrides the document's `tileSize` when set. @defaultValue 512 */
487
+ tileSize?: number;
488
+ /** Overrides the document's `encoding` when set. @defaultValue "mapbox" */
489
+ encoding?: TileJsonDemEncoding;
490
+ };
491
+
492
+ /** Registers a TileJSON document as a `raster-tile` (imagery) source. */
493
+ export declare type TileJsonRasterTileSourceDescription = TileJsonSourceDescriptionBase & {
494
+ type: "raster-tile";
495
+ };
496
+
459
497
  /**
460
498
  * Describes a TileJSON tile source to register via {@link TileJsonPlugin.addSource}.
461
499
  * Mirrors the shape of {@link ThreeView.addSource}, except `url` is the TileJSON
462
500
  * document URL (not a tile template) — the plugin fetches it and derives the tile
463
- * URL, zoom range, and attribution from the document.
501
+ * URL, zoom range, and attribution from the document. Document-derived fields
502
+ * (`minzoom`, `maxzoom`, `scheme` — plus `tileSize` and `encoding` for
503
+ * `raster-dem`) can also be set here to take precedence over the document.
464
504
  */
465
- export declare type TileJsonSourceDescription = {
466
- /** Navara source type to create, as in `addSource`. */
467
- type: TileJsonSourceType;
505
+ export declare type TileJsonSourceDescription = TileJsonRasterTileSourceDescription | TileJsonVectorTileSourceDescription | TileJsonRasterDemSourceDescription;
506
+
507
+ /**
508
+ * Fields shared by every {@link TileJsonSourceDescription} variant. Alongside
509
+ * `url` and `id`, it accepts the same optional fields the TileJSON document
510
+ * carries; a field set here takes precedence over the fetched document's value.
511
+ */
512
+ declare type TileJsonSourceDescriptionBase = {
468
513
  /** URL of the TileJSON 3.0.0 document to fetch and expand. */
469
514
  url: string;
470
515
  /**
@@ -473,14 +518,25 @@ export declare type TileJsonSourceDescription = {
473
518
  * handle. When omitted, the engine generates one.
474
519
  */
475
520
  id?: string;
521
+ /** Overrides the document's `minzoom` when set. */
522
+ minzoom?: number;
523
+ /** Overrides the document's `maxzoom` when set. */
524
+ maxzoom?: number;
525
+ /** Overrides the document's `scheme` when set. */
526
+ scheme?: "xyz" | "tms";
476
527
  };
477
528
 
478
529
  /**
479
530
  * Which Navara source type a TileJSON document is materialized into. Declared by
480
531
  * the caller because TileJSON has no field that reliably distinguishes raster
481
- * imagery from vector tiles.
532
+ * imagery from vector or elevation tiles.
482
533
  */
483
- export declare type TileJsonSourceType = "raster-tile" | "vector-tile";
534
+ export declare type TileJsonSourceType = "raster-tile" | "vector-tile" | "raster-dem";
535
+
536
+ /** Registers a TileJSON document as a `vector-tile` (MVT) source. */
537
+ export declare type TileJsonVectorTileSourceDescription = TileJsonSourceDescriptionBase & {
538
+ type: "vector-tile";
539
+ };
484
540
 
485
541
  declare type UpdateListener = (state: OverlayState) => void;
486
542
 
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { EventHandler as e, Plugin as t, convertWorldToScreen as n, degreeToRadian as r, eastNorthUpToFixedFrame as i, geodeticToVector3 as a, radianToDegree as o, vector3ToGeodetic as s } from "@navaramap/three";
2
- import { Matrix4 as c, Vector3 as l } from "three";
1
+ import { EventHandler as e, MAPBOX_ELEVATION_DECODER as t, Plugin as n, TERRARIUM_ELEVATION_DECODER as r, convertWorldToScreen as i, degreeToRadian as a, eastNorthUpToFixedFrame as o, geodeticToVector3 as s, radianToDegree as c, vector3ToGeodetic as l } from "@navaramap/three";
2
+ import { Matrix4 as u, Vector3 as d } from "three";
3
3
  //#region src/PersonViewPlugin.ts
4
- var u = {
4
+ var f = {
5
5
  x: 0,
6
6
  y: 0,
7
7
  z: 0
8
- }, d = {
8
+ }, p = {
9
9
  forward: ["KeyW"],
10
10
  backward: ["KeyS"],
11
11
  turnLeft: ["KeyA"],
@@ -19,7 +19,7 @@ var u = {
19
19
  dash: ["ShiftLeft", "ShiftRight"],
20
20
  orbitCamera: ["AltLeft", "AltRight"],
21
21
  toggleView: ["KeyV"]
22
- }, f = {
22
+ }, m = {
23
23
  allowCameraControl: !1,
24
24
  initialView: "tpv",
25
25
  moveSpeed: 50,
@@ -37,7 +37,7 @@ var u = {
37
37
  startLng: 139.7671,
38
38
  startHeight: 500,
39
39
  startHeading: Math.PI * 1.3
40
- }, p = 3, m = 1e3, h = class extends t {
40
+ }, h = 3, g = 1e3, _ = class extends n {
41
41
  view;
42
42
  config;
43
43
  character;
@@ -60,21 +60,21 @@ var u = {
60
60
  movementSuppressed = !1;
61
61
  boundKeyDown;
62
62
  boundKeyUp;
63
- _curPos = new l();
64
- _east = new l();
65
- _north = new l();
66
- _worldForward = new l();
67
- _offset = new l();
68
- _headingMatrix = new c();
69
- _characterFrame = new c();
63
+ _curPos = new d();
64
+ _east = new d();
65
+ _north = new d();
66
+ _worldForward = new d();
67
+ _offset = new d();
68
+ _headingMatrix = new u();
69
+ _characterFrame = new u();
70
70
  constructor(e = {}) {
71
71
  super();
72
72
  let { character: t, keys: n, ...r } = e;
73
73
  this.config = {
74
- ...f,
74
+ ...m,
75
75
  ...r
76
76
  }, this.character = t ? this.resolveCharacter(t) : null, this.keys = {
77
- ...d,
77
+ ...p,
78
78
  ...n
79
79
  }, this.keyToAction = this.buildKeyMap(this.keys), this.modelHeading = this.config.startHeading, this.cameraHeading = this.config.startHeading, this.viewMode = this.config.initialView, this.currentAnimState = this.character ? this.character.animation.idleClip : null, this.boundKeyDown = this.onKeyDown.bind(this), this.boundKeyUp = this.onKeyUp.bind(this);
80
80
  }
@@ -92,18 +92,18 @@ var u = {
92
92
  start() {
93
93
  if (!this.view || this.animId != null) return;
94
94
  this.animId = -1;
95
- let { startLat: e, startLng: t, startHeight: n, startHeading: i } = this.config, o = a({
96
- lat: r(e),
97
- lng: r(t),
95
+ let { startLat: e, startLng: t, startHeight: n, startHeading: r } = this.config, i = s({
96
+ lat: a(e),
97
+ lng: a(t),
98
98
  height: n
99
99
  });
100
- if (this.modelHeading = i, this.cameraHeading = i, this.character) {
101
- let { animation: e, modelRotationOffset: t, modelScale: n, castShadow: r, receiveShadow: a } = this.character;
100
+ if (this.modelHeading = r, this.cameraHeading = r, this.character) {
101
+ let { animation: e, modelRotationOffset: t, modelScale: n, castShadow: a, receiveShadow: o } = this.character;
102
102
  this.handle = this.view.addMesh({
103
103
  gltfModel: {
104
104
  url: this.character.modelUrl,
105
- castShadow: r,
106
- receiveShadow: a,
105
+ castShadow: a,
106
+ receiveShadow: o,
107
107
  animationEnabled: !0,
108
108
  animationAutoPlay: !0,
109
109
  animationActiveClip: e.idleClip,
@@ -111,7 +111,7 @@ var u = {
111
111
  animationLoop: !0,
112
112
  animationCrossfadeDuration: e.crossfadeDuration
113
113
  },
114
- matrixWorld: this.composeCharacterFrame(o, i),
114
+ matrixWorld: this.composeCharacterFrame(i, r),
115
115
  rotation: {
116
116
  x: t.x,
117
117
  y: t.y,
@@ -130,20 +130,20 @@ var u = {
130
130
  }
131
131
  teleport(e) {
132
132
  if (!this.view) return;
133
- let { lng: t, lat: n, alt: i } = e, o = e.heading == null ? this.cameraHeading : e.heading;
133
+ let { lng: t, lat: n, alt: r } = e, i = e.heading == null ? this.cameraHeading : e.heading;
134
134
  if (this.handle && this.character) {
135
- let e = a({
136
- lat: r(n),
137
- lng: r(t),
138
- height: i
135
+ let e = s({
136
+ lat: a(n),
137
+ lng: a(t),
138
+ height: r
139
139
  });
140
- this.handle.update({ matrixWorld: this.composeCharacterFrame(e, o) });
140
+ this.handle.update({ matrixWorld: this.composeCharacterFrame(e, i) });
141
141
  }
142
- this.modelHeading = o, this.cameraHeading = o, this.placeChaseCamera(n, t, i, o), this.state = {
142
+ this.modelHeading = i, this.cameraHeading = i, this.placeChaseCamera(n, t, r, i), this.state = {
143
143
  lng: t,
144
144
  lat: n,
145
- alt: i,
146
- heading: o,
145
+ alt: r,
146
+ heading: i,
147
147
  speed: 0,
148
148
  animationState: this.currentAnimState,
149
149
  mode: this.viewMode
@@ -169,16 +169,16 @@ var u = {
169
169
  }
170
170
  setHeading(e) {
171
171
  if (!this.view) return;
172
- let { lat: t, lng: n, alt: i } = this.state;
172
+ let { lat: t, lng: n, alt: r } = this.state;
173
173
  if (this.modelHeading = e, this.cameraHeading = e, this.handle && this.character) {
174
- let o = a({
175
- lat: r(t),
176
- lng: r(n),
177
- height: i
174
+ let i = s({
175
+ lat: a(t),
176
+ lng: a(n),
177
+ height: r
178
178
  });
179
- this.handle.update({ matrixWorld: this.composeCharacterFrame(o, e) });
179
+ this.handle.update({ matrixWorld: this.composeCharacterFrame(i, e) });
180
180
  }
181
- this.isFreeCamera() || this.placeChaseCamera(t, n, i, e), this.state = {
181
+ this.isFreeCamera() || this.placeChaseCamera(t, n, r, e), this.state = {
182
182
  ...this.state,
183
183
  heading: e
184
184
  }, this.notify();
@@ -212,10 +212,10 @@ var u = {
212
212
  modelUrl: e.modelUrl,
213
213
  animation: e.animation,
214
214
  modelRotationOffset: e.modelRotationOffset ? {
215
- ...u,
215
+ ...f,
216
216
  ...e.modelRotationOffset
217
- } : u,
218
- modelScale: e.modelScale ?? p,
217
+ } : f,
218
+ modelScale: e.modelScale ?? h,
219
219
  hideModelInFpv: e.hideModelInFpv ?? !0,
220
220
  castShadow: e.castShadow ?? !1,
221
221
  receiveShadow: e.receiveShadow ?? !1
@@ -246,7 +246,7 @@ var u = {
246
246
  if (!this.view) return;
247
247
  let { cameraDistance: i, cameraPitch: a, fpvPitch: o, fpvHeightOffset: s, fpvForwardOffset: c } = this.config, l = this.viewMode === "fpv", u = n + s;
248
248
  if (l) {
249
- let n = m, i = c + m, a = Math.tan(o) * n, s = this.advanceLatLng(e, t, r, i);
249
+ let n = g, i = c + g, a = Math.tan(o) * n, s = this.advanceLatLng(e, t, r, i);
250
250
  this._offset.set(-Math.sin(r) * n, -Math.cos(r) * n, a), this.view.cameraFollow(!0, {
251
251
  lat: s.lat,
252
252
  lng: s.lng,
@@ -262,20 +262,20 @@ var u = {
262
262
  }, this._offset);
263
263
  }
264
264
  composeCharacterFrame(e, t) {
265
- let n = i(e);
265
+ let n = o(e);
266
266
  return this._headingMatrix.makeRotationZ(-t), this._characterFrame.multiplyMatrices(n, this._headingMatrix);
267
267
  }
268
- advanceLatLng(e, t, n, c) {
269
- let l = a({
270
- lat: r(e),
271
- lng: r(t),
268
+ advanceLatLng(e, t, n, r) {
269
+ let i = s({
270
+ lat: a(e),
271
+ lng: a(t),
272
272
  height: 0
273
- }), u = i(l);
274
- this._east.setFromMatrixColumn(u, 0).normalize(), this._north.setFromMatrixColumn(u, 1).normalize(), this._worldForward.copy(this._east).multiplyScalar(Math.sin(n)).addScaledVector(this._north, Math.cos(n)), l.addScaledVector(this._worldForward, c);
275
- let d = s(l);
273
+ }), u = o(i);
274
+ this._east.setFromMatrixColumn(u, 0).normalize(), this._north.setFromMatrixColumn(u, 1).normalize(), this._worldForward.copy(this._east).multiplyScalar(Math.sin(n)).addScaledVector(this._north, Math.cos(n)), i.addScaledVector(this._worldForward, r);
275
+ let d = l(i);
276
276
  return {
277
- lat: o(d.lat),
278
- lng: o(d.lng)
277
+ lat: c(d.lat),
278
+ lng: c(d.lng)
279
279
  };
280
280
  }
281
281
  notify() {
@@ -323,27 +323,27 @@ var u = {
323
323
  this.animId = requestAnimationFrame(this.tick);
324
324
  let t = Math.min((e - this.lastTime) / 1e3, .1);
325
325
  this.lastTime = e;
326
- let n = 0, c = 0, l = 0;
327
- this.heldActions.has("forward") && (c += 1), this.heldActions.has("backward") && --c, this.heldActions.has("turnLeft") && --n, this.heldActions.has("turnRight") && (n += 1), this.heldActions.has("ascend") && (l += 1), this.heldActions.has("descend") && --l;
326
+ let n = 0, r = 0, i = 0;
327
+ this.heldActions.has("forward") && (r += 1), this.heldActions.has("backward") && --r, this.heldActions.has("turnLeft") && --n, this.heldActions.has("turnRight") && (n += 1), this.heldActions.has("ascend") && (i += 1), this.heldActions.has("descend") && --i;
328
328
  let { moveSpeed: u, rotationSpeed: d, altSpeed: f, minAlt: p, maxAlt: m, cameraLerpSpeed: h } = this.config;
329
- n !== 0 && (this.modelHeading += r(d * n));
329
+ n !== 0 && (this.modelHeading += a(d * n));
330
330
  let g = this._curPos;
331
331
  if (this.handle && this.modelRef) {
332
332
  let e = this.modelRef.getWorldPosition();
333
333
  if (!e) return;
334
334
  g.copy(e);
335
- } else g.copy(a({
336
- lat: r(this.state.lat),
337
- lng: r(this.state.lng),
335
+ } else g.copy(s({
336
+ lat: a(this.state.lat),
337
+ lng: a(this.state.lng),
338
338
  height: this.state.alt
339
339
  }));
340
- let _ = i(g);
340
+ let _ = o(g);
341
341
  this._east.setFromMatrixColumn(_, 0).normalize(), this._north.setFromMatrixColumn(_, 1).normalize();
342
- let v = s(g);
343
- this._worldForward.copy(this._east).multiplyScalar(Math.sin(this.modelHeading)).addScaledVector(this._north, Math.cos(this.modelHeading)), c !== 0 && g.addScaledVector(this._worldForward, u * this.dashMultiplier * t * c);
344
- let y = s(g), b = v.height + l * f * t, x = Math.max(p, Math.min(m, b)), S = o(y.lat), C = o(y.lng), w = x;
342
+ let v = l(g);
343
+ this._worldForward.copy(this._east).multiplyScalar(Math.sin(this.modelHeading)).addScaledVector(this._north, Math.cos(this.modelHeading)), r !== 0 && g.addScaledVector(this._worldForward, u * this.dashMultiplier * t * r);
344
+ let y = l(g), b = v.height + i * f * t, x = Math.max(p, Math.min(m, b)), S = c(y.lat), C = c(y.lng), w = x;
345
345
  if (this.handle && this.character) {
346
- let e = a({
346
+ let e = s({
347
347
  lat: y.lat,
348
348
  lng: y.lng,
349
349
  height: x
@@ -364,7 +364,7 @@ var u = {
364
364
  height: w + this.config.fpvHeightOffset
365
365
  });
366
366
  else this.placeChaseCamera(S, C, w, this.cameraHeading);
367
- let E = c !== 0 || n !== 0 || l !== 0, D = E && this.dashMultiplier > 1, O = this.currentAnimState;
367
+ let E = r !== 0 || n !== 0 || i !== 0, D = E && this.dashMultiplier > 1, O = this.currentAnimState;
368
368
  if (this.character && this.modelRef) {
369
369
  let { animation: e } = this.character, t = e.walkClip ?? e.idleClip, n;
370
370
  n = D ? e.dashClip : E ? t : e.idleClip, n !== this.currentAnimState && (this.modelRef.crossFadeAnimation(this.currentAnimState ?? "", n, e.crossfadeDuration), this.currentAnimState = n), O = n;
@@ -386,10 +386,10 @@ var u = {
386
386
  };
387
387
  //#endregion
388
388
  //#region src/OverlayPlugin.ts
389
- function g(e, t, n) {
389
+ function v(e, t, n) {
390
390
  e.style.transform = `translate(${t}px, ${n}px)`, e.style.willChange = "transform";
391
391
  }
392
- var _ = 1e5, v = class extends t {
392
+ var y = 1e5, b = class extends n {
393
393
  view;
394
394
  maxDistance;
395
395
  positions = [];
@@ -397,7 +397,7 @@ var _ = 1e5, v = class extends t {
397
397
  listeners = /* @__PURE__ */ new Set();
398
398
  boundHandler;
399
399
  constructor(e) {
400
- super(), this.maxDistance = e?.maxDistance ?? _, this.boundHandler = this.handlePreRender.bind(this);
400
+ super(), this.maxDistance = e?.maxDistance ?? y, this.boundHandler = this.handlePreRender.bind(this);
401
401
  }
402
402
  async init(e, t) {
403
403
  this.view = e, e.on("preRender", this.boundHandler);
@@ -411,47 +411,47 @@ var _ = 1e5, v = class extends t {
411
411
  dispose() {
412
412
  this.view && this.view.off("preRender", this.boundHandler), this.listeners.clear(), this.positions = [], this.projected.clear();
413
413
  }
414
- worldToScreen(e, t, i) {
414
+ worldToScreen(e, t, n) {
415
415
  if (!this.view) return null;
416
- let o = a({
417
- lng: r(e),
418
- lat: r(t),
419
- height: i
420
- }), s = n({
416
+ let r = s({
417
+ lng: a(e),
418
+ lat: a(t),
419
+ height: n
420
+ }), o = i({
421
421
  width: this.view.screenSize.x,
422
422
  height: this.view.screenSize.y,
423
423
  pixelRatio: this.view.pixelRatio
424
- }, this.view.camera.raw, o);
425
- return s ? {
426
- x: s.x,
427
- y: s.y
424
+ }, this.view.camera.raw, r);
425
+ return o ? {
426
+ x: o.x,
427
+ y: o.y
428
428
  } : null;
429
429
  }
430
430
  handlePreRender() {
431
431
  if (!this.view) return;
432
- let e = this.view.camera.positionGeographic, t = a({
433
- lng: r(e.lng),
434
- lat: r(e.lat),
432
+ let e = this.view.camera.positionGeographic, t = s({
433
+ lng: a(e.lng),
434
+ lat: a(e.lat),
435
435
  height: e.height
436
436
  }), n = /* @__PURE__ */ new Map();
437
437
  for (let e of this.positions) {
438
- let i = a({
439
- lng: r(e.lng),
440
- lat: r(e.lat),
438
+ let r = s({
439
+ lng: a(e.lng),
440
+ lat: a(e.lat),
441
441
  height: e.alt
442
- }), o = t.distanceTo(i);
443
- if (o > this.maxDistance) continue;
444
- let s = this.worldToScreen(e.lng, e.lat, e.alt);
445
- s && n.set(e.id, {
446
- ...s,
447
- distance: o
442
+ }), i = t.distanceTo(r);
443
+ if (i > this.maxDistance) continue;
444
+ let o = this.worldToScreen(e.lng, e.lat, e.alt);
445
+ o && n.set(e.id, {
446
+ ...o,
447
+ distance: i
448
448
  });
449
449
  }
450
450
  this.projected = n;
451
- let i = { projected: this.projected };
452
- for (let e of this.listeners) e(i);
451
+ let r = { projected: this.projected };
452
+ for (let e of this.listeners) e(r);
453
453
  }
454
- }, y = "https://api.cesium.com/v1/assets", b = class extends t {
454
+ }, x = "https://api.cesium.com/v1/assets", S = class extends n {
455
455
  view;
456
456
  config;
457
457
  endpoint;
@@ -460,7 +460,7 @@ var _ = 1e5, v = class extends t {
460
460
  }
461
461
  async init(e, t) {
462
462
  this.view = e;
463
- let { assetId: n, accessToken: r, endpoint: i = y } = this.config, a = `${i}/${encodeURIComponent(String(n))}/endpoint?access_token=${encodeURIComponent(r)}`, o = await fetch(a);
463
+ let { assetId: n, accessToken: r, endpoint: i = x } = this.config, a = `${i}/${encodeURIComponent(String(n))}/endpoint?access_token=${encodeURIComponent(r)}`, o = await fetch(a);
464
464
  if (!o.ok) throw Error(`CesiumIonPlugin: failed to fetch asset ${n} endpoint (${o.status} ${o.statusText})`);
465
465
  let s = await o.json();
466
466
  this.endpoint = {
@@ -489,12 +489,19 @@ var _ = 1e5, v = class extends t {
489
489
  });
490
490
  return l.once("deleted", () => c.delete()), l;
491
491
  }
492
- }, x = /^3\.\d+\.\d+$/;
493
- function S(e) {
494
- if (typeof e.tilejson != "string" || !x.test(e.tilejson)) throw Error(`TileJsonPlugin: invalid or missing "tilejson" version field (got ${JSON.stringify(e.tilejson)}).`);
492
+ }, C = /^3\.\d+\.\d+$/, w = 512, T = "mapbox";
493
+ function E(e) {
494
+ if (typeof e.tilejson != "string" || !C.test(e.tilejson)) throw Error(`TileJsonPlugin: invalid or missing "tilejson" version field (got ${JSON.stringify(e.tilejson)}).`);
495
495
  if (!Array.isArray(e.tiles) || e.tiles.length === 0) throw Error("TileJsonPlugin: \"tiles\" must be a non-empty array of URL templates.");
496
496
  }
497
- var C = class extends t {
497
+ function D(e) {
498
+ switch (e) {
499
+ case "mapbox": return t();
500
+ case "terrarium": return r();
501
+ default: throw Error(`TileJsonPlugin: unsupported raster-dem encoding ${JSON.stringify(e)}. Supported encodings: "mapbox", "terrarium".`);
502
+ }
503
+ }
504
+ var O = class extends n {
498
505
  view;
499
506
  events = new e();
500
507
  credits = [];
@@ -514,27 +521,34 @@ var C = class extends t {
514
521
  let t = this.view;
515
522
  if (!t) throw Error("TileJsonPlugin: addSource() must be called after view.init().");
516
523
  let n = await this.fetchTileJson(e.url);
517
- S(n);
524
+ E(n);
518
525
  let [r, ...i] = n.tiles;
519
526
  i.length > 0 && console.warn(`TileJsonPlugin: TileJSON at ${e.url} lists ${n.tiles.length} mirror tile endpoints; Navara sources use a single URL, so only the first is used.`);
520
- let { minzoom: a, maxzoom: o, scheme: s } = n, c = e.id, l = e.type === "raster-tile" ? t.addSource({
521
- type: "raster-tile",
527
+ let a = e.minzoom ?? n.minzoom, o = e.maxzoom ?? n.maxzoom, s = e.scheme ?? n.scheme, c = e.id, l = {
522
528
  url: r,
523
529
  ...c === void 0 ? {} : { id: c },
530
+ ...o === void 0 ? {} : { maxZoom: o }
531
+ }, u = {
532
+ ...l,
524
533
  ...a === void 0 ? {} : { minZoom: a },
525
- ...o === void 0 ? {} : { maxZoom: o },
526
534
  ...s === "tms" ? { tms: !0 } : {}
535
+ }, d;
536
+ return d = e.type === "raster-tile" ? t.addSource({
537
+ type: "raster-tile",
538
+ ...u
539
+ }) : e.type === "raster-dem" ? t.addSource({
540
+ type: "raster-dem",
541
+ ...u,
542
+ tileSize: e.tileSize ?? n.tileSize ?? w,
543
+ elevationDecoder: D(e.encoding ?? n.encoding ?? T)
527
544
  }) : t.addSource({
528
545
  type: "vector-tile",
529
- url: r,
530
- ...c === void 0 ? {} : { id: c },
531
- ...o === void 0 ? {} : { maxZoom: o }
532
- });
533
- return n.attribution && this.addCredit(n.attribution), this.events.emit("loaded", {
534
- source: l,
546
+ ...l
547
+ }), n.attribution && this.addCredit(n.attribution), this.events.emit("loaded", {
548
+ source: d,
535
549
  tilejson: n,
536
550
  attribution: n.attribution
537
- }), l;
551
+ }), d;
538
552
  }
539
553
  dispose() {
540
554
  let e = this.view?.attribution;
@@ -550,4 +564,4 @@ var C = class extends t {
550
564
  }
551
565
  };
552
566
  //#endregion
553
- export { b as CesiumIonPlugin, v as OverlayPlugin, h as PersonViewPlugin, C as TileJsonPlugin, g as moveOverlayElement };
567
+ export { S as CesiumIonPlugin, b as OverlayPlugin, _ as PersonViewPlugin, O as TileJsonPlugin, v as moveOverlayElement };
@@ -1 +1 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("@navaramap/three"),require("three")):typeof define==`function`&&define.amd?define([`exports`,`@navaramap/three`,`three`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.NavaraThreePlugins={},e._navaramap_three,e.three))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);var u={x:0,y:0,z:0},d={forward:[`KeyW`],backward:[`KeyS`],turnLeft:[`KeyA`],turnRight:[`KeyD`],ascend:[`ArrowUp`,`Space`],descend:[`ArrowDown`,`ControlLeft`,`ControlRight`],dash:[`ShiftLeft`,`ShiftRight`],orbitCamera:[`AltLeft`,`AltRight`],toggleView:[`KeyV`]},f={allowCameraControl:!1,initialView:`tpv`,moveSpeed:50,rotationSpeed:3,altSpeed:30,minAlt:50,maxAlt:5e3,cameraDistance:50,cameraPitch:0,cameraLerpSpeed:3,fpvForwardOffset:0,fpvHeightOffset:1,fpvPitch:0,startLat:35.6812,startLng:139.7671,startHeight:500,startHeading:Math.PI*1.3},p=3,m=1e3,h=class extends t.Plugin{view;config;character;keys;keyToAction;handle=null;modelRef=null;animId=null;lastTime=0;state;listeners=new Set;heldActions=new Set;dashMultiplier=1;orbitKeyHeld=!1;orbitLatched=!1;currentAnimState;modelHeading;cameraHeading;viewMode;movementSuppressed=!1;boundKeyDown;boundKeyUp;_curPos=new n.Vector3;_east=new n.Vector3;_north=new n.Vector3;_worldForward=new n.Vector3;_offset=new n.Vector3;_headingMatrix=new n.Matrix4;_characterFrame=new n.Matrix4;constructor(e={}){super();let{character:t,keys:n,...r}=e;this.config={...f,...r},this.character=t?this.resolveCharacter(t):null,this.keys={...d,...n},this.keyToAction=this.buildKeyMap(this.keys),this.modelHeading=this.config.startHeading,this.cameraHeading=this.config.startHeading,this.viewMode=this.config.initialView,this.currentAnimState=this.character?this.character.animation.idleClip:null,this.boundKeyDown=this.onKeyDown.bind(this),this.boundKeyUp=this.onKeyUp.bind(this)}async init(e,t){this.view=e,this.state={lng:this.config.startLng,lat:this.config.startLat,alt:this.config.startHeight,heading:this.config.startHeading,speed:0,animationState:this.currentAnimState,mode:this.viewMode}}start(){if(!this.view||this.animId!=null)return;this.animId=-1;let{startLat:e,startLng:n,startHeight:r,startHeading:i}=this.config,a=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(e),lng:(0,t.degreeToRadian)(n),height:r});if(this.modelHeading=i,this.cameraHeading=i,this.character){let{animation:e,modelRotationOffset:t,modelScale:n,castShadow:r,receiveShadow:o}=this.character;this.handle=this.view.addMesh({gltfModel:{url:this.character.modelUrl,castShadow:r,receiveShadow:o,animationEnabled:!0,animationAutoPlay:!0,animationActiveClip:e.idleClip,animationSpeed:e.speed,animationLoop:!0,animationCrossfadeDuration:e.crossfadeDuration},matrixWorld:this.composeCharacterFrame(a,i),rotation:{x:t.x,y:t.y,z:t.z},scale:{x:n,y:n,z:n}}),this.modelRef=this.handle.ref,this.modelRef.on(`load`,()=>{this.view&&(this.lastTime=performance.now(),this.animId=requestAnimationFrame(this.tick))})}else this.lastTime=performance.now(),this.animId=requestAnimationFrame(this.tick);this.applyInitialCamera(),this.applyModelVisibility(),document.addEventListener(`keydown`,this.boundKeyDown),document.addEventListener(`keyup`,this.boundKeyUp)}teleport(e){if(!this.view)return;let{lng:n,lat:r,alt:i}=e,a=e.heading==null?this.cameraHeading:e.heading;if(this.handle&&this.character){let e=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(r),lng:(0,t.degreeToRadian)(n),height:i});this.handle.update({matrixWorld:this.composeCharacterFrame(e,a)})}this.modelHeading=a,this.cameraHeading=a,this.placeChaseCamera(r,n,i,a),this.state={lng:n,lat:r,alt:i,heading:a,speed:0,animationState:this.currentAnimState,mode:this.viewMode},this.notify()}getState(){return this.state}onStateChange(e){return this.listeners.add(e),()=>this.listeners.delete(e)}setViewMode(e){this.viewMode!==e&&(this.viewMode=e,this.state={...this.state,mode:e},this.applyModelVisibility(),this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading),this.notify())}toggleViewMode(){this.setViewMode(this.viewMode===`tpv`?`fpv`:`tpv`)}setAllowCameraControl(e){this.config.allowCameraControl=e}setHeading(e){if(!this.view)return;let{lat:n,lng:r,alt:i}=this.state;if(this.modelHeading=e,this.cameraHeading=e,this.handle&&this.character){let a=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(n),lng:(0,t.degreeToRadian)(r),height:i});this.handle.update({matrixWorld:this.composeCharacterFrame(a,e)})}this.isFreeCamera()||this.placeChaseCamera(n,r,i,e),this.state={...this.state,heading:e},this.notify()}getHeading(){return this.state.heading}setCameraPitch(e){this.config.cameraPitch=e,this.isFreeCamera()||this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading)}getCameraPitch(){return this.config.cameraPitch}setFpvPitch(e){this.config.fpvPitch=e,this.isFreeCamera()||this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading)}getFpvPitch(){return this.config.fpvPitch}setFpvHeightOffset(e){this.config.fpvHeightOffset=e,this.isFreeCamera()||this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading)}getFpvHeightOffset(){return this.config.fpvHeightOffset}dispose(){this.animId!=null&&cancelAnimationFrame(this.animId),this.animId=null,document.removeEventListener(`keydown`,this.boundKeyDown),document.removeEventListener(`keyup`,this.boundKeyUp),this.handle&&=(this.handle.delete(),null),this.modelRef=null,this.heldActions.clear(),this.listeners.clear(),this.orbitKeyHeld=!1,this.orbitLatched=!1,this.dashMultiplier=1,this.view=void 0}resolveCharacter(e){return{modelUrl:e.modelUrl,animation:e.animation,modelRotationOffset:e.modelRotationOffset?{...u,...e.modelRotationOffset}:u,modelScale:e.modelScale??p,hideModelInFpv:e.hideModelInFpv??!0,castShadow:e.castShadow??!1,receiveShadow:e.receiveShadow??!1}}buildKeyMap(e){let t=new Map,n=(e,n)=>{for(let r of e)t.set(r,n)};return n(e.forward,`forward`),n(e.backward,`backward`),n(e.turnLeft,`turnLeft`),n(e.turnRight,`turnRight`),n(e.ascend,`ascend`),n(e.descend,`descend`),n(e.dash,`dash`),n(e.orbitCamera,`orbitCamera`),n(e.toggleView,`toggleView`),t}isFreeCamera(){return this.config.allowCameraControl||this.orbitKeyHeld||this.orbitLatched}isMovementAction(e){return e===`forward`||e===`backward`||e===`turnLeft`||e===`turnRight`||e===`ascend`||e===`descend`}applyModelVisibility(){if(!this.handle||!this.character)return;let e=this.viewMode===`fpv`&&this.character.hideModelInFpv;this.handle.visible=!e}applyInitialCamera(){let{startLat:e,startLng:t,startHeight:n,startHeading:r}=this.config;this.placeChaseCamera(e,t,n,r)}placeChaseCamera(e,t,n,r){if(!this.view)return;let{cameraDistance:i,cameraPitch:a,fpvPitch:o,fpvHeightOffset:s,fpvForwardOffset:c}=this.config,l=this.viewMode===`fpv`,u=n+s;if(l){let n=m,i=c+m,a=Math.tan(o)*n,s=this.advanceLatLng(e,t,r,i);this._offset.set(-Math.sin(r)*n,-Math.cos(r)*n,a),this.view.cameraFollow(!0,{lat:s.lat,lng:s.lng,height:u-a},this._offset);return}let d=i*Math.cos(a),f=i*Math.sin(a);this._offset.set(-Math.sin(r)*d,-Math.cos(r)*d,f),this.view.cameraFollow(!0,{lat:e,lng:t,height:u},this._offset)}composeCharacterFrame(e,n){let r=(0,t.eastNorthUpToFixedFrame)(e);return this._headingMatrix.makeRotationZ(-n),this._characterFrame.multiplyMatrices(r,this._headingMatrix)}advanceLatLng(e,n,r,i){let a=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(e),lng:(0,t.degreeToRadian)(n),height:0}),o=(0,t.eastNorthUpToFixedFrame)(a);this._east.setFromMatrixColumn(o,0).normalize(),this._north.setFromMatrixColumn(o,1).normalize(),this._worldForward.copy(this._east).multiplyScalar(Math.sin(r)).addScaledVector(this._north,Math.cos(r)),a.addScaledVector(this._worldForward,i);let s=(0,t.vector3ToGeodetic)(a);return{lat:(0,t.radianToDegree)(s.lat),lng:(0,t.radianToDegree)(s.lng)}}notify(){for(let e of this.listeners)e(this.state)}onKeyDown(e){let t=e.target;if(t.tagName===`INPUT`||t.tagName===`TEXTAREA`||t.isContentEditable)return;let n=this.keyToAction.get(e.code);if(n!==void 0){if(n===`toggleView`){if(e.repeat)return;this.toggleViewMode();return}if(n===`orbitCamera`){this.orbitKeyHeld=!0,this.orbitLatched=!0;return}if(!this.movementSuppressed){if(n===`dash`){this.dashMultiplier=2.5;return}this.isMovementAction(n)&&(this.orbitLatched=!1),this.heldActions.add(n)}}}onKeyUp(e){let t=this.keyToAction.get(e.code);if(t!==void 0&&t!==`toggleView`){if(t===`orbitCamera`){this.orbitKeyHeld=!1;return}if(t===`dash`){this.dashMultiplier=1;return}this.heldActions.delete(t)}}tick=e=>{if(!this.view)return;this.animId=requestAnimationFrame(this.tick);let n=Math.min((e-this.lastTime)/1e3,.1);this.lastTime=e;let r=0,i=0,a=0;this.heldActions.has(`forward`)&&(i+=1),this.heldActions.has(`backward`)&&--i,this.heldActions.has(`turnLeft`)&&--r,this.heldActions.has(`turnRight`)&&(r+=1),this.heldActions.has(`ascend`)&&(a+=1),this.heldActions.has(`descend`)&&--a;let{moveSpeed:o,rotationSpeed:s,altSpeed:c,minAlt:l,maxAlt:u,cameraLerpSpeed:d}=this.config;r!==0&&(this.modelHeading+=(0,t.degreeToRadian)(s*r));let f=this._curPos;if(this.handle&&this.modelRef){let e=this.modelRef.getWorldPosition();if(!e)return;f.copy(e)}else f.copy((0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(this.state.lat),lng:(0,t.degreeToRadian)(this.state.lng),height:this.state.alt}));let p=(0,t.eastNorthUpToFixedFrame)(f);this._east.setFromMatrixColumn(p,0).normalize(),this._north.setFromMatrixColumn(p,1).normalize();let m=(0,t.vector3ToGeodetic)(f);this._worldForward.copy(this._east).multiplyScalar(Math.sin(this.modelHeading)).addScaledVector(this._north,Math.cos(this.modelHeading)),i!==0&&f.addScaledVector(this._worldForward,o*this.dashMultiplier*n*i);let h=(0,t.vector3ToGeodetic)(f),g=m.height+a*c*n,_=Math.max(l,Math.min(u,g)),v=(0,t.radianToDegree)(h.lat),y=(0,t.radianToDegree)(h.lng),b=_;if(this.handle&&this.character){let e=(0,t.geodeticToVector3)({lat:h.lat,lng:h.lng,height:_});this.handle.update({matrixWorld:this.composeCharacterFrame(e,this.modelHeading)})}let x=this.modelHeading-this.cameraHeading;if(x-=Math.round(x/(Math.PI*2))*(Math.PI*2),this.cameraHeading+=x*Math.min(n*d,1),this.isFreeCamera())if(this.viewMode===`fpv`){let e=this.advanceLatLng(v,y,this.cameraHeading,this.config.fpvForwardOffset);this.view.cameraFreeLook(!0,{lat:e.lat,lng:e.lng,height:b+this.config.fpvHeightOffset})}else this.view.cameraFollow(!0,{lat:v,lng:y,height:b+this.config.fpvHeightOffset});else this.placeChaseCamera(v,y,b,this.cameraHeading);let S=i!==0||r!==0||a!==0,C=S&&this.dashMultiplier>1,w=this.currentAnimState;if(this.character&&this.modelRef){let{animation:e}=this.character,t=e.walkClip??e.idleClip,n;n=C?e.dashClip:S?t:e.idleClip,n!==this.currentAnimState&&(this.modelRef.crossFadeAnimation(this.currentAnimState??``,n,e.crossfadeDuration),this.currentAnimState=n),w=n}let T={lng:y,lat:v,alt:b,heading:this.modelHeading,speed:o*this.dashMultiplier,animationState:w,mode:this.viewMode};this.hasStateChanged(this.state,T)&&(this.state=T,this.notify())};hasStateChanged(e,t){return e.lng!==t.lng||e.lat!==t.lat||e.alt!==t.alt||e.heading!==t.heading||e.speed!==t.speed||e.animationState!==t.animationState||e.mode!==t.mode}};function g(e,t,n){e.style.transform=`translate(${t}px, ${n}px)`,e.style.willChange=`transform`}var _=1e5,v=class extends t.Plugin{view;maxDistance;positions=[];projected=new Map;listeners=new Set;boundHandler;constructor(e){super(),this.maxDistance=e?.maxDistance??_,this.boundHandler=this.handlePreRender.bind(this)}async init(e,t){this.view=e,e.on(`preRender`,this.boundHandler)}setPositions(e){this.positions=e}onUpdate(e){return this.listeners.add(e),()=>this.listeners.delete(e)}dispose(){this.view&&this.view.off(`preRender`,this.boundHandler),this.listeners.clear(),this.positions=[],this.projected.clear()}worldToScreen(e,n,r){if(!this.view)return null;let i=(0,t.geodeticToVector3)({lng:(0,t.degreeToRadian)(e),lat:(0,t.degreeToRadian)(n),height:r}),a=(0,t.convertWorldToScreen)({width:this.view.screenSize.x,height:this.view.screenSize.y,pixelRatio:this.view.pixelRatio},this.view.camera.raw,i);return a?{x:a.x,y:a.y}:null}handlePreRender(){if(!this.view)return;let e=this.view.camera.positionGeographic,n=(0,t.geodeticToVector3)({lng:(0,t.degreeToRadian)(e.lng),lat:(0,t.degreeToRadian)(e.lat),height:e.height}),r=new Map;for(let e of this.positions){let i=(0,t.geodeticToVector3)({lng:(0,t.degreeToRadian)(e.lng),lat:(0,t.degreeToRadian)(e.lat),height:e.alt}),a=n.distanceTo(i);if(a>this.maxDistance)continue;let o=this.worldToScreen(e.lng,e.lat,e.alt);o&&r.set(e.id,{...o,distance:a})}this.projected=r;let i={projected:this.projected};for(let e of this.listeners)e(i)}},y=`https://api.cesium.com/v1/assets`,b=class extends t.Plugin{view;config;endpoint;constructor(e){super(),this.config=e}async init(e,t){this.view=e;let{assetId:n,accessToken:r,endpoint:i=y}=this.config,a=`${i}/${encodeURIComponent(String(n))}/endpoint?access_token=${encodeURIComponent(r)}`,o=await fetch(a);if(!o.ok)throw Error(`CesiumIonPlugin: failed to fetch asset ${n} endpoint (${o.status} ${o.statusText})`);let s=await o.json();this.endpoint={url:s.url,accessToken:s.accessToken}}addTerrain(e={}){if(!this.view||!this.endpoint)throw Error(`CesiumIonPlugin: addTerrain() must be called after view.init().`);let{castShadow:t,receiveShadow:n,show:r,showBoundingBox:i,skirt:a,skirtExaggeration:o,...s}=e,c=this.view.addSource({type:`quantized-mesh`,url:`${this.endpoint.url}{z}/{x}/{y}.terrain`,token:this.endpoint.accessToken,...s}),l=this.view.addLayer({type:`terrain`,source:c,terrain:{castShadow:t,receiveShadow:n,show:r,showBoundingBox:i,skirt:a,skirtExaggeration:o}});return l.once(`deleted`,()=>c.delete()),l}},x=/^3\.\d+\.\d+$/;function S(e){if(typeof e.tilejson!=`string`||!x.test(e.tilejson))throw Error(`TileJsonPlugin: invalid or missing "tilejson" version field (got ${JSON.stringify(e.tilejson)}).`);if(!Array.isArray(e.tiles)||e.tiles.length===0)throw Error(`TileJsonPlugin: "tiles" must be a non-empty array of URL templates.`)}var C=class extends t.Plugin{view;events=new t.EventHandler;credits=[];async init(e,t){this.view=e}on(e,t){this.events.on(e,t)}once(e,t){this.events.once(e,t)}off(e,t){this.events.off(e,t)}async addSource(e){let t=this.view;if(!t)throw Error(`TileJsonPlugin: addSource() must be called after view.init().`);let n=await this.fetchTileJson(e.url);S(n);let[r,...i]=n.tiles;i.length>0&&console.warn(`TileJsonPlugin: TileJSON at ${e.url} lists ${n.tiles.length} mirror tile endpoints; Navara sources use a single URL, so only the first is used.`);let{minzoom:a,maxzoom:o,scheme:s}=n,c=e.id,l=e.type===`raster-tile`?t.addSource({type:`raster-tile`,url:r,...c===void 0?{}:{id:c},...a===void 0?{}:{minZoom:a},...o===void 0?{}:{maxZoom:o},...s===`tms`?{tms:!0}:{}}):t.addSource({type:`vector-tile`,url:r,...c===void 0?{}:{id:c},...o===void 0?{}:{maxZoom:o}});return n.attribution&&this.addCredit(n.attribution),this.events.emit(`loaded`,{source:l,tilejson:n,attribution:n.attribution}),l}dispose(){let e=this.view?.attribution;e&&this.credits.length>0&&e.remove(this.credits.map(e=>({attributionHtml:e}))),this.credits.length=0,this.events.clear(`loaded`),this.view=void 0}async fetchTileJson(e){let t=await fetch(e);if(!t.ok)throw Error(`TileJsonPlugin: failed to fetch TileJSON from ${e} (${t.status} ${t.statusText})`);return await t.json()}addCredit(e){this.credits.includes(e)||(this.credits.push(e),this.view?.attribution?.add([{attributionHtml:e}]))}};e.CesiumIonPlugin=b,e.OverlayPlugin=v,e.PersonViewPlugin=h,e.TileJsonPlugin=C,e.moveOverlayElement=g});
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("@navaramap/three"),require("three")):typeof define==`function`&&define.amd?define([`exports`,`@navaramap/three`,`three`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.NavaraThreePlugins={},e._navaramap_three,e.three))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),l(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);var u={x:0,y:0,z:0},d={forward:[`KeyW`],backward:[`KeyS`],turnLeft:[`KeyA`],turnRight:[`KeyD`],ascend:[`ArrowUp`,`Space`],descend:[`ArrowDown`,`ControlLeft`,`ControlRight`],dash:[`ShiftLeft`,`ShiftRight`],orbitCamera:[`AltLeft`,`AltRight`],toggleView:[`KeyV`]},f={allowCameraControl:!1,initialView:`tpv`,moveSpeed:50,rotationSpeed:3,altSpeed:30,minAlt:50,maxAlt:5e3,cameraDistance:50,cameraPitch:0,cameraLerpSpeed:3,fpvForwardOffset:0,fpvHeightOffset:1,fpvPitch:0,startLat:35.6812,startLng:139.7671,startHeight:500,startHeading:Math.PI*1.3},p=3,m=1e3,h=class extends t.Plugin{view;config;character;keys;keyToAction;handle=null;modelRef=null;animId=null;lastTime=0;state;listeners=new Set;heldActions=new Set;dashMultiplier=1;orbitKeyHeld=!1;orbitLatched=!1;currentAnimState;modelHeading;cameraHeading;viewMode;movementSuppressed=!1;boundKeyDown;boundKeyUp;_curPos=new n.Vector3;_east=new n.Vector3;_north=new n.Vector3;_worldForward=new n.Vector3;_offset=new n.Vector3;_headingMatrix=new n.Matrix4;_characterFrame=new n.Matrix4;constructor(e={}){super();let{character:t,keys:n,...r}=e;this.config={...f,...r},this.character=t?this.resolveCharacter(t):null,this.keys={...d,...n},this.keyToAction=this.buildKeyMap(this.keys),this.modelHeading=this.config.startHeading,this.cameraHeading=this.config.startHeading,this.viewMode=this.config.initialView,this.currentAnimState=this.character?this.character.animation.idleClip:null,this.boundKeyDown=this.onKeyDown.bind(this),this.boundKeyUp=this.onKeyUp.bind(this)}async init(e,t){this.view=e,this.state={lng:this.config.startLng,lat:this.config.startLat,alt:this.config.startHeight,heading:this.config.startHeading,speed:0,animationState:this.currentAnimState,mode:this.viewMode}}start(){if(!this.view||this.animId!=null)return;this.animId=-1;let{startLat:e,startLng:n,startHeight:r,startHeading:i}=this.config,a=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(e),lng:(0,t.degreeToRadian)(n),height:r});if(this.modelHeading=i,this.cameraHeading=i,this.character){let{animation:e,modelRotationOffset:t,modelScale:n,castShadow:r,receiveShadow:o}=this.character;this.handle=this.view.addMesh({gltfModel:{url:this.character.modelUrl,castShadow:r,receiveShadow:o,animationEnabled:!0,animationAutoPlay:!0,animationActiveClip:e.idleClip,animationSpeed:e.speed,animationLoop:!0,animationCrossfadeDuration:e.crossfadeDuration},matrixWorld:this.composeCharacterFrame(a,i),rotation:{x:t.x,y:t.y,z:t.z},scale:{x:n,y:n,z:n}}),this.modelRef=this.handle.ref,this.modelRef.on(`load`,()=>{this.view&&(this.lastTime=performance.now(),this.animId=requestAnimationFrame(this.tick))})}else this.lastTime=performance.now(),this.animId=requestAnimationFrame(this.tick);this.applyInitialCamera(),this.applyModelVisibility(),document.addEventListener(`keydown`,this.boundKeyDown),document.addEventListener(`keyup`,this.boundKeyUp)}teleport(e){if(!this.view)return;let{lng:n,lat:r,alt:i}=e,a=e.heading==null?this.cameraHeading:e.heading;if(this.handle&&this.character){let e=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(r),lng:(0,t.degreeToRadian)(n),height:i});this.handle.update({matrixWorld:this.composeCharacterFrame(e,a)})}this.modelHeading=a,this.cameraHeading=a,this.placeChaseCamera(r,n,i,a),this.state={lng:n,lat:r,alt:i,heading:a,speed:0,animationState:this.currentAnimState,mode:this.viewMode},this.notify()}getState(){return this.state}onStateChange(e){return this.listeners.add(e),()=>this.listeners.delete(e)}setViewMode(e){this.viewMode!==e&&(this.viewMode=e,this.state={...this.state,mode:e},this.applyModelVisibility(),this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading),this.notify())}toggleViewMode(){this.setViewMode(this.viewMode===`tpv`?`fpv`:`tpv`)}setAllowCameraControl(e){this.config.allowCameraControl=e}setHeading(e){if(!this.view)return;let{lat:n,lng:r,alt:i}=this.state;if(this.modelHeading=e,this.cameraHeading=e,this.handle&&this.character){let a=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(n),lng:(0,t.degreeToRadian)(r),height:i});this.handle.update({matrixWorld:this.composeCharacterFrame(a,e)})}this.isFreeCamera()||this.placeChaseCamera(n,r,i,e),this.state={...this.state,heading:e},this.notify()}getHeading(){return this.state.heading}setCameraPitch(e){this.config.cameraPitch=e,this.isFreeCamera()||this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading)}getCameraPitch(){return this.config.cameraPitch}setFpvPitch(e){this.config.fpvPitch=e,this.isFreeCamera()||this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading)}getFpvPitch(){return this.config.fpvPitch}setFpvHeightOffset(e){this.config.fpvHeightOffset=e,this.isFreeCamera()||this.placeChaseCamera(this.state.lat,this.state.lng,this.state.alt,this.cameraHeading)}getFpvHeightOffset(){return this.config.fpvHeightOffset}dispose(){this.animId!=null&&cancelAnimationFrame(this.animId),this.animId=null,document.removeEventListener(`keydown`,this.boundKeyDown),document.removeEventListener(`keyup`,this.boundKeyUp),this.handle&&=(this.handle.delete(),null),this.modelRef=null,this.heldActions.clear(),this.listeners.clear(),this.orbitKeyHeld=!1,this.orbitLatched=!1,this.dashMultiplier=1,this.view=void 0}resolveCharacter(e){return{modelUrl:e.modelUrl,animation:e.animation,modelRotationOffset:e.modelRotationOffset?{...u,...e.modelRotationOffset}:u,modelScale:e.modelScale??p,hideModelInFpv:e.hideModelInFpv??!0,castShadow:e.castShadow??!1,receiveShadow:e.receiveShadow??!1}}buildKeyMap(e){let t=new Map,n=(e,n)=>{for(let r of e)t.set(r,n)};return n(e.forward,`forward`),n(e.backward,`backward`),n(e.turnLeft,`turnLeft`),n(e.turnRight,`turnRight`),n(e.ascend,`ascend`),n(e.descend,`descend`),n(e.dash,`dash`),n(e.orbitCamera,`orbitCamera`),n(e.toggleView,`toggleView`),t}isFreeCamera(){return this.config.allowCameraControl||this.orbitKeyHeld||this.orbitLatched}isMovementAction(e){return e===`forward`||e===`backward`||e===`turnLeft`||e===`turnRight`||e===`ascend`||e===`descend`}applyModelVisibility(){if(!this.handle||!this.character)return;let e=this.viewMode===`fpv`&&this.character.hideModelInFpv;this.handle.visible=!e}applyInitialCamera(){let{startLat:e,startLng:t,startHeight:n,startHeading:r}=this.config;this.placeChaseCamera(e,t,n,r)}placeChaseCamera(e,t,n,r){if(!this.view)return;let{cameraDistance:i,cameraPitch:a,fpvPitch:o,fpvHeightOffset:s,fpvForwardOffset:c}=this.config,l=this.viewMode===`fpv`,u=n+s;if(l){let n=m,i=c+m,a=Math.tan(o)*n,s=this.advanceLatLng(e,t,r,i);this._offset.set(-Math.sin(r)*n,-Math.cos(r)*n,a),this.view.cameraFollow(!0,{lat:s.lat,lng:s.lng,height:u-a},this._offset);return}let d=i*Math.cos(a),f=i*Math.sin(a);this._offset.set(-Math.sin(r)*d,-Math.cos(r)*d,f),this.view.cameraFollow(!0,{lat:e,lng:t,height:u},this._offset)}composeCharacterFrame(e,n){let r=(0,t.eastNorthUpToFixedFrame)(e);return this._headingMatrix.makeRotationZ(-n),this._characterFrame.multiplyMatrices(r,this._headingMatrix)}advanceLatLng(e,n,r,i){let a=(0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(e),lng:(0,t.degreeToRadian)(n),height:0}),o=(0,t.eastNorthUpToFixedFrame)(a);this._east.setFromMatrixColumn(o,0).normalize(),this._north.setFromMatrixColumn(o,1).normalize(),this._worldForward.copy(this._east).multiplyScalar(Math.sin(r)).addScaledVector(this._north,Math.cos(r)),a.addScaledVector(this._worldForward,i);let s=(0,t.vector3ToGeodetic)(a);return{lat:(0,t.radianToDegree)(s.lat),lng:(0,t.radianToDegree)(s.lng)}}notify(){for(let e of this.listeners)e(this.state)}onKeyDown(e){let t=e.target;if(t.tagName===`INPUT`||t.tagName===`TEXTAREA`||t.isContentEditable)return;let n=this.keyToAction.get(e.code);if(n!==void 0){if(n===`toggleView`){if(e.repeat)return;this.toggleViewMode();return}if(n===`orbitCamera`){this.orbitKeyHeld=!0,this.orbitLatched=!0;return}if(!this.movementSuppressed){if(n===`dash`){this.dashMultiplier=2.5;return}this.isMovementAction(n)&&(this.orbitLatched=!1),this.heldActions.add(n)}}}onKeyUp(e){let t=this.keyToAction.get(e.code);if(t!==void 0&&t!==`toggleView`){if(t===`orbitCamera`){this.orbitKeyHeld=!1;return}if(t===`dash`){this.dashMultiplier=1;return}this.heldActions.delete(t)}}tick=e=>{if(!this.view)return;this.animId=requestAnimationFrame(this.tick);let n=Math.min((e-this.lastTime)/1e3,.1);this.lastTime=e;let r=0,i=0,a=0;this.heldActions.has(`forward`)&&(i+=1),this.heldActions.has(`backward`)&&--i,this.heldActions.has(`turnLeft`)&&--r,this.heldActions.has(`turnRight`)&&(r+=1),this.heldActions.has(`ascend`)&&(a+=1),this.heldActions.has(`descend`)&&--a;let{moveSpeed:o,rotationSpeed:s,altSpeed:c,minAlt:l,maxAlt:u,cameraLerpSpeed:d}=this.config;r!==0&&(this.modelHeading+=(0,t.degreeToRadian)(s*r));let f=this._curPos;if(this.handle&&this.modelRef){let e=this.modelRef.getWorldPosition();if(!e)return;f.copy(e)}else f.copy((0,t.geodeticToVector3)({lat:(0,t.degreeToRadian)(this.state.lat),lng:(0,t.degreeToRadian)(this.state.lng),height:this.state.alt}));let p=(0,t.eastNorthUpToFixedFrame)(f);this._east.setFromMatrixColumn(p,0).normalize(),this._north.setFromMatrixColumn(p,1).normalize();let m=(0,t.vector3ToGeodetic)(f);this._worldForward.copy(this._east).multiplyScalar(Math.sin(this.modelHeading)).addScaledVector(this._north,Math.cos(this.modelHeading)),i!==0&&f.addScaledVector(this._worldForward,o*this.dashMultiplier*n*i);let h=(0,t.vector3ToGeodetic)(f),g=m.height+a*c*n,_=Math.max(l,Math.min(u,g)),v=(0,t.radianToDegree)(h.lat),y=(0,t.radianToDegree)(h.lng),b=_;if(this.handle&&this.character){let e=(0,t.geodeticToVector3)({lat:h.lat,lng:h.lng,height:_});this.handle.update({matrixWorld:this.composeCharacterFrame(e,this.modelHeading)})}let x=this.modelHeading-this.cameraHeading;if(x-=Math.round(x/(Math.PI*2))*(Math.PI*2),this.cameraHeading+=x*Math.min(n*d,1),this.isFreeCamera())if(this.viewMode===`fpv`){let e=this.advanceLatLng(v,y,this.cameraHeading,this.config.fpvForwardOffset);this.view.cameraFreeLook(!0,{lat:e.lat,lng:e.lng,height:b+this.config.fpvHeightOffset})}else this.view.cameraFollow(!0,{lat:v,lng:y,height:b+this.config.fpvHeightOffset});else this.placeChaseCamera(v,y,b,this.cameraHeading);let S=i!==0||r!==0||a!==0,C=S&&this.dashMultiplier>1,w=this.currentAnimState;if(this.character&&this.modelRef){let{animation:e}=this.character,t=e.walkClip??e.idleClip,n;n=C?e.dashClip:S?t:e.idleClip,n!==this.currentAnimState&&(this.modelRef.crossFadeAnimation(this.currentAnimState??``,n,e.crossfadeDuration),this.currentAnimState=n),w=n}let T={lng:y,lat:v,alt:b,heading:this.modelHeading,speed:o*this.dashMultiplier,animationState:w,mode:this.viewMode};this.hasStateChanged(this.state,T)&&(this.state=T,this.notify())};hasStateChanged(e,t){return e.lng!==t.lng||e.lat!==t.lat||e.alt!==t.alt||e.heading!==t.heading||e.speed!==t.speed||e.animationState!==t.animationState||e.mode!==t.mode}};function g(e,t,n){e.style.transform=`translate(${t}px, ${n}px)`,e.style.willChange=`transform`}var _=1e5,v=class extends t.Plugin{view;maxDistance;positions=[];projected=new Map;listeners=new Set;boundHandler;constructor(e){super(),this.maxDistance=e?.maxDistance??_,this.boundHandler=this.handlePreRender.bind(this)}async init(e,t){this.view=e,e.on(`preRender`,this.boundHandler)}setPositions(e){this.positions=e}onUpdate(e){return this.listeners.add(e),()=>this.listeners.delete(e)}dispose(){this.view&&this.view.off(`preRender`,this.boundHandler),this.listeners.clear(),this.positions=[],this.projected.clear()}worldToScreen(e,n,r){if(!this.view)return null;let i=(0,t.geodeticToVector3)({lng:(0,t.degreeToRadian)(e),lat:(0,t.degreeToRadian)(n),height:r}),a=(0,t.convertWorldToScreen)({width:this.view.screenSize.x,height:this.view.screenSize.y,pixelRatio:this.view.pixelRatio},this.view.camera.raw,i);return a?{x:a.x,y:a.y}:null}handlePreRender(){if(!this.view)return;let e=this.view.camera.positionGeographic,n=(0,t.geodeticToVector3)({lng:(0,t.degreeToRadian)(e.lng),lat:(0,t.degreeToRadian)(e.lat),height:e.height}),r=new Map;for(let e of this.positions){let i=(0,t.geodeticToVector3)({lng:(0,t.degreeToRadian)(e.lng),lat:(0,t.degreeToRadian)(e.lat),height:e.alt}),a=n.distanceTo(i);if(a>this.maxDistance)continue;let o=this.worldToScreen(e.lng,e.lat,e.alt);o&&r.set(e.id,{...o,distance:a})}this.projected=r;let i={projected:this.projected};for(let e of this.listeners)e(i)}},y=`https://api.cesium.com/v1/assets`,b=class extends t.Plugin{view;config;endpoint;constructor(e){super(),this.config=e}async init(e,t){this.view=e;let{assetId:n,accessToken:r,endpoint:i=y}=this.config,a=`${i}/${encodeURIComponent(String(n))}/endpoint?access_token=${encodeURIComponent(r)}`,o=await fetch(a);if(!o.ok)throw Error(`CesiumIonPlugin: failed to fetch asset ${n} endpoint (${o.status} ${o.statusText})`);let s=await o.json();this.endpoint={url:s.url,accessToken:s.accessToken}}addTerrain(e={}){if(!this.view||!this.endpoint)throw Error(`CesiumIonPlugin: addTerrain() must be called after view.init().`);let{castShadow:t,receiveShadow:n,show:r,showBoundingBox:i,skirt:a,skirtExaggeration:o,...s}=e,c=this.view.addSource({type:`quantized-mesh`,url:`${this.endpoint.url}{z}/{x}/{y}.terrain`,token:this.endpoint.accessToken,...s}),l=this.view.addLayer({type:`terrain`,source:c,terrain:{castShadow:t,receiveShadow:n,show:r,showBoundingBox:i,skirt:a,skirtExaggeration:o}});return l.once(`deleted`,()=>c.delete()),l}},x=/^3\.\d+\.\d+$/,S=512,C=`mapbox`;function w(e){if(typeof e.tilejson!=`string`||!x.test(e.tilejson))throw Error(`TileJsonPlugin: invalid or missing "tilejson" version field (got ${JSON.stringify(e.tilejson)}).`);if(!Array.isArray(e.tiles)||e.tiles.length===0)throw Error(`TileJsonPlugin: "tiles" must be a non-empty array of URL templates.`)}function T(e){switch(e){case`mapbox`:return(0,t.MAPBOX_ELEVATION_DECODER)();case`terrarium`:return(0,t.TERRARIUM_ELEVATION_DECODER)();default:throw Error(`TileJsonPlugin: unsupported raster-dem encoding ${JSON.stringify(e)}. Supported encodings: "mapbox", "terrarium".`)}}var E=class extends t.Plugin{view;events=new t.EventHandler;credits=[];async init(e,t){this.view=e}on(e,t){this.events.on(e,t)}once(e,t){this.events.once(e,t)}off(e,t){this.events.off(e,t)}async addSource(e){let t=this.view;if(!t)throw Error(`TileJsonPlugin: addSource() must be called after view.init().`);let n=await this.fetchTileJson(e.url);w(n);let[r,...i]=n.tiles;i.length>0&&console.warn(`TileJsonPlugin: TileJSON at ${e.url} lists ${n.tiles.length} mirror tile endpoints; Navara sources use a single URL, so only the first is used.`);let a=e.minzoom??n.minzoom,o=e.maxzoom??n.maxzoom,s=e.scheme??n.scheme,c=e.id,l={url:r,...c===void 0?{}:{id:c},...o===void 0?{}:{maxZoom:o}},u={...l,...a===void 0?{}:{minZoom:a},...s===`tms`?{tms:!0}:{}},d;return d=e.type===`raster-tile`?t.addSource({type:`raster-tile`,...u}):e.type===`raster-dem`?t.addSource({type:`raster-dem`,...u,tileSize:e.tileSize??n.tileSize??S,elevationDecoder:T(e.encoding??n.encoding??C)}):t.addSource({type:`vector-tile`,...l}),n.attribution&&this.addCredit(n.attribution),this.events.emit(`loaded`,{source:d,tilejson:n,attribution:n.attribution}),d}dispose(){let e=this.view?.attribution;e&&this.credits.length>0&&e.remove(this.credits.map(e=>({attributionHtml:e}))),this.credits.length=0,this.events.clear(`loaded`),this.view=void 0}async fetchTileJson(e){let t=await fetch(e);if(!t.ok)throw Error(`TileJsonPlugin: failed to fetch TileJSON from ${e} (${t.status} ${t.statusText})`);return await t.json()}addCredit(e){this.credits.includes(e)||(this.credits.push(e),this.view?.attribution?.add([{attributionHtml:e}]))}};e.CesiumIonPlugin=b,e.OverlayPlugin=v,e.PersonViewPlugin=h,e.TileJsonPlugin=E,e.moveOverlayElement=g});
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@navaramap/three-plugins",
3
3
  "type": "module",
4
- "version": "0.0.4",
4
+ "version": "0.0.5",
5
5
  "main": "dist/index.umd.cjs",
6
6
  "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -20,14 +20,14 @@
20
20
  },
21
21
  "peerDependencies": {
22
22
  "three": ">=0.183.0",
23
- "@navaramap/three": "0.0.4",
24
- "@navaramap/three-default-plugin": "0.0.4"
23
+ "@navaramap/three": "0.0.5",
24
+ "@navaramap/three-default-plugin": "0.0.5"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/three": "0.185.1",
28
28
  "three": "0.185.1",
29
- "@navaramap/three-default-plugin": "0.0.4",
30
- "@navaramap/three": "0.0.4"
29
+ "@navaramap/three": "0.0.5",
30
+ "@navaramap/three-default-plugin": "0.0.5"
31
31
  },
32
32
  "scripts": {
33
33
  "type-check": "tsc && tsc -p tsconfig.node.json",
@@ -3,13 +3,17 @@ import { describe, it, expect, vi, afterEach } from "vitest";
3
3
  import { TileJsonPlugin, type TileJson } from "./TileJsonPlugin";
4
4
 
5
5
  // Importing the real @navaramap/three touches WASM/os at module load, which fails
6
- // in the test environment. TileJsonPlugin only needs `Plugin` and `EventHandler`
7
- // as runtime values (everything else it imports from there is type-only), so
8
- // stub the module. The EventHandler stub mirrors the real on/once/off/emit/clear.
6
+ // in the test environment. TileJsonPlugin only needs `Plugin`, `EventHandler`,
7
+ // and the elevation decoder factories as runtime values (everything else it
8
+ // imports from there is type-only), so stub the module. The decoder factories
9
+ // return sentinels the tests can assert on; the EventHandler stub mirrors the
10
+ // real on/once/off/emit/clear.
9
11
  /* eslint-disable @typescript-eslint/no-extraneous-class */
10
12
  vi.mock("@navaramap/three", () => ({
11
13
  default: class ThreeView {},
12
14
  Plugin: class Plugin {},
15
+ MAPBOX_ELEVATION_DECODER: () => ({ decoder: "mapbox" }),
16
+ TERRARIUM_ELEVATION_DECODER: () => ({ decoder: "terrarium" }),
13
17
  EventHandler: class EventHandler {
14
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
19
  private listeners = new Map<string, Set<(...a: any[]) => void>>();
@@ -192,6 +196,118 @@ describe("TileJsonPlugin.addSource", () => {
192
196
  expect(desc).not.toHaveProperty("tms");
193
197
  });
194
198
 
199
+ it("creates a raster-dem source with MapLibre defaults (tileSize 512, mapbox encoding)", async () => {
200
+ const { view } = makeFakeView();
201
+ const plugin = initPlugin(view);
202
+ stubTileJson({
203
+ tilejson: "3.0.0",
204
+ tiles: ["https://a/{z}/{x}/{y}.png"],
205
+ minzoom: 2,
206
+ maxzoom: 12,
207
+ } satisfies TileJson);
208
+
209
+ await plugin.addSource({ type: "raster-dem", id: "dem", url: DOC_URL });
210
+
211
+ expect(view.addSource).toHaveBeenCalledWith({
212
+ type: "raster-dem",
213
+ url: "https://a/{z}/{x}/{y}.png",
214
+ id: "dem",
215
+ minZoom: 2,
216
+ maxZoom: 12,
217
+ tileSize: 512,
218
+ elevationDecoder: { decoder: "mapbox" },
219
+ });
220
+ });
221
+
222
+ it("honors the document's MapLibre-compatible tileSize/encoding for raster-dem", async () => {
223
+ const { view } = makeFakeView();
224
+ const plugin = initPlugin(view);
225
+ stubTileJson({
226
+ tilejson: "3.0.0",
227
+ tiles: ["https://a/{z}/{x}/{y}.png"],
228
+ tileSize: 256,
229
+ encoding: "terrarium",
230
+ } satisfies TileJson);
231
+
232
+ await plugin.addSource({ type: "raster-dem", url: DOC_URL });
233
+
234
+ expect(view.addSource).toHaveBeenCalledWith(
235
+ expect.objectContaining({
236
+ tileSize: 256,
237
+ elevationDecoder: { decoder: "terrarium" },
238
+ }),
239
+ );
240
+ });
241
+
242
+ it("rejects an unsupported raster-dem encoding", async () => {
243
+ const { view } = makeFakeView();
244
+ const plugin = initPlugin(view);
245
+ stubTileJson({
246
+ tilejson: "3.0.0",
247
+ tiles: ["https://a/{z}/{x}/{y}.png"],
248
+ encoding: "custom" as never,
249
+ } satisfies TileJson);
250
+
251
+ await expect(
252
+ plugin.addSource({ type: "raster-dem", url: DOC_URL }),
253
+ ).rejects.toThrow(/unsupported raster-dem encoding/);
254
+ expect(view.addSource).not.toHaveBeenCalled();
255
+ });
256
+
257
+ it("prefers description-level fields over the fetched document", async () => {
258
+ const { view } = makeFakeView();
259
+ const plugin = initPlugin(view);
260
+ stubTileJson({
261
+ tilejson: "3.0.0",
262
+ tiles: ["https://a/{z}/{x}/{y}.png"],
263
+ minzoom: 0,
264
+ maxzoom: 10,
265
+ scheme: "xyz",
266
+ tileSize: 512,
267
+ encoding: "mapbox",
268
+ } satisfies TileJson);
269
+
270
+ await plugin.addSource({
271
+ type: "raster-dem",
272
+ url: DOC_URL,
273
+ minzoom: 3,
274
+ maxzoom: 15,
275
+ scheme: "tms",
276
+ tileSize: 256,
277
+ encoding: "terrarium",
278
+ });
279
+
280
+ expect(view.addSource).toHaveBeenCalledWith({
281
+ type: "raster-dem",
282
+ url: "https://a/{z}/{x}/{y}.png",
283
+ minZoom: 3,
284
+ maxZoom: 15,
285
+ tms: true,
286
+ tileSize: 256,
287
+ elevationDecoder: { decoder: "terrarium" },
288
+ });
289
+ });
290
+
291
+ it("applies description-level fields even when the document omits them", async () => {
292
+ const { view } = makeFakeView();
293
+ const plugin = initPlugin(view);
294
+ stubTileJson({
295
+ tilejson: "3.0.0",
296
+ tiles: ["https://a/{z}/{x}/{y}.png"],
297
+ } satisfies TileJson);
298
+
299
+ await plugin.addSource({
300
+ type: "raster-tile",
301
+ url: DOC_URL,
302
+ minzoom: 4,
303
+ maxzoom: 8,
304
+ });
305
+
306
+ expect(view.addSource).toHaveBeenCalledWith(
307
+ expect.objectContaining({ minZoom: 4, maxZoom: 8 }),
308
+ );
309
+ });
310
+
195
311
  it("throws when the TileJSON fetch fails", async () => {
196
312
  const { view } = makeFakeView();
197
313
  const plugin = initPlugin(view);
@@ -6,7 +6,10 @@
6
6
  * document and registers it as a single Navara source. {@link TileJsonPlugin.addSource}
7
7
  * mirrors {@link ThreeView.addSource}: a discriminated `type` plus an optional
8
8
  * `id`, with `url` pointing at the TileJSON document. The plugin derives the tile
9
- * URL, `minzoom`/`maxzoom`, and `scheme` from the document.
9
+ * URL, `minzoom`/`maxzoom`, and `scheme` from the document, plus — for
10
+ * `raster-dem` sources — the MapLibre-compatible `tileSize` and `encoding`
11
+ * fields. Each derived field can also be set on the description itself, which
12
+ * takes precedence over the document.
10
13
  *
11
14
  * Each document's `attribution` is surfaced automatically through the view's
12
15
  * built-in attribution UI (`view.attribution`). Callers who want a custom credit
@@ -36,6 +39,16 @@
36
39
  * view.addLayer({ type: "raster", source });
37
40
  * // ...or directly by the id passed above.
38
41
  * view.addLayer({ type: "raster", source: "basemap" });
42
+ *
43
+ * // Raster-DEM terrain. The MapLibre-compatible `tileSize`/`encoding` fields
44
+ * // are honored from the document (or the description below), defaulting to
45
+ * // MapLibre's 512 / "mapbox".
46
+ * const dem = await tilejson.addSource({
47
+ * type: "raster-dem",
48
+ * url: "https://example.com/terrain.json",
49
+ * encoding: "terrarium", // takes precedence over the document
50
+ * });
51
+ * view.addLayer({ type: "terrain", source: dem, terrain: {} });
39
52
  * ```
40
53
  *
41
54
  * ### Custom attribution UI
@@ -55,6 +68,8 @@
55
68
  import ThreeView, {
56
69
  Plugin,
57
70
  EventHandler,
71
+ MAPBOX_ELEVATION_DECODER,
72
+ TERRARIUM_ELEVATION_DECODER,
58
73
  type Source,
59
74
  type ViewContext,
60
75
  } from "@navaramap/three";
@@ -65,6 +80,17 @@ type View = ThreeView<DefaultDescriptions>;
65
80
  /** Matches a TileJSON version such as "3.0.0" (major.minor.patch). */
66
81
  const TILEJSON_VERSION = /^3\.\d+\.\d+$/;
67
82
 
83
+ /** MapLibre's raster-dem defaults, applied when neither the description nor the document sets them. */
84
+ const DEFAULT_DEM_TILE_SIZE = 512;
85
+ const DEFAULT_DEM_ENCODING: TileJsonDemEncoding = "mapbox";
86
+
87
+ /**
88
+ * How a `raster-dem` document's RGB tiles encode elevation, matching MapLibre's
89
+ * `encoding` values. MapLibre's `"custom"` (free-form decode factors) is not
90
+ * supported.
91
+ */
92
+ export type TileJsonDemEncoding = "mapbox" | "terrarium";
93
+
68
94
  /**
69
95
  * The subset of a TileJSON 3.0.0 document this plugin consumes. Other spec
70
96
  * fields (`bounds`, `center`, `grids`, ...) are ignored.
@@ -82,24 +108,34 @@ export type TileJson = {
82
108
  maxzoom?: number;
83
109
  /** Tiling scheme. `"tms"` flips the Y axis (raster sources only). @defaultValue "xyz" */
84
110
  scheme?: "xyz" | "tms";
111
+ /**
112
+ * Tile size in pixels. Not part of the TileJSON spec, but emitted by
113
+ * MapLibre-oriented tile servers; applied to `raster-dem` sources only.
114
+ * @defaultValue 512
115
+ */
116
+ tileSize?: number;
117
+ /**
118
+ * Elevation encoding of DEM tiles. Not part of the TileJSON spec, but
119
+ * emitted by MapLibre-oriented tile servers; applied to `raster-dem` sources
120
+ * only.
121
+ * @defaultValue "mapbox"
122
+ */
123
+ encoding?: TileJsonDemEncoding;
85
124
  };
86
125
 
87
126
  /**
88
127
  * Which Navara source type a TileJSON document is materialized into. Declared by
89
128
  * the caller because TileJSON has no field that reliably distinguishes raster
90
- * imagery from vector tiles.
129
+ * imagery from vector or elevation tiles.
91
130
  */
92
- export type TileJsonSourceType = "raster-tile" | "vector-tile";
131
+ export type TileJsonSourceType = "raster-tile" | "vector-tile" | "raster-dem";
93
132
 
94
133
  /**
95
- * Describes a TileJSON tile source to register via {@link TileJsonPlugin.addSource}.
96
- * Mirrors the shape of {@link ThreeView.addSource}, except `url` is the TileJSON
97
- * document URL (not a tile template) — the plugin fetches it and derives the tile
98
- * URL, zoom range, and attribution from the document.
134
+ * Fields shared by every {@link TileJsonSourceDescription} variant. Alongside
135
+ * `url` and `id`, it accepts the same optional fields the TileJSON document
136
+ * carries; a field set here takes precedence over the fetched document's value.
99
137
  */
100
- export type TileJsonSourceDescription = {
101
- /** Navara source type to create, as in `addSource`. */
102
- type: TileJsonSourceType;
138
+ type TileJsonSourceDescriptionBase = {
103
139
  /** URL of the TileJSON 3.0.0 document to fetch and expand. */
104
140
  url: string;
105
141
  /**
@@ -108,8 +144,49 @@ export type TileJsonSourceDescription = {
108
144
  * handle. When omitted, the engine generates one.
109
145
  */
110
146
  id?: string;
147
+ /** Overrides the document's `minzoom` when set. */
148
+ minzoom?: number;
149
+ /** Overrides the document's `maxzoom` when set. */
150
+ maxzoom?: number;
151
+ /** Overrides the document's `scheme` when set. */
152
+ scheme?: "xyz" | "tms";
111
153
  };
112
154
 
155
+ /** Registers a TileJSON document as a `raster-tile` (imagery) source. */
156
+ export type TileJsonRasterTileSourceDescription =
157
+ TileJsonSourceDescriptionBase & {
158
+ type: "raster-tile";
159
+ };
160
+
161
+ /** Registers a TileJSON document as a `vector-tile` (MVT) source. */
162
+ export type TileJsonVectorTileSourceDescription =
163
+ TileJsonSourceDescriptionBase & {
164
+ type: "vector-tile";
165
+ };
166
+
167
+ /** Registers a TileJSON document as a `raster-dem` (elevation) source. */
168
+ export type TileJsonRasterDemSourceDescription =
169
+ TileJsonSourceDescriptionBase & {
170
+ type: "raster-dem";
171
+ /** Overrides the document's `tileSize` when set. @defaultValue 512 */
172
+ tileSize?: number;
173
+ /** Overrides the document's `encoding` when set. @defaultValue "mapbox" */
174
+ encoding?: TileJsonDemEncoding;
175
+ };
176
+
177
+ /**
178
+ * Describes a TileJSON tile source to register via {@link TileJsonPlugin.addSource}.
179
+ * Mirrors the shape of {@link ThreeView.addSource}, except `url` is the TileJSON
180
+ * document URL (not a tile template) — the plugin fetches it and derives the tile
181
+ * URL, zoom range, and attribution from the document. Document-derived fields
182
+ * (`minzoom`, `maxzoom`, `scheme` — plus `tileSize` and `encoding` for
183
+ * `raster-dem`) can also be set here to take precedence over the document.
184
+ */
185
+ export type TileJsonSourceDescription =
186
+ | TileJsonRasterTileSourceDescription
187
+ | TileJsonVectorTileSourceDescription
188
+ | TileJsonRasterDemSourceDescription;
189
+
113
190
  /** Detail passed to the {@link TileJsonPlugin} `loaded` event. */
114
191
  export type TileJsonLoadedEvent = {
115
192
  /** The Navara source created for the document. */
@@ -153,6 +230,26 @@ function validateTileJson(doc: TileJson): void {
153
230
  }
154
231
  }
155
232
 
233
+ /**
234
+ * Map a MapLibre `encoding` value to the engine's elevation decoder. Throws for
235
+ * unknown encodings (including MapLibre's `"custom"`, whose free-form decode
236
+ * factors have no TileJSON representation) so a raster-dem source is never
237
+ * silently created with a wrong decoder.
238
+ */
239
+ function resolveElevationDecoder(encoding: TileJsonDemEncoding) {
240
+ switch (encoding) {
241
+ case "mapbox":
242
+ return MAPBOX_ELEVATION_DECODER();
243
+ case "terrarium":
244
+ return TERRARIUM_ELEVATION_DECODER();
245
+ default:
246
+ throw new Error(
247
+ `TileJsonPlugin: unsupported raster-dem encoding ${JSON.stringify(encoding)}. ` +
248
+ 'Supported encodings: "mapbox", "terrarium".',
249
+ );
250
+ }
251
+ }
252
+
156
253
  export class TileJsonPlugin extends Plugin<View, ViewContext> {
157
254
  private view?: View;
158
255
  private readonly events = new EventHandler<TileJsonPluginEventMap>();
@@ -195,9 +292,13 @@ export class TileJsonPlugin extends Plugin<View, ViewContext> {
195
292
  *
196
293
  * `desc.url` is the TileJSON document URL; the plugin fetches it, then creates
197
294
  * one source of the requested `desc.type` using the document's first tile
198
- * endpoint, `minzoom`/`maxzoom`, and `scheme`. The document's `attribution` is
199
- * surfaced through the view's built-in attribution UI (`view.attribution`), and
200
- * a `loaded` event is emitted carrying the source, document, and attribution.
295
+ * endpoint, `minzoom`/`maxzoom`, and `scheme` — plus, for `raster-dem`, the
296
+ * MapLibre-compatible `tileSize` and `encoding` fields (defaulting to
297
+ * MapLibre's 512 and `"mapbox"`). Each of those fields can also be set on
298
+ * `desc` itself, taking precedence over the document. The document's
299
+ * `attribution` is surfaced through the view's built-in attribution UI
300
+ * (`view.attribution`), and a `loaded` event is emitted carrying the source,
301
+ * document, and attribution.
201
302
  *
202
303
  * A TileJSON `tiles` array lists mirror endpoints for the same tileset (as in
203
304
  * MapLibre, which shards requests across them). Navara sources take a single
@@ -228,26 +329,38 @@ export class TileJsonPlugin extends Plugin<View, ViewContext> {
228
329
  );
229
330
  }
230
331
 
231
- const { minzoom, maxzoom, scheme } = doc;
332
+ // Description-level fields take precedence over the fetched document.
333
+ const minzoom = desc.minzoom ?? doc.minzoom;
334
+ const maxzoom = desc.maxzoom ?? doc.maxzoom;
335
+ const scheme = desc.scheme ?? doc.scheme;
232
336
  const id = desc.id;
233
- const source =
234
- desc.type === "raster-tile"
235
- ? view.addSource({
236
- type: "raster-tile",
237
- url,
238
- ...(id !== undefined ? { id } : {}),
239
- ...(minzoom !== undefined ? { minZoom: minzoom } : {}),
240
- ...(maxzoom !== undefined ? { maxZoom: maxzoom } : {}),
241
- ...(scheme === "tms" ? { tms: true } : {}),
242
- })
243
- : // vector-tile sources have no minZoom / tms fields in the engine, so
244
- // only maxzoom carries over.
245
- view.addSource({
246
- type: "vector-tile",
247
- url,
248
- ...(id !== undefined ? { id } : {}),
249
- ...(maxzoom !== undefined ? { maxZoom: maxzoom } : {}),
250
- });
337
+ const shared = {
338
+ url,
339
+ ...(id !== undefined ? { id } : {}),
340
+ ...(maxzoom !== undefined ? { maxZoom: maxzoom } : {}),
341
+ };
342
+ const rasterShared = {
343
+ ...shared,
344
+ ...(minzoom !== undefined ? { minZoom: minzoom } : {}),
345
+ ...(scheme === "tms" ? { tms: true } : {}),
346
+ };
347
+ let source: Source;
348
+ if (desc.type === "raster-tile") {
349
+ source = view.addSource({ type: "raster-tile", ...rasterShared });
350
+ } else if (desc.type === "raster-dem") {
351
+ source = view.addSource({
352
+ type: "raster-dem",
353
+ ...rasterShared,
354
+ tileSize: desc.tileSize ?? doc.tileSize ?? DEFAULT_DEM_TILE_SIZE,
355
+ elevationDecoder: resolveElevationDecoder(
356
+ desc.encoding ?? doc.encoding ?? DEFAULT_DEM_ENCODING,
357
+ ),
358
+ });
359
+ } else {
360
+ // vector-tile sources have no minZoom / tms fields in the engine, so
361
+ // only maxzoom carries over.
362
+ source = view.addSource({ type: "vector-tile", ...shared });
363
+ }
251
364
 
252
365
  if (doc.attribution) {
253
366
  this.addCredit(doc.attribution);
package/src/index.ts CHANGED
@@ -28,8 +28,12 @@ export {
28
28
  export {
29
29
  TileJsonPlugin,
30
30
  type TileJson,
31
+ type TileJsonDemEncoding,
31
32
  type TileJsonSourceType,
32
33
  type TileJsonSourceDescription,
34
+ type TileJsonRasterTileSourceDescription,
35
+ type TileJsonVectorTileSourceDescription,
36
+ type TileJsonRasterDemSourceDescription,
33
37
  type TileJsonLoadedEvent,
34
38
  type TileJsonPluginEventMap,
35
39
  } from "./TileJsonPlugin";