@tomtom-org/maps-sdk-plugin-landmarks-3d 0.0.13 → 0.1.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.
package/dist/index.d.ts CHANGED
@@ -1,17 +1,73 @@
1
- import { TomTomMap } from "@tomtom-org/maps-sdk/map";
1
+ import { KnobConfigOf, KnobEntry, KnobRange, KnobValueOf, TomTomMap } from "@tomtom-org/maps-sdk/map";
2
2
  import { CustomLayerInterface, CustomRenderMethodInput, Map } from "maplibre-gl";
3
3
  import { AmbientLight, Camera, DirectionalLight, Group, Mesh, Scene, ShaderMaterial, Texture, WebGLRenderer } from "three";
4
- //#region src/displayMode.d.ts
4
+ //#region src/knobCatalogue.d.ts
5
+ declare const landmarks3DKnobDefinitions: {
6
+ readonly visible: {
7
+ readonly kind: "toggle";
8
+ readonly description: "The landmark models.";
9
+ readonly default: true;
10
+ };
11
+ readonly color: {
12
+ readonly kind: "color";
13
+ readonly description: "Colour the landmarks are shaded in. Unset: the base map's 3D buildings' colour, following the map colours that restyle them.";
14
+ };
15
+ readonly opacity: {
16
+ readonly kind: "number";
17
+ readonly description: "Opacity of the landmarks, 0 to 1. Unset: the base map's 3D buildings' opacity.";
18
+ readonly range: KnobRange;
19
+ };
20
+ readonly verticalGradient: {
21
+ readonly kind: "toggle";
22
+ readonly description: "Whether the landmarks' walls darken towards the ground. Unset: as the base map's 3D buildings do.";
23
+ };
24
+ readonly minZoom: {
25
+ readonly kind: "number";
26
+ readonly description: "Lowest zoom the landmarks are drawn at. Unset: the base map's 3D buildings' one.";
27
+ readonly range: KnobRange;
28
+ };
29
+ readonly maxZoom: {
30
+ readonly kind: "number";
31
+ readonly description: "Highest whole zoom level the landmarks are drawn at, up to the next one. Unset: the base map's 3D buildings' one.";
32
+ readonly range: KnobRange;
33
+ };
34
+ };
5
35
  /**
6
- * How 3D landmarks are shaded relative to the base map. Every mode renders the
7
- * landmarks as maplibre-style fill-extrusion buildings:
8
- * - `inherited`: mirrors the colour, opacity and vertical gradient of the basemap 3D building layer (default).
9
- * - `dark`: uses the 3D building look of the SDK's `streetDark` style.
10
- * - `light`: uses the 3D building look of the SDK's `streetLight` style.
36
+ * The id of a landmarks knob, as accepted by the SDK's `setKnob` and as a key of {@link Landmarks3DConfig}.
11
37
  *
12
38
  * @group Landmarks 3D
13
39
  */
14
- type Landmarks3DDisplayMode = 'inherited' | 'dark' | 'light';
40
+ type Landmarks3DKnobId = keyof typeof landmarks3DKnobDefinitions;
41
+ /**
42
+ * The value type a landmarks knob takes.
43
+ *
44
+ * @group Landmarks 3D
45
+ */
46
+ type Landmarks3DKnobValueOf<ID extends Landmarks3DKnobId> = KnobValueOf<(typeof landmarks3DKnobDefinitions)[ID]['kind']>;
47
+ /**
48
+ * One entry of {@link landmarks3DKnobCatalogue}, with what a UI or a coding agent needs to offer the knob
49
+ * and validate a value for it. The SDK's `getKnob` reads the value in force.
50
+ *
51
+ * @group Landmarks 3D
52
+ */
53
+ type Landmarks3DKnob = KnobEntry<Landmarks3DKnobId>;
54
+ /**
55
+ * Every landmarks knob with its kind, description and, for `visible`, its default. It's static data,
56
+ * readable without a map.
57
+ *
58
+ * @remarks
59
+ * A knob is a plain value in {@link Landmarks3DConfig}, under its id. Every knob but `visible`
60
+ * follows the base map's 3D buildings while unset, which the SDK's `getKnob` reports.
61
+ *
62
+ * @group Landmarks 3D
63
+ */
64
+ export declare const landmarks3DKnobCatalogue: readonly Landmarks3DKnob[];
65
+ /**
66
+ * Every landmarks knob id, in catalogue order.
67
+ *
68
+ * @group Landmarks 3D
69
+ */
70
+ export declare const landmarks3DKnobIds: readonly Landmarks3DKnobId[];
15
71
  //#endregion
16
72
  //#region src/FillExtrusionMaterial.d.ts
17
73
  /**
@@ -234,35 +290,23 @@ export declare class ModelsLayer implements CustomLayerInterface {
234
290
  //#endregion
235
291
  //#region src/Landmarks3D.d.ts
236
292
  /**
237
- * Configuration for the {@link Landmarks3D} plugin.
293
+ * The landmark knobs an app sets, as {@link Landmarks3D.getConfig} returns them. Each knob id is its
294
+ * key in here. Every knob but `visible` follows the base map's 3D buildings while unset. It's plain JSON.
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * const config: Landmarks3DConfig = { color: '#c8b28a', opacity: 0.9 };
299
+ * ```
238
300
  *
239
301
  * @group Landmarks 3D
240
302
  */
241
- type Landmarks3DOptions = {
242
- /**
243
- * Initial display mode for the landmarks. To change it at runtime, call
244
- * {@link Landmarks3D.setDisplayMode}.
245
- * @defaultValue `'inherited'`
246
- */
247
- displayMode?: Landmarks3DDisplayMode;
248
- /**
249
- * Whether the landmarks are initially visible. To change it at runtime, call
250
- * {@link Landmarks3D.setVisible}.
251
- * @defaultValue `true`
252
- */
253
- visible?: boolean;
254
- /**
255
- * Minimum zoom at which landmarks are rendered.
256
- * @defaultValue The basemap 3D building layer's minimum zoom, so landmarks appear together
257
- * with it (falls back to `10` when that layer is absent).
258
- */
259
- minZoom?: number;
260
- /**
261
- * Maximum zoom at which landmarks are rendered.
262
- * @defaultValue The basemap 3D building layer's maximum zoom, so landmarks disappear together
263
- * with it (falls back to `22` when that layer is absent).
264
- */
265
- maxZoom?: number;
303
+ type Landmarks3DConfig = KnobConfigOf<{ [ID in Landmarks3DKnobId]: Landmarks3DKnobValueOf<ID>; }>;
304
+ /**
305
+ * What the {@link Landmarks3D} plugin starts with: its knobs, and how it requests tiles.
306
+ *
307
+ * @group Landmarks 3D
308
+ */
309
+ type Landmarks3DOptions = Landmarks3DConfig & {
266
310
  /**
267
311
  * Whether each tile request sends `credentials: 'include'`, so a proxy session
268
312
  * cookie travels with it. By default, this tracks the SDK's proxy config,
@@ -282,9 +326,11 @@ type Landmarks3DOptions = {
282
326
  * across map style changes.
283
327
  *
284
328
  * @remarks
285
- * It requires an API key with Orbis 3D Landmarks entitlements. The exclusion
286
- * filter and `inherited` mode only have an effect on a map style that shows
287
- * extruded 3D buildings.
329
+ * It requires an API key with Orbis 3D Landmarks entitlements. Its look is a set of knobs, listed in
330
+ * {@link landmarks3DKnobCatalogue}. Each one you leave unset follows the base map's 3D buildings,
331
+ * through style changes and anything that restyles them, falling back to the `streetLight` style's look.
332
+ * Whether those buildings show is the `buildings3D` group of the `BaseMapModule`, hidden in most
333
+ * standard styles; the plugin leaves it as it is.
288
334
  *
289
335
  * @example
290
336
  * ```typescript
@@ -293,7 +339,7 @@ type Landmarks3DOptions = {
293
339
  * // assume `map` is your initialized TomTomMap instance
294
340
  * const landmarks = new Landmarks3D(map);
295
341
  *
296
- * await landmarks.setDisplayMode('inherited');
342
+ * landmarks.updateConfig({ color: '#c8b28a' });
297
343
  * ```
298
344
  *
299
345
  * @group Landmarks 3D
@@ -303,50 +349,81 @@ export declare class Landmarks3D {
303
349
  private readonly layerID;
304
350
  private readonly basemapBuildingLayerID;
305
351
  private readonly modelsLayer;
306
- private readonly layerMinZoom?;
307
- private readonly layerMaxZoom?;
308
- private displayMode;
352
+ private config;
353
+ private renderedLook;
309
354
  /**
310
355
  * Creates the plugin bound to a TomTom map and starts rendering landmarks
311
356
  * as soon as the map is ready.
312
357
  *
313
358
  * @param map - The TomTom map instance to render landmarks on.
359
+ * @param options - The knobs to start with, and how to request tiles.
314
360
  */
315
- constructor(map: TomTomMap, options?: Landmarks3DOptions);
361
+ constructor(map: TomTomMap, { withCredentials, ...config }?: Landmarks3DOptions);
316
362
  private installWhenReady;
317
363
  /**
318
364
  * The underlying MapLibre custom layer, for advanced use beyond this facade.
319
365
  */
320
366
  get layer(): ModelsLayer;
321
367
  /**
322
- * The currently applied display mode.
368
+ * Returns the knobs set, as a plain serializable copy. The SDK's `getKnob` reads a knob's value
369
+ * in force, the base map's one when unset.
323
370
  */
324
- getDisplayMode(): Landmarks3DDisplayMode;
371
+ getConfig(): Landmarks3DConfig;
325
372
  /**
326
- * Changes how landmarks are rendered relative to the base map.
373
+ * Replaces the whole configuration. An empty object has every knob follow the base map's 3D buildings.
374
+ *
375
+ * @remarks
376
+ * Use it, and {@link updateConfig}, for a look your code writes: as on a map module, the type is
377
+ * the check, and the values are not checked again. For values held as data, the SDK's `setKnobs`
378
+ * checks each against {@link landmarks3DKnobCatalogue} and sets them by id.
327
379
  *
328
- * @param mode - The display mode to apply.
329
- * @returns A promise that resolves once the mode has been applied.
380
+ * @example
381
+ * ```typescript
382
+ * landmarks.applyConfig({ color: '#c8b28a', opacity: 0.9 });
383
+ * ```
384
+ */
385
+ applyConfig(config: Landmarks3DConfig): void;
386
+ /**
387
+ * Changes the given knobs and keeps the others.
388
+ *
389
+ * @example
390
+ * ```typescript
391
+ * landmarks.updateConfig({ opacity: 1 }); // the colour still follows the base map
392
+ * ```
393
+ */
394
+ updateConfig(partial: Landmarks3DConfig): void;
395
+ /**
396
+ * Unsets every knob, so the landmarks are shown and look like the base map's 3D buildings.
330
397
  */
331
- setDisplayMode(mode: Landmarks3DDisplayMode): Promise<void>;
398
+ resetConfig(): void;
332
399
  /**
333
- * Whether the landmarks are currently visible.
400
+ * Whether the landmarks are set to be shown: the `visible` knob, `true` unless set.
334
401
  */
335
402
  isVisible(): boolean;
336
403
  /**
337
- * Shows or hides the landmarks layer.
404
+ * Shows or hides the landmarks. Stored as `visible` in the configuration.
338
405
  *
339
406
  * @param visible - `true` to show landmarks, `false` to hide them.
340
- * @returns A promise that resolves once the visibility has been applied.
341
407
  */
342
- setVisible(visible: boolean): Promise<void>;
408
+ setVisible(visible: boolean): void;
409
+ /**
410
+ * @ignore
411
+ */
412
+ _knobValue(id: string): unknown;
413
+ /**
414
+ * @ignore
415
+ */
416
+ _setKnobValues(values: Readonly<Record<string, unknown>>): void;
417
+ /**
418
+ * @ignore
419
+ */
420
+ _resetKnobValue(id: string): void;
343
421
  private install;
344
422
  private landmarkInsertBeforeID;
345
- private alignZoomRangeWithBasemapBuildings;
346
423
  private excludeLandmarkBuildingsFromBasemap;
347
- private readBasemapBuildingMaterial;
348
- private resolveCurrentMaterialState;
349
- private applyDisplayMode;
424
+ private readBasemapBuildingsLook;
425
+ private look;
426
+ private render;
350
427
  }
351
428
  //#endregion
352
429
  //#region src/landmarksTileURL.d.ts
@@ -361,4 +438,4 @@ export declare class Landmarks3D {
361
438
  */
362
439
  export declare const buildLandmarksTileURL: () => string;
363
440
  //#endregion
364
- export type { Landmarks3DDisplayMode, Landmarks3DOptions, ModelsLayerSpecification, ModelsSourceSpecification };
441
+ export type { Landmarks3DConfig, Landmarks3DKnob, Landmarks3DKnobId, Landmarks3DKnobValueOf, Landmarks3DOptions, ModelsLayerSpecification, ModelsSourceSpecification };
package/dist/index.es.js CHANGED
@@ -1 +1 @@
1
- import{TomTomConfig as e,isProxyCredentialsMode as i}from"@tomtom-org/maps-sdk/core";import{LayerFilterComposer as t,mapStyleLayerIDs as a}from"@tomtom-org/maps-sdk/map";import{AmbientLight as s,BufferAttribute as r,Camera as n,Color as o,CustomBlending as l,DirectionalLight as h,DoubleSide as m,FrontSide as d,Group as c,LoaderUtils as u,LoadingManager as p,Matrix4 as f,OneFactor as y,OneMinusSrcAlphaFactor as g,SRGBColorSpace as v,Scene as x,ShaderMaterial as M,Vector3 as L,WebGLRenderer as w}from"three";import{MeshoptDecoder as b}from"three/examples/jsm/libs/meshopt_decoder.module.js";import{GLTFLoader as k}from"three/examples/jsm/loaders/GLTFLoader.js";import{KTX2Loader as C}from"three/examples/jsm/loaders/KTX2Loader.js";import{toCreasedNormals as S}from"three/examples/jsm/utils/BufferGeometryUtils.js";import{MercatorCoordinate as P}from"maplibre-gl";var T={diffuseColor:"hsl(38, 6%, 90%)",opacity:.7,verticalGradient:!0},D={diffuseColor:"hsl(210, 9%, 17%)",opacity:.7,verticalGradient:!0},z=()=>{const{commonBaseURL:i,apiKey:t}=e.instance.get();return`${i}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${t?`key=${t}&`:""}apiVersion=2`},B="fillExtrusionGradient",O=`\n attribute float ${B};\n varying vec3 vWorldNormal;\n varying float vGradient;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n vGradient = ${B};\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n`,E="\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n",I="\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n",A=`\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n uniform float verticalGradient;\n varying vec3 vWorldNormal;\n varying float vGradient;\n ${E}\n\n void main() {\n ${I}\n vec3 normal = normalize(vWorldNormal);\n // Double-sided (alpha-masked) surfaces show their interior through carved openings;\n // flip backface normals so those interiors shade like the facade instead of going dark.\n if (!gl_FrontFacing) {\n normal = -normal;\n }\n // Extrusions have only vertical walls and flat roofs, so a sloped landmark roof would shade\n // darker than every basemap roof around it: up-facing normals light like a flat roof, the\n // rest like a wall, blended for curved surfaces.\n vec3 flattened = vec3(normal.xy, 0.0);\n vec3 wallNormal = flattened / max(length(flattened), 1e-5);\n float upness = smoothstep(0.4, 0.8, normal.z);\n normal = normalize(mix(wallNormal, vec3(0.0, 0.0, 1.0), upness));\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n vec3 color = diffuseColor + vec3(0.03);\n float directional = clamp(dot(normal, lightPosition), 0.0, 1.0);\n directional = mix(1.0 - lightIntensity, max(1.0 - colorValue + lightIntensity, 1.0), directional);\n if (abs(normal.y) > 0.001) {\n directional *=\n (1.0 - verticalGradient) +\n verticalGradient * clamp(vGradient, mix(0.7, 0.98, 1.0 - lightIntensity), 1.0);\n }\n vec3 shaded = clamp(\n color * directional * lightColor,\n mix(vec3(0.0), vec3(0.3), vec3(1.0) - lightColor),\n vec3(1.0)\n );\n gl_FragColor = vec4(shaded, 1.0) * layerOpacity;\n }\n`,F=new L(.2875,-.498,.996),G=new o,_=new o,R=class extends M{constructor(e){super({uniforms:{diffuseColor:{value:new L(1,1,1)},lightPosition:{value:F.clone()},lightIntensity:{value:.5},lightColor:{value:new L(1,1,1)},layerOpacity:{value:1},verticalGradient:{value:1},...e&&{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}}},...e&&{defines:{USE_ALPHA_MASK:""}},vertexShader:O,fragmentShader:A,side:e?m:d,transparent:!0,blending:l,blendSrc:y,blendDst:g})}copyShadingFrom(e){this.uniforms.diffuseColor.value.copy(e.uniforms.diffuseColor.value),this.uniforms.lightPosition.value.copy(e.uniforms.lightPosition.value),this.uniforms.lightIntensity.value=e.uniforms.lightIntensity.value,this.uniforms.lightColor.value.copy(e.uniforms.lightColor.value),this.uniforms.layerOpacity.value=e.uniforms.layerOpacity.value,this.uniforms.verticalGradient.value=e.uniforms.verticalGradient.value}setDiffuseColor(e){G.setStyle(e),G.getRGB(_,v),this.uniforms.diffuseColor.value.set(_.r,_.g,_.b)}setLayerOpacity(e){this.uniforms.layerOpacity.value=e}setVerticalGradient(e){this.uniforms.verticalGradient.value=e?1:0}setLight(e,i,t,a){const s=Math.cos(i),r=Math.sin(i);this.uniforms.lightPosition.value.set(e.x*s-e.y*r,e.x*r+e.y*s,e.z),this.uniforms.lightIntensity.value=t,this.uniforms.lightColor.value.set(a.r,a.g,a.b)}},U=class extends M{constructor(e){super({...e&&{uniforms:{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}},defines:{USE_ALPHA_MASK:""}},vertexShader:O,fragmentShader:`\n ${E}\n void main() {\n ${I}\n gl_FragColor = vec4(0.0);\n }\n `,side:e?m:d,colorWrite:!1})}},j=e=>!0===e.isMesh,Z=1/(2*Math.PI*6378137),K=Math.PI/6,V=class{type="models";id;minzoom;maxzoom;map;tiles;scene;loadedTiles=/* @__PURE__ */new Set;sceneOrigin=null;onTileLoaded;ktx2Loader;transcoderReady;gltfLoader;specification;withCredentials;coveringTileKeys=/* @__PURE__ */new Set;constructor(e,i){this.id=e,this.minzoom=i.minzoom??0,this.maxzoom=i.maxzoom??22,this.tiles=i.tiles,this.specification=i,this.withCredentials=i.withCredentials??!1,this.scene=new c,this.ktx2Loader=new C(new p).setCrossOrigin("anonymous").setTranscoderPath(i.transcoderPath??""),this.gltfLoader=(new k).setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(b)}onAdd(e,i){this.map=e,this.ktx2Loader.detectSupport(i)}updateTiles(){if(0===this.tiles.length)return;const e=this.coveringTiles();this.coveringTileKeys=new Set(e.map(e=>e.key));for(const i of e)this.loadTile(i);this.showCoveringTilesOnly()}showCoveringTilesOnly(){for(const e of this.scene.children)e.visible=this.coveringTileKeys.has(e.userData.key)}coveringTiles(){const e={tileSize:this.map._camera.transform.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,terrain:this.map.terrain};return this.map.coveringTiles(e)}loadTile(e){if(this.loadedTiles.has(e.key))return;this.loadedTiles.add(e.key);const i=e.canonical.url(this.tiles,this.map.getPixelRatio(),this.specification.scheme);this.fetchTile(i).then(i=>{i&&(W(i),this.rebaseToSceneOrigin(i),i.userData={isTile:!0,key:e.key},i.visible=this.coveringTileKeys.has(e.key),this.scene.add(i),this.map.triggerRepaint(),this.onTileLoaded?.(i,e.key))}).catch(i=>{console.warn(`Problem with loading ${e.toString()}: ${i?.message??"unknown"}`)})}rebaseToSceneOrigin(e){const i=[];if(e.traverse(e=>{j(e)&&i.push(e)}),0===i.length)return;const t=i[0].position,a={x:Math.round(t.x),y:Math.round(t.y)};if(this.sceneOrigin){if(Math.hypot(t.x-this.sceneOrigin.x,t.y-this.sceneOrigin.y)>1e5){const e=a.x-this.sceneOrigin.x,i=a.y-this.sceneOrigin.y;this.scene.traverse(t=>{j(t)&&(t.position.x-=e,t.position.y-=i)}),this.sceneOrigin=a}}else this.sceneOrigin=a;for(const s of i)s.position.x-=this.sceneOrigin.x,s.position.y-=this.sceneOrigin.y}async fetchTile(e){const i=await fetch(e,this.withCredentials?{credentials:"include"}:void 0);if(204===i.status||404===i.status)return null;if(403===i.status&&"InvalidReferer"!==await(async e=>(await e.json().catch(()=>{}))?.detailedError?.code)(i))throw new Error("tile request failed with 403: Orbis 3D Landmarks is in Private Preview (https://docs.tomtom.com/legal/private-preview), and the API key needs access to it: request it from TomTom Sales at https://www.tomtom.com/contact-sales/?source_app=docs-portal&source_product=map-display-api-3d");if(!i.ok)throw new Error(`tile request failed with ${i.status}`);const t=await i.arrayBuffer();this.transcoderReady??=(async(e,i)=>{if(void 0!==i)return;const{resolveBundledTranscoderUrl:t}=await(import("./bundledTranscoder-Dn27cuYm.js"));e.setURLModifier(t)})(this.ktx2Loader.manager,this.specification.transcoderPath),await this.transcoderReady;const{scene:a}=await this.gltfLoader.parseAsync(t,u.extractUrlBase(e));return a}},W=e=>{e.traverse(e=>{if(!j(e))return;const i=e.material;i.alphaTest=.1,i.side=m,e.userData.originalMaterial=i,e.geometry=S(e.geometry,K),$(e)})},$=e=>{const i=e.geometry,t=i.getAttribute("position");i.computeBoundingBox();const a=i.boundingBox?.max.z??0,s=a>0?Math.sqrt(a/150)/a:0,n=new Float32Array(t.count);for(let r=0;r<t.count;r++)n[r]=t.getZ(r)*s;i.setAttribute(B,new r(n,1))},H=e=>e*Math.PI/180,N=class{id;type="custom";renderingMode="3d";renderer;scene;camera;tiles;directionalLight;ambientLight;map;source;minzoom=0;maxzoom=22;visible=!0;diffuseColor="#ffffff";fillExtrusionMaterial=new R;depthPrepassMaterial=new U;maskedFillExtrusionMaterials=/* @__PURE__ */new Map;tilesMainMatrix=/* @__PURE__ */new Float64Array(16);tilesStale=!0;markTilesStale=()=>{this.tilesStale=!0};constructor(e,i){this.id=e.id,this.source=new V(this.id,i),this.tiles=this.source.scene,this.updateProperties(e)}updateProperties(e){this.visible="none"!==e.layout?.visibility,this.diffuseColor=e.paint?.["models-base-color"]??"#ffffff",this.minzoom=e.minzoom??0,this.maxzoom=e.maxzoom??22}setDirectionalLight(e,i,t){this.directionalLight.position.setFromSphericalCoords(e,H(i),H(t))}onAdd(e,i){this.map=e,this.scene=new x,this.tiles=this.source.scene,this.camera=new n,this.directionalLight=new h(16777215,.5),this.setDirectionalLight(1.15,90,0),this.scene.add(this.directionalLight),this.ambientLight=new s(16777215,.1),this.scene.add(this.ambientLight),this.scene.add(this.tiles),this.renderer=new w({canvas:e.getCanvas(),context:i,antialias:!0}),this.renderer.autoClear=!1,this.source.onAdd(e,this.renderer),this.tilesStale=!0,e.off("data",this.markTilesStale),e.on("data",this.markTilesStale)}render(e,i){const t=this.map._camera.transform.tileZoom,a=t>=this.minzoom&&t<=this.maxzoom;this.tiles.visible=this.visible&&a,this.visible&&a&&(this.syncLighting(),this.updateTilesWhenStale(i.defaultProjectionData.mainMatrix),this.groundLandmarksOnTerrain(),this.alignCameraToMap(i),this.renderer.resetState(),this.needsTranslucentPrepass()&&this.renderDepthPrepass(),this.applyMaterials(),this.renderer.render(this.scene,this.camera))}updateTilesWhenStale(e){(this.tilesMainMatrix.some((i,t)=>i!==e[t])||this.tilesStale)&&(this.tilesMainMatrix.set(e),this.tilesStale=!1,this.source.updateTiles())}groundLandmarksOnTerrain(){for(const e of this.tiles.children)e.visible&&e.traverse(e=>{j(e)&&this.groundMeshOnTerrain(e)})}groundMeshOnTerrain(e){const i=this.terrainSamplePoints(e);if(!i)return;let t=null;for(const a of i){const i=this.map.queryTerrainElevation(a);if(null===i)return void(e.position.z=0);(null===t||i<t)&&(t=i)}null!==t&&(e.position.z=t-.5)}terrainSamplePoints(e){let i=e.userData.terrainSamplePoints;if(!i){e.geometry.boundingBox||e.geometry.computeBoundingBox();const t=e.geometry.boundingBox;if(!t||t.isEmpty())return null;const a=e.position.x+this.sceneOrigin.x,s=e.position.y+this.sceneOrigin.y,r=(t.min.x+t.max.x)/2,n=(t.min.y+t.max.y)/2;i=[[r,n],[t.min.x,t.min.y],[t.min.x,t.max.y],[t.max.x,t.min.y],[t.max.x,t.max.y],[r,t.min.y],[r,t.max.y],[t.min.x,n],[t.max.x,n]].map(([e,i])=>new P(.5+(a+e)*Z,.5-(s+i)*Z).toLngLat().toArray()),e.userData.terrainSamplePoints=i}return i}alignCameraToMap(e){const i=Z/Math.cos(H(this.map.getCenter().lat)),t=.5+Z*this.sceneOrigin.x,a=.5-Z*this.sceneOrigin.y,s=new f;s.set(Z,0,0,t,0,-Z,0,a,0,0,i,0,0,0,0,1);const r=(new f).fromArray(e.defaultProjectionData.mainMatrix);this.camera.projectionMatrix.copy(r).multiply(s)}get sceneOrigin(){return this.source.sceneOrigin??{x:0,y:0}}needsTranslucentPrepass(){return this.fillExtrusionMaterial.uniforms.layerOpacity.value<1}renderDepthPrepass(){this.tiles.traverse(e=>{j(e)&&(e.material=this.maskedFillExtrusionVariant(e)?.depth??this.depthPrepassMaterial)}),this.renderer.render(this.scene,this.camera)}setOpacity(e){this.forEachFillExtrusionMaterial(i=>i.setLayerOpacity(e))}setVerticalGradient(e){this.forEachFillExtrusionMaterial(i=>i.setVerticalGradient(e))}getMaterial(e){return this.maskedFillExtrusionVariant(e)?.color??this.fillExtrusionMaterial}maskedFillExtrusionVariant(e){const i=e.userData.originalMaterial,t=i?.map;if(!t)return null;let a=this.maskedFillExtrusionMaterials.get(t);if(!a){const e={map:t,cutoff:i.alphaTest||.1};a={color:new R(e),depth:new U(e)},a.color.copyShadingFrom(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.set(t,a)}return a}forEachFillExtrusionMaterial(e){e(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.forEach(i=>e(i.color))}syncLighting(){const e=this.map,i=e.style?.light,t=i?.properties,a=i?.getCartesianPosition?.();if(t&&a)try{const i=t.get("intensity");this.directionalLight.intensity=i*Math.PI,this.ambientLight.intensity=(1-i)*Math.PI;const s={x:-a[0],y:a[1],z:a[2]};this.directionalLight.position.set(s.x,s.y,s.z);const r="viewport"===t.get("anchor")?-(e._camera.transform.bearingInRadians??0):0,n=t.get("color");this.forEachFillExtrusionMaterial(e=>e.setLight(s,r,i,n))}catch{}}applyMaterials(){this.forEachFillExtrusionMaterial(e=>e.setDiffuseColor(this.diffuseColor)),this.tiles.traverse(e=>{j(e)&&(e.material=this.getMaterial(e))})}},q=["!",["coalesce",["get","has_landmark"],!1]],X=class{map;layerID="orbis-3d-landmarks";basemapBuildingLayerID="3D - Building";modelsLayer;layerMinZoom;layerMaxZoom;displayMode;constructor(e,t={}){this.map=e,this.layerMinZoom=t.minZoom,this.layerMaxZoom=t.maxZoom,this.displayMode=t.displayMode??"inherited";const a={id:this.layerID,type:"models",source:this.layerID,minzoom:t.minZoom??10,maxzoom:t.maxZoom??22},s={type:"models",tiles:[z()],minzoom:15,maxzoom:15,withCredentials:t.withCredentials??i()};this.modelsLayer=new N(a,s),this.modelsLayer.visible=t.visible??!0,this.map.addStyleChangeHandler({onStyleChanged:()=>this.install()}),this.installWhenReady()}installWhenReady(){this.map.mapReady?this.install():this.map.mapLibreMap.once("styledata",()=>this.installWhenReady())}get layer(){return this.modelsLayer}getDisplayMode(){return this.displayMode}async setDisplayMode(e){this.displayMode=e,this.map.mapReady&&this.applyDisplayMode()}isVisible(){return this.modelsLayer.visible}async setVisible(e){this.modelsLayer.visible=e,this.map.mapReady&&this.map.mapLibreMap.triggerRepaint()}install(){const e=this.map.mapLibreMap;e.getLayer(this.layerID)||e.addLayer(this.modelsLayer,this.landmarkInsertBeforeID()),e.getLayer(this.basemapBuildingLayerID)&&e.setLayoutProperty(this.basemapBuildingLayerID,"visibility","visible"),this.alignZoomRangeWithBasemapBuildings(),this.excludeLandmarkBuildingsFromBasemap(),this.modelsLayer.source.onTileLoaded=()=>{this.resolveCurrentMaterialState().opacity<1&&this.applyDisplayMode()},this.applyDisplayMode()}landmarkInsertBeforeID(){const e=a.poi;return this.map.mapLibreMap.getLayer(e)?e:void 0}alignZoomRangeWithBasemapBuildings(){const e=this.map.mapLibreMap.getLayer(this.basemapBuildingLayerID);e&&(this.modelsLayer.minzoom=this.layerMinZoom??e.minzoom??10,this.modelsLayer.maxzoom=this.layerMaxZoom??e.maxzoom??22)}excludeLandmarkBuildingsFromBasemap(){t.for(this.map).setClause(this.basemapBuildingLayerID,"landmarks3d.buildings",q)}readBasemapBuildingMaterial(){const e=this.map.mapLibreMap;if(!e.getLayer(this.basemapBuildingLayerID))return null;const i=(e=>{if("string"==typeof e)return e;if(Array.isArray(e)&&"match"===e[0]){const i=e.at(-1);if("string"==typeof i)return i}return null})(e.getPaintProperty(this.basemapBuildingLayerID,"fill-extrusion-color"));if(!i)return null;const t=e.getPaintProperty(this.basemapBuildingLayerID,"fill-extrusion-opacity"),a=e.getPaintProperty(this.basemapBuildingLayerID,"fill-extrusion-vertical-gradient");return{diffuseColor:i,..."number"==typeof t&&{opacity:t},..."boolean"==typeof a&&{verticalGradient:a}}}resolveCurrentMaterialState(){const e="inherited"===this.displayMode?this.readBasemapBuildingMaterial():null;return((e,i)=>{switch(e){case"dark":return D;case"light":return T;default:return{...T,...i}}})(this.displayMode,e)}applyDisplayMode(){const e=this.resolveCurrentMaterialState();this.modelsLayer.diffuseColor=e.diffuseColor,this.modelsLayer.setOpacity(e.opacity),this.modelsLayer.setVerticalGradient(e.verticalGradient),this.map.mapLibreMap.triggerRepaint()}};export{X as Landmarks3D,N as ModelsLayer,V as ModelsSource,z as buildLandmarksTileURL};
1
+ import{TomTomConfig,isProxyCredentialsMode}from"@tomtom-org/maps-sdk/core";import{LayerFilterComposer,configWithKnobs,configWithoutKnob,mapStyleLayerIDs}from"@tomtom-org/maps-sdk/map";import{AmbientLight,BufferAttribute,Camera,Color,CustomBlending,DirectionalLight,DoubleSide,FrontSide,Group,LoaderUtils,LoadingManager,Matrix4,OneFactor,OneMinusSrcAlphaFactor,SRGBColorSpace,Scene,ShaderMaterial,Vector3,WebGLRenderer}from"three";import{MeshoptDecoder}from"three/examples/jsm/libs/meshopt_decoder.module.js";import{GLTFLoader}from"three/examples/jsm/loaders/GLTFLoader.js";import{KTX2Loader}from"three/examples/jsm/loaders/KTX2Loader.js";import{toCreasedNormals}from"three/examples/jsm/utils/BufferGeometryUtils.js";import{MercatorCoordinate}from"maplibre-gl";var ZOOM_RANGE={min:0,max:22,step:1,bounds:"hard"},landmarks3DKnobDefinitions={visible:{kind:"toggle",description:"The landmark models.",default:!0},color:{kind:"color",description:"Colour the landmarks are shaded in. Unset: the base map's 3D buildings' colour, following the map colours that restyle them."},opacity:{kind:"number",description:"Opacity of the landmarks, 0 to 1. Unset: the base map's 3D buildings' opacity.",range:{min:0,max:1,step:.05,bounds:"hard"}},verticalGradient:{kind:"toggle",description:"Whether the landmarks' walls darken towards the ground. Unset: as the base map's 3D buildings do."},minZoom:{kind:"number",description:"Lowest zoom the landmarks are drawn at. Unset: the base map's 3D buildings' one.",range:ZOOM_RANGE},maxZoom:{kind:"number",description:"Highest whole zoom level the landmarks are drawn at, up to the next one. Unset: the base map's 3D buildings' one.",range:ZOOM_RANGE}},landmarks3DKnobCatalogue=/* @__PURE__ */(()=>Object.keys(landmarks3DKnobDefinitions).map(e=>({id:e,...landmarks3DKnobDefinitions[e]})))(),landmarks3DKnobIds=/* @__PURE__ */landmarks3DKnobCatalogue.map(({id:e})=>e),buildLandmarksTileURL=()=>{const{commonBaseURL:e,apiKey:i}=TomTomConfig.instance.get();return`${e}/maps/orbis/display/3d/landmarks/tile/{z}/{x}/{y}?${i?`key=${i}&`:""}apiVersion=2`},STREET_LIGHT_LOOK={visible:!0,color:"hsl(38, 6%, 90%)",opacity:.7,verticalGradient:!0,minZoom:10,maxZoom:22},VERTEX_SHADER="\n attribute float fillExtrusionGradient;\n varying vec3 vWorldNormal;\n varying float vGradient;\n #ifdef USE_ALPHA_MASK\n varying vec2 vUv;\n #endif\n\n void main() {\n vWorldNormal = normalize(mat3(modelMatrix) * normal);\n vGradient = fillExtrusionGradient;\n #ifdef USE_ALPHA_MASK\n vUv = uv;\n #endif\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n",ALPHA_MASK_FRAGMENT_CHUNK="\n #ifdef USE_ALPHA_MASK\n uniform sampler2D alphaMask;\n uniform float alphaMaskCutoff;\n varying vec2 vUv;\n #endif\n",ALPHA_MASK_MAIN_CHUNK="\n #ifdef USE_ALPHA_MASK\n if (texture2D(alphaMask, vUv).a < alphaMaskCutoff) {\n discard;\n }\n #endif\n",FRAGMENT_SHADER=`\n uniform vec3 diffuseColor;\n uniform vec3 lightPosition;\n uniform float lightIntensity;\n uniform vec3 lightColor;\n uniform float layerOpacity;\n uniform float verticalGradient;\n varying vec3 vWorldNormal;\n varying float vGradient;\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n vec3 normal = normalize(vWorldNormal);\n // Double-sided (alpha-masked) surfaces show their interior through carved openings;\n // flip backface normals so those interiors shade like the facade instead of going dark.\n if (!gl_FrontFacing) {\n normal = -normal;\n }\n // Extrusions have only vertical walls and flat roofs, so a sloped landmark roof would shade\n // darker than every basemap roof around it: up-facing normals light like a flat roof, the\n // rest like a wall, blended for curved surfaces.\n vec3 flattened = vec3(normal.xy, 0.0);\n vec3 wallNormal = flattened / max(length(flattened), 1e-5);\n float upness = smoothstep(0.4, 0.8, normal.z);\n normal = normalize(mix(wallNormal, vec3(0.0, 0.0, 1.0), upness));\n float colorValue = dot(diffuseColor, vec3(0.2126, 0.7152, 0.0722));\n vec3 color = diffuseColor + vec3(0.03);\n float directional = clamp(dot(normal, lightPosition), 0.0, 1.0);\n directional = mix(1.0 - lightIntensity, max(1.0 - colorValue + lightIntensity, 1.0), directional);\n if (abs(normal.y) > 0.001) {\n directional *=\n (1.0 - verticalGradient) +\n verticalGradient * clamp(vGradient, mix(0.7, 0.98, 1.0 - lightIntensity), 1.0);\n }\n vec3 shaded = clamp(\n color * directional * lightColor,\n mix(vec3(0.0), vec3(0.3), vec3(1.0) - lightColor),\n vec3(1.0)\n );\n gl_FragColor = vec4(shaded, 1.0) * layerOpacity;\n }\n`,DEFAULT_LIGHT_POSITION=/* @__PURE__ */new Vector3(.2875,-.498,.996),parsedColor=/* @__PURE__ */new Color,srgbColor=/* @__PURE__ */new Color,FillExtrusionMaterial=class extends ShaderMaterial{constructor(e){super({uniforms:{diffuseColor:{value:new Vector3(1,1,1)},lightPosition:{value:DEFAULT_LIGHT_POSITION.clone()},lightIntensity:{value:.5},lightColor:{value:new Vector3(1,1,1)},layerOpacity:{value:1},verticalGradient:{value:1},...e&&{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}}},...e&&{defines:{USE_ALPHA_MASK:""}},vertexShader:VERTEX_SHADER,fragmentShader:FRAGMENT_SHADER,side:e?DoubleSide:FrontSide,transparent:!0,blending:CustomBlending,blendSrc:OneFactor,blendDst:OneMinusSrcAlphaFactor})}copyShadingFrom(e){this.uniforms.diffuseColor.value.copy(e.uniforms.diffuseColor.value),this.uniforms.lightPosition.value.copy(e.uniforms.lightPosition.value),this.uniforms.lightIntensity.value=e.uniforms.lightIntensity.value,this.uniforms.lightColor.value.copy(e.uniforms.lightColor.value),this.uniforms.layerOpacity.value=e.uniforms.layerOpacity.value,this.uniforms.verticalGradient.value=e.uniforms.verticalGradient.value}setDiffuseColor(e){parsedColor.setStyle(e),parsedColor.getRGB(srgbColor,SRGBColorSpace),this.uniforms.diffuseColor.value.set(srgbColor.r,srgbColor.g,srgbColor.b)}setLayerOpacity(e){this.uniforms.layerOpacity.value=e}setVerticalGradient(e){this.uniforms.verticalGradient.value=e?1:0}setLight(e,i,t,a){const r=Math.cos(i),s=Math.sin(i);this.uniforms.lightPosition.value.set(e.x*r-e.y*s,e.x*s+e.y*r,e.z),this.uniforms.lightIntensity.value=t,this.uniforms.lightColor.value.set(a.r,a.g,a.b)}},FillExtrusionDepthMaterial=class extends ShaderMaterial{constructor(e){super({...e&&{uniforms:{alphaMask:{value:e.map},alphaMaskCutoff:{value:e.cutoff}},defines:{USE_ALPHA_MASK:""}},vertexShader:VERTEX_SHADER,fragmentShader:`\n ${ALPHA_MASK_FRAGMENT_CHUNK}\n void main() {\n ${ALPHA_MASK_MAIN_CHUNK}\n gl_FragColor = vec4(0.0);\n }\n `,side:e?DoubleSide:FrontSide,colorWrite:!1})}},isMesh=e=>!0===e.isMesh,MERCATOR_UNITS_PER_METRE=1/(2*Math.PI*6378137),CREASE_ANGLE_RADIANS=Math.PI/6,ModelsSource=class{type="models";id;minzoom;maxzoom;map;tiles;scene;loadedTiles=/* @__PURE__ */new Set;sceneOrigin=null;onTileLoaded;ktx2Loader;transcoderReady;gltfLoader;specification;withCredentials;coveringTileKeys=/* @__PURE__ */new Set;constructor(e,i){this.id=e,this.minzoom=i.minzoom??0,this.maxzoom=i.maxzoom??22,this.tiles=i.tiles,this.specification=i,this.withCredentials=i.withCredentials??!1,this.scene=new Group,this.ktx2Loader=new KTX2Loader(new LoadingManager).setCrossOrigin("anonymous").setTranscoderPath(i.transcoderPath??""),this.gltfLoader=(new GLTFLoader).setKTX2Loader(this.ktx2Loader).setMeshoptDecoder(MeshoptDecoder)}onAdd(e,i){this.map=e,this.ktx2Loader.detectSupport(i)}updateTiles(){if(0===this.tiles.length)return;const e=this.coveringTiles();this.coveringTileKeys=new Set(e.map(e=>e.key));for(const i of e)this.loadTile(i);this.showCoveringTilesOnly()}showCoveringTilesOnly(){for(const e of this.scene.children)e.visible=this.coveringTileKeys.has(e.userData.key)}coveringTiles(){const e={tileSize:this.map._camera.transform.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,terrain:this.map.terrain};return this.map.coveringTiles(e)}loadTile(e){if(this.loadedTiles.has(e.key))return;this.loadedTiles.add(e.key);const i=e.canonical.url(this.tiles,this.map.getPixelRatio(),this.specification.scheme);this.fetchTile(i).then(i=>{i&&(prepareTileMeshes(i),this.rebaseToSceneOrigin(i),i.userData={isTile:!0,key:e.key},i.visible=this.coveringTileKeys.has(e.key),this.scene.add(i),this.map.triggerRepaint(),this.onTileLoaded?.(i,e.key))}).catch(i=>{console.warn(`Problem with loading ${e.toString()}: ${i?.message??"unknown"}`)})}rebaseToSceneOrigin(e){const i=[];if(e.traverse(e=>{isMesh(e)&&i.push(e)}),0===i.length)return;const t=i[0].position,a={x:Math.round(t.x),y:Math.round(t.y)};if(this.sceneOrigin){if(Math.hypot(t.x-this.sceneOrigin.x,t.y-this.sceneOrigin.y)>1e5){const e=a.x-this.sceneOrigin.x,i=a.y-this.sceneOrigin.y;this.scene.traverse(t=>{isMesh(t)&&(t.position.x-=e,t.position.y-=i)}),this.sceneOrigin=a}}else this.sceneOrigin=a;for(const r of i)r.position.x-=this.sceneOrigin.x,r.position.y-=this.sceneOrigin.y}async fetchTile(e){const i=await fetch(e,this.withCredentials?{credentials:"include"}:void 0);if(204===i.status||404===i.status)return null;if(403===i.status&&"InvalidReferer"!==await(async e=>(await e.json().catch(()=>{}))?.detailedError?.code)(i))throw new Error("tile request failed with 403: Orbis 3D Landmarks is in Private Preview (https://docs.tomtom.com/legal/private-preview), and the API key needs access to it: request it from TomTom Sales at https://www.tomtom.com/contact-sales/?source_app=docs-portal&source_product=map-display-api-3d");if(!i.ok)throw new Error(`tile request failed with ${i.status}`);const t=await i.arrayBuffer();this.transcoderReady??=(async e=>{if(void 0!==this.specification.transcoderPath)return;const{resolveBundledTranscoderUrl:i}=await(import("./bundledTranscoder-IHtuhlDT.js"));e.setURLModifier(i)})(this.ktx2Loader.manager),await this.transcoderReady;const{scene:a}=await this.gltfLoader.parseAsync(t,LoaderUtils.extractUrlBase(e));return a}},prepareTileMeshes=e=>{e.traverse(e=>{if(!isMesh(e))return;const i=e.material;i.alphaTest=.1,i.side=DoubleSide,e.userData.originalMaterial=i,e.geometry=toCreasedNormals(e.geometry,CREASE_ANGLE_RADIANS),bakeGradientAttribute(e)})},bakeGradientAttribute=e=>{const i=e.geometry,t=i.getAttribute("position");i.computeBoundingBox();const a=i.boundingBox?.max.z??0,r=a>0?Math.sqrt(a/150)/a:0,s=new Float32Array(t.count);for(let o=0;o<t.count;o++)s[o]=t.getZ(o)*r;i.setAttribute("fillExtrusionGradient",new BufferAttribute(s,1))},toRadians=e=>e*Math.PI/180,ModelsLayer=class{id;type="custom";renderingMode="3d";renderer;scene;camera;tiles;directionalLight;ambientLight;map;source;minzoom=0;maxzoom=22;visible=!0;diffuseColor="#ffffff";fillExtrusionMaterial=new FillExtrusionMaterial;depthPrepassMaterial=new FillExtrusionDepthMaterial;maskedFillExtrusionMaterials=/* @__PURE__ */new Map;tilesMainMatrix=/* @__PURE__ */new Float64Array(16);tilesStale=!0;markTilesStale=()=>{this.tilesStale=!0};constructor(e,i){this.id=e.id,this.source=new ModelsSource(this.id,i),this.tiles=this.source.scene,this.updateProperties(e)}updateProperties(e){this.visible="none"!==e.layout?.visibility,this.diffuseColor=e.paint?.["models-base-color"]??"#ffffff",this.minzoom=e.minzoom??0,this.maxzoom=e.maxzoom??22}setDirectionalLight(e,i,t){this.directionalLight.position.setFromSphericalCoords(e,toRadians(i),toRadians(t))}onAdd(e,i){this.map=e,this.scene=new Scene,this.tiles=this.source.scene,this.camera=new Camera,this.directionalLight=new DirectionalLight(16777215,.5),this.setDirectionalLight(1.15,90,0),this.scene.add(this.directionalLight),this.ambientLight=new AmbientLight(16777215,.1),this.scene.add(this.ambientLight),this.scene.add(this.tiles),this.renderer=new WebGLRenderer({canvas:e.getCanvas(),context:i,antialias:!0}),this.renderer.autoClear=!1,this.source.onAdd(e,this.renderer),this.tilesStale=!0,e.off("data",this.markTilesStale),e.on("data",this.markTilesStale)}render(e,i){const t=this.map._camera.transform.tileZoom,a=t>=this.minzoom&&t<=this.maxzoom;this.tiles.visible=this.visible&&a,this.visible&&a&&(this.syncLighting(),this.updateTilesWhenStale(i.defaultProjectionData.mainMatrix),this.groundLandmarksOnTerrain(),this.alignCameraToMap(i),this.renderer.resetState(),this.needsTranslucentPrepass()&&this.renderDepthPrepass(),this.applyMaterials(),this.renderer.render(this.scene,this.camera))}updateTilesWhenStale(e){(this.tilesMainMatrix.some((i,t)=>i!==e[t])||this.tilesStale)&&(this.tilesMainMatrix.set(e),this.tilesStale=!1,this.source.updateTiles())}groundLandmarksOnTerrain(){for(const e of this.tiles.children)e.visible&&e.traverse(e=>{isMesh(e)&&this.groundMeshOnTerrain(e)})}groundMeshOnTerrain(e){const i=this.terrainSamplePoints(e);if(!i)return;let t=null;for(const a of i){const i=this.map.queryTerrainElevation(a);if(null===i)return void(e.position.z=0);(null===t||i<t)&&(t=i)}null!==t&&(e.position.z=t-.5)}terrainSamplePoints(e){let i=e.userData.terrainSamplePoints;if(!i){e.geometry.boundingBox||e.geometry.computeBoundingBox();const t=e.geometry.boundingBox;if(!t||t.isEmpty())return null;const a=e.position.x+this.sceneOrigin.x,r=e.position.y+this.sceneOrigin.y,s=(t.min.x+t.max.x)/2,o=(t.min.y+t.max.y)/2;i=[[s,o],[t.min.x,t.min.y],[t.min.x,t.max.y],[t.max.x,t.min.y],[t.max.x,t.max.y],[s,t.min.y],[s,t.max.y],[t.min.x,o],[t.max.x,o]].map(([e,i])=>new MercatorCoordinate(.5+(a+e)*MERCATOR_UNITS_PER_METRE,.5-(r+i)*MERCATOR_UNITS_PER_METRE).toLngLat().toArray()),e.userData.terrainSamplePoints=i}return i}alignCameraToMap(e){const i=MERCATOR_UNITS_PER_METRE/Math.cos(toRadians(this.map.getCenter().lat)),t=.5+MERCATOR_UNITS_PER_METRE*this.sceneOrigin.x,a=.5-MERCATOR_UNITS_PER_METRE*this.sceneOrigin.y,r=new Matrix4;r.set(MERCATOR_UNITS_PER_METRE,0,0,t,0,-MERCATOR_UNITS_PER_METRE,0,a,0,0,i,0,0,0,0,1);const s=(new Matrix4).fromArray(e.defaultProjectionData.mainMatrix);this.camera.projectionMatrix.copy(s).multiply(r)}get sceneOrigin(){return this.source.sceneOrigin??{x:0,y:0}}needsTranslucentPrepass(){return this.fillExtrusionMaterial.uniforms.layerOpacity.value<1}renderDepthPrepass(){this.tiles.traverse(e=>{isMesh(e)&&(e.material=this.maskedFillExtrusionVariant(e)?.depth??this.depthPrepassMaterial)}),this.renderer.render(this.scene,this.camera)}setOpacity(e){this.forEachFillExtrusionMaterial(i=>i.setLayerOpacity(e))}setVerticalGradient(e){this.forEachFillExtrusionMaterial(i=>i.setVerticalGradient(e))}getMaterial(e){return this.maskedFillExtrusionVariant(e)?.color??this.fillExtrusionMaterial}maskedFillExtrusionVariant(e){const i=e.userData.originalMaterial,t=i?.map;if(!t)return null;let a=this.maskedFillExtrusionMaterials.get(t);if(!a){const e={map:t,cutoff:i.alphaTest||.1};a={color:new FillExtrusionMaterial(e),depth:new FillExtrusionDepthMaterial(e)},a.color.copyShadingFrom(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.set(t,a)}return a}forEachFillExtrusionMaterial(e){e(this.fillExtrusionMaterial),this.maskedFillExtrusionMaterials.forEach(i=>e(i.color))}syncLighting(){const e=this.map,i=e.style?.light,t=i?.properties,a=i?.getCartesianPosition?.();if(t&&a)try{const i=t.get("intensity");this.directionalLight.intensity=i*Math.PI,this.ambientLight.intensity=(1-i)*Math.PI;const r={x:-a[0],y:a[1],z:a[2]};this.directionalLight.position.set(r.x,r.y,r.z);const s="viewport"===t.get("anchor")?-(e._camera.transform.bearingInRadians??0):0,o=t.get("color");this.forEachFillExtrusionMaterial(e=>e.setLight(r,s,i,o))}catch{}}applyMaterials(){this.forEachFillExtrusionMaterial(e=>e.setDiffuseColor(this.diffuseColor)),this.tiles.traverse(e=>{isMesh(e)&&(e.material=this.getMaterial(e))})}},NO_LANDMARK_FILTER_CLAUSE=["!",["coalesce",["get","has_landmark"],!1]],Landmarks3D=class{map;layerID="orbis-3d-landmarks";basemapBuildingLayerID="3D - Building";modelsLayer;config;renderedLook;constructor(e,{withCredentials:i,...t}={}){this.map=e,this.config=t;const a={id:this.layerID,type:"models",source:this.layerID},r={type:"models",tiles:[buildLandmarksTileURL()],minzoom:15,maxzoom:15,withCredentials:i??isProxyCredentialsMode()};this.modelsLayer=new ModelsLayer(a,r),this.render(),this.map.addStyleChangeHandler({onStyleChanged:()=>this.install()}),this.map.mapLibreMap.on("styledata",()=>{var e,i;this.map.mapReady&&(e=this.look(),void 0===(i=this.renderedLook)||!Object.keys(e).every(t=>e[t]===i[t]))&&this.render()}),this.installWhenReady()}installWhenReady(){this.map.mapReady?this.install():this.map.mapLibreMap.once("styledata",()=>this.installWhenReady())}get layer(){return this.modelsLayer}getConfig(){return structuredClone(this.config)}applyConfig(e){this.config=e,this.render()}updateConfig(e){this.applyConfig({...this.config,...e})}resetConfig(){this.applyConfig({})}isVisible(){return!1!==this.config.visible}setVisible(e){this.updateConfig({visible:e})}_knobValue(e){return this.look()[e]}_setKnobValues(e){this.applyConfig(configWithKnobs(this.config,e))}_resetKnobValue(e){this.applyConfig(configWithoutKnob(this.config,e))}install(){const e=this.map.mapLibreMap;e.getLayer(this.layerID)||e.addLayer(this.modelsLayer,this.landmarkInsertBeforeID()),this.excludeLandmarkBuildingsFromBasemap(),this.modelsLayer.source.onTileLoaded=()=>{this.look().opacity<1&&this.render()},this.render()}landmarkInsertBeforeID(){const e=mapStyleLayerIDs.poi;return this.map.mapLibreMap.getLayer(e)?e:void 0}excludeLandmarkBuildingsFromBasemap(){LayerFilterComposer.for(this.map).setClause(this.basemapBuildingLayerID,"landmarks3d.buildings",NO_LANDMARK_FILTER_CLAUSE)}readBasemapBuildingsLook(){const e=this.map.mapLibreMap,i=e.getLayer(this.basemapBuildingLayerID);if(!i)return{};const t=i=>e.getPaintProperty(this.basemapBuildingLayerID,i),a=(e=>{if("string"==typeof e)return e;if(Array.isArray(e)&&"match"===e[0]){const i=e.at(-1);if("string"==typeof i)return i}return null})(t("fill-extrusion-color")),r=t("fill-extrusion-opacity"),s=t("fill-extrusion-vertical-gradient");return{...a&&{color:a},..."number"==typeof r&&{opacity:r},..."boolean"==typeof s&&{verticalGradient:s},...void 0!==i.minzoom&&{minZoom:i.minzoom},...void 0!==i.maxzoom&&{maxZoom:i.maxzoom}}}look(){const e=this.map.mapReady?this.readBasemapBuildingsLook():{};return((e,i)=>{const t={...STREET_LIGHT_LOOK,...i},a=i=>e[i]??t[i];return{visible:a("visible"),color:a("color"),opacity:a("opacity"),verticalGradient:a("verticalGradient"),minZoom:a("minZoom"),maxZoom:a("maxZoom")}})(this.config,e)}render(){const e=this.look();this.renderedLook=e,this.modelsLayer.visible=e.visible,this.modelsLayer.minzoom=e.minZoom,this.modelsLayer.maxzoom=e.maxZoom,this.modelsLayer.diffuseColor=e.color,this.modelsLayer.setOpacity(e.opacity),this.modelsLayer.setVerticalGradient(e.verticalGradient),this.map.mapReady&&this.map.mapLibreMap.triggerRepaint()}};export{Landmarks3D,ModelsLayer,ModelsSource,buildLandmarksTileURL,landmarks3DKnobCatalogue,landmarks3DKnobIds};
package/package.json CHANGED
@@ -2,12 +2,13 @@
2
2
  "name": "@tomtom-org/maps-sdk-plugin-landmarks-3d",
3
3
  "author": "TomTom International B.V.",
4
4
  "license": "See in LICENSE.txt",
5
- "version": "0.0.13",
5
+ "version": "0.1.0",
6
6
  "type": "module",
7
7
  "description": "Plugin to render TomTom Orbis 3D Landmarks (GLB tiles) on a TomTomMap with Three.js",
8
8
  "types": "dist/index.d.ts",
9
+ "sideEffects": false,
9
10
  "peerDependencies": {
10
- "@tomtom-org/maps-sdk": ">=0.64.0 <1.0.0 || >=1.0.0-rc.0 <1.0.0",
11
+ "@tomtom-org/maps-sdk": ">=1.0.0-rc.2 <1.0.0",
11
12
  "maplibre-gl": "^6.13.0",
12
13
  "three": "^0.170.0"
13
14
  },
@@ -22,7 +23,7 @@
22
23
  "vite": "8.3.3",
23
24
  "vitest": "^5.0.3",
24
25
  "@testing/core-utils": "0.0.0",
25
- "@tomtom-org/maps-sdk": "1.0.0-rc.2"
26
+ "@tomtom-org/maps-sdk": "1.0.0-rc.3"
26
27
  },
27
28
  "files": [
28
29
  "dist",