@tomtom-org/maps-sdk-plugin-landmarks-3d 0.0.12 → 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/LICENSE.txt +2 -2
- package/README.md +12 -10
- package/THIRD_PARTY.txt +1 -1
- package/dist/THIRD_PARTY.txt +1 -1
- package/dist/bundledTranscoder-IHtuhlDT.js +1 -0
- package/dist/index.d.ts +143 -57
- package/dist/index.es.js +1 -1
- package/dist/third-party.json +1 -1
- package/package.json +16 -8
- package/dist/bundledTranscoder-Dn27cuYm.js +0 -1
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/
|
|
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
|
-
*
|
|
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
|
|
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
|
/**
|
|
@@ -162,6 +218,7 @@ export declare class ModelsSource {
|
|
|
162
218
|
private readonly gltfLoader;
|
|
163
219
|
private readonly specification;
|
|
164
220
|
private readonly withCredentials;
|
|
221
|
+
private coveringTileKeys;
|
|
165
222
|
constructor(id: string, options: ModelsSourceSpecification);
|
|
166
223
|
onAdd(map: Map, renderer: WebGLRenderer): void;
|
|
167
224
|
updateTiles(): void;
|
|
@@ -180,6 +237,10 @@ export declare class ModelsSource {
|
|
|
180
237
|
* viewport, renders them as maplibre-style fill-extrusion buildings, and keeps its
|
|
181
238
|
* lighting in sync with the map style light.
|
|
182
239
|
*
|
|
240
|
+
* It draws whenever the map draws, and asks for a frame of its own only when a tile arrives,
|
|
241
|
+
* so a still map stays idle. After changing one of its fields yourself, call
|
|
242
|
+
* `triggerRepaint()` on the MapLibre map.
|
|
243
|
+
*
|
|
183
244
|
* @group Landmarks 3D
|
|
184
245
|
*/
|
|
185
246
|
export declare class ModelsLayer implements CustomLayerInterface {
|
|
@@ -201,11 +262,15 @@ export declare class ModelsLayer implements CustomLayerInterface {
|
|
|
201
262
|
private readonly fillExtrusionMaterial;
|
|
202
263
|
private readonly depthPrepassMaterial;
|
|
203
264
|
private readonly maskedFillExtrusionMaterials;
|
|
265
|
+
private readonly tilesMainMatrix;
|
|
266
|
+
private tilesStale;
|
|
267
|
+
private readonly markTilesStale;
|
|
204
268
|
constructor(options: ModelsLayerSpecification, source: ModelsSourceSpecification);
|
|
205
269
|
updateProperties(options: ModelsLayerSpecification): void;
|
|
206
270
|
setDirectionalLight(radius: number, phi: number, theta: number): void;
|
|
207
271
|
onAdd(map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
|
|
208
272
|
render(_gl: WebGLRenderingContext | WebGL2RenderingContext, options: CustomRenderMethodInput): void;
|
|
273
|
+
private updateTilesWhenStale;
|
|
209
274
|
private groundLandmarksOnTerrain;
|
|
210
275
|
private groundMeshOnTerrain;
|
|
211
276
|
private terrainSamplePoints;
|
|
@@ -225,35 +290,23 @@ export declare class ModelsLayer implements CustomLayerInterface {
|
|
|
225
290
|
//#endregion
|
|
226
291
|
//#region src/Landmarks3D.d.ts
|
|
227
292
|
/**
|
|
228
|
-
*
|
|
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
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* @group Landmarks 3D
|
|
302
|
+
*/
|
|
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.
|
|
229
306
|
*
|
|
230
307
|
* @group Landmarks 3D
|
|
231
308
|
*/
|
|
232
|
-
type Landmarks3DOptions = {
|
|
233
|
-
/**
|
|
234
|
-
* Initial display mode for the landmarks. To change it at runtime, call
|
|
235
|
-
* {@link Landmarks3D.setDisplayMode}.
|
|
236
|
-
* @defaultValue `'inherited'`
|
|
237
|
-
*/
|
|
238
|
-
displayMode?: Landmarks3DDisplayMode;
|
|
239
|
-
/**
|
|
240
|
-
* Whether the landmarks are initially visible. To change it at runtime, call
|
|
241
|
-
* {@link Landmarks3D.setVisible}.
|
|
242
|
-
* @defaultValue `true`
|
|
243
|
-
*/
|
|
244
|
-
visible?: boolean;
|
|
245
|
-
/**
|
|
246
|
-
* Minimum zoom at which landmarks are rendered.
|
|
247
|
-
* @defaultValue The basemap 3D building layer's minimum zoom, so landmarks appear together
|
|
248
|
-
* with it (falls back to `10` when that layer is absent).
|
|
249
|
-
*/
|
|
250
|
-
minZoom?: number;
|
|
251
|
-
/**
|
|
252
|
-
* Maximum zoom at which landmarks are rendered.
|
|
253
|
-
* @defaultValue The basemap 3D building layer's maximum zoom, so landmarks disappear together
|
|
254
|
-
* with it (falls back to `22` when that layer is absent).
|
|
255
|
-
*/
|
|
256
|
-
maxZoom?: number;
|
|
309
|
+
type Landmarks3DOptions = Landmarks3DConfig & {
|
|
257
310
|
/**
|
|
258
311
|
* Whether each tile request sends `credentials: 'include'`, so a proxy session
|
|
259
312
|
* cookie travels with it. By default, this tracks the SDK's proxy config,
|
|
@@ -273,9 +326,11 @@ type Landmarks3DOptions = {
|
|
|
273
326
|
* across map style changes.
|
|
274
327
|
*
|
|
275
328
|
* @remarks
|
|
276
|
-
* It requires an API key with Orbis 3D Landmarks entitlements.
|
|
277
|
-
*
|
|
278
|
-
*
|
|
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.
|
|
279
334
|
*
|
|
280
335
|
* @example
|
|
281
336
|
* ```typescript
|
|
@@ -284,7 +339,7 @@ type Landmarks3DOptions = {
|
|
|
284
339
|
* // assume `map` is your initialized TomTomMap instance
|
|
285
340
|
* const landmarks = new Landmarks3D(map);
|
|
286
341
|
*
|
|
287
|
-
*
|
|
342
|
+
* landmarks.updateConfig({ color: '#c8b28a' });
|
|
288
343
|
* ```
|
|
289
344
|
*
|
|
290
345
|
* @group Landmarks 3D
|
|
@@ -294,50 +349,81 @@ export declare class Landmarks3D {
|
|
|
294
349
|
private readonly layerID;
|
|
295
350
|
private readonly basemapBuildingLayerID;
|
|
296
351
|
private readonly modelsLayer;
|
|
297
|
-
private
|
|
298
|
-
private
|
|
299
|
-
private displayMode;
|
|
352
|
+
private config;
|
|
353
|
+
private renderedLook;
|
|
300
354
|
/**
|
|
301
355
|
* Creates the plugin bound to a TomTom map and starts rendering landmarks
|
|
302
356
|
* as soon as the map is ready.
|
|
303
357
|
*
|
|
304
358
|
* @param map - The TomTom map instance to render landmarks on.
|
|
359
|
+
* @param options - The knobs to start with, and how to request tiles.
|
|
305
360
|
*/
|
|
306
|
-
constructor(map: TomTomMap,
|
|
361
|
+
constructor(map: TomTomMap, { withCredentials, ...config }?: Landmarks3DOptions);
|
|
307
362
|
private installWhenReady;
|
|
308
363
|
/**
|
|
309
364
|
* The underlying MapLibre custom layer, for advanced use beyond this facade.
|
|
310
365
|
*/
|
|
311
366
|
get layer(): ModelsLayer;
|
|
312
367
|
/**
|
|
313
|
-
* The
|
|
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.
|
|
314
370
|
*/
|
|
315
|
-
|
|
371
|
+
getConfig(): Landmarks3DConfig;
|
|
316
372
|
/**
|
|
317
|
-
*
|
|
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.
|
|
318
379
|
*
|
|
319
|
-
* @
|
|
320
|
-
*
|
|
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.
|
|
321
397
|
*/
|
|
322
|
-
|
|
398
|
+
resetConfig(): void;
|
|
323
399
|
/**
|
|
324
|
-
* Whether the landmarks are
|
|
400
|
+
* Whether the landmarks are set to be shown: the `visible` knob, `true` unless set.
|
|
325
401
|
*/
|
|
326
402
|
isVisible(): boolean;
|
|
327
403
|
/**
|
|
328
|
-
* Shows or hides the landmarks
|
|
404
|
+
* Shows or hides the landmarks. Stored as `visible` in the configuration.
|
|
329
405
|
*
|
|
330
406
|
* @param visible - `true` to show landmarks, `false` to hide them.
|
|
331
|
-
* @returns A promise that resolves once the visibility has been applied.
|
|
332
407
|
*/
|
|
333
|
-
setVisible(visible: boolean):
|
|
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;
|
|
334
421
|
private install;
|
|
335
422
|
private landmarkInsertBeforeID;
|
|
336
|
-
private alignZoomRangeWithBasemapBuildings;
|
|
337
423
|
private excludeLandmarkBuildingsFromBasemap;
|
|
338
|
-
private
|
|
339
|
-
private
|
|
340
|
-
private
|
|
424
|
+
private readBasemapBuildingsLook;
|
|
425
|
+
private look;
|
|
426
|
+
private render;
|
|
341
427
|
}
|
|
342
428
|
//#endregion
|
|
343
429
|
//#region src/landmarksTileURL.d.ts
|
|
@@ -352,4 +438,4 @@ export declare class Landmarks3D {
|
|
|
352
438
|
*/
|
|
353
439
|
export declare const buildLandmarksTileURL: () => string;
|
|
354
440
|
//#endregion
|
|
355
|
-
export type {
|
|
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 d,FrontSide as m,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 b}from"three";import{MeshoptDecoder as w}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 P}from"three/examples/jsm/utils/BufferGeometryUtils.js";import{MercatorCoordinate as D}from"maplibre-gl";var S={diffuseColor:"hsl(38, 6%, 90%)",opacity:.7,verticalGradient:!0},z={diffuseColor:"hsl(210, 9%, 17%)",opacity:.7,verticalGradient:!0},T=()=>{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?d:m,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?d:m,colorWrite:!1})}},Z=e=>!0===e.isMesh,j=1/(2*Math.PI*6378137),V=Math.PI/6,$=class{type="models";id;minzoom;maxzoom;map;tiles;scene;loadedTiles=/* @__PURE__ */new Set;sceneOrigin=null;onTileLoaded;ktx2Loader;transcoderReady;gltfLoader;specification;withCredentials;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(w)}onAdd(e,i){this.map=e,this.ktx2Loader.detectSupport(i)}updateTiles(){if(0===this.tiles.length)return;const e=/* @__PURE__ */new Set;for(const i of this.coveringTiles())e.add(i.key),this.loadTile(i);this.showCoveringTilesOnly(e)}showCoveringTilesOnly(e){for(const i of this.scene.children)i.visible=e.has(i.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},this.scene.add(i),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=>{Z(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=>{Z(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(!Z(e))return;const i=e.material;i.alphaTest=.1,i.side=d,e.userData.originalMaterial=i,e.geometry=P(e.geometry,V),K(e)})},K=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;constructor(e,i){this.id=e.id,this.source=new $(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 b({canvas:e.getCanvas(),context:i,antialias:!0}),this.renderer.autoClear=!1,this.source.onAdd(e,this.renderer)}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.source.updateTiles(),this.groundLandmarksOnTerrain(),this.alignCameraToMap(i),this.renderer.resetState(),this.needsTranslucentPrepass()&&this.renderDepthPrepass(),this.applyMaterials(),this.renderer.render(this.scene,this.camera),this.map.triggerRepaint())}groundLandmarksOnTerrain(){for(const e of this.tiles.children)e.visible&&e.traverse(e=>{Z(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 D(.5+(a+e)*j,.5-(s+i)*j).toLngLat().toArray()),e.userData.terrainSamplePoints=i}return i}alignCameraToMap(e){const i=j/Math.cos(H(this.map.getCenter().lat)),t=.5+j*this.sceneOrigin.x,a=.5-j*this.sceneOrigin.y,s=new f;s.set(j,0,0,t,0,-j,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=>{Z(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=>{Z(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:[T()],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 z;case"light":return S;default:return{...S,...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,$ as ModelsSource,T 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/dist/third-party.json
CHANGED
package/package.json
CHANGED
|
@@ -2,25 +2,28 @@
|
|
|
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
|
|
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": ">=
|
|
11
|
-
"maplibre-gl": "^6.
|
|
11
|
+
"@tomtom-org/maps-sdk": ">=1.0.0-rc.2 <1.0.0",
|
|
12
|
+
"maplibre-gl": "^6.13.0",
|
|
12
13
|
"three": "^0.170.0"
|
|
13
14
|
},
|
|
14
15
|
"devDependencies": {
|
|
16
|
+
"@playwright/test": "1.60.0",
|
|
15
17
|
"@tsconfig/recommended": "^1.0.13",
|
|
16
18
|
"@types/node": "^26.6.4",
|
|
17
19
|
"@types/three": "^0.170.0",
|
|
18
|
-
"@vitest/coverage-v8": "^
|
|
19
|
-
"rolldown": "^1.2.
|
|
20
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
21
|
+
"rolldown": "^1.2.13",
|
|
20
22
|
"typescript": "^6.0.3",
|
|
21
|
-
"vite": "
|
|
22
|
-
"vitest": "^
|
|
23
|
-
"@
|
|
23
|
+
"vite": "8.3.3",
|
|
24
|
+
"vitest": "^5.0.3",
|
|
25
|
+
"@testing/core-utils": "0.0.0",
|
|
26
|
+
"@tomtom-org/maps-sdk": "1.0.0-rc.3"
|
|
24
27
|
},
|
|
25
28
|
"files": [
|
|
26
29
|
"dist",
|
|
@@ -41,6 +44,11 @@
|
|
|
41
44
|
"build:watch": "vite build --watch --config ../plugin-vite-config.ts",
|
|
42
45
|
"test": "vitest run",
|
|
43
46
|
"test:coverage": "vitest run --coverage",
|
|
47
|
+
"test:e2e": "playwright test --config e2e-tests/playwright.config.ts",
|
|
48
|
+
"test:e2e:ui": "playwright test --config e2e-tests/playwright.config.ts --ui",
|
|
49
|
+
"test:e2e:update-snapshots": "playwright test --config e2e-tests/playwright.config.ts --update-snapshots",
|
|
50
|
+
"test:e2e:server": "vite --config e2e-tests/vite.config.ts",
|
|
51
|
+
"test:e2e:install": "playwright install chromium",
|
|
44
52
|
"type-check": "tsc --noEmit"
|
|
45
53
|
}
|
|
46
54
|
}
|