@tileflow/dev 0.1.0-alpha.33 → 0.1.0-alpha.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/dist/artifacts.d.ts +3 -3
- package/dist/artifacts.js +4 -2
- package/dist/{chunk-WJOF6USY.js → chunk-2F7LOYQI.js} +3 -1
- package/dist/{chunk-QYU4OW5A.js → chunk-5RT4CHSK.js} +485 -73
- package/dist/devtools-browser.js +1 -1
- package/dist/fonts.d.ts +1 -1
- package/dist/{icons-BdOehQmB.d.ts → icons-BIHpw5Zy.d.ts} +15 -22
- package/dist/icons.d.ts +1 -1
- package/dist/icons.js +1 -3
- package/dist/{preview-assets-KGUICQHG.js → preview-assets-MT3RXZMN.js} +0 -51
- package/dist/preview.d.ts +1 -41
- package/dist/preview.js +109 -797
- package/dist/server.d.ts +1 -1
- package/dist/server.js +3 -3
- package/docs/bounded-vector-feature-inspection.md +1 -1
- package/docs/building-models.md +179 -0
- package/package.json +3 -4
package/dist/preview.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveThemeSelection
|
|
12
12
|
} from "@tileflow/core";
|
|
13
13
|
|
|
14
|
-
// src/
|
|
14
|
+
// src/bounded-response.ts
|
|
15
15
|
async function readBoundedTileflowResponse(response, maximumBytes) {
|
|
16
16
|
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1) {
|
|
17
17
|
throw new Error("invalid response byte limit");
|
|
@@ -51,94 +51,6 @@ async function readBoundedTileflowJsonResponse(response, maximumBytes) {
|
|
|
51
51
|
const bytes = await readBoundedTileflowResponse(response, maximumBytes);
|
|
52
52
|
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
53
53
|
}
|
|
54
|
-
function normalizeTileflowLandmarkManifest(candidate, manifestUrl) {
|
|
55
|
-
const value = candidate;
|
|
56
|
-
const portableId = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
57
|
-
const sha256 = /^[a-f0-9]{64}$/;
|
|
58
|
-
const axisConventions = /* @__PURE__ */ new Set(["EUN_Y_UP", "EUS_Y_UP", "ENU_Z_UP"]);
|
|
59
|
-
if (value?.schemaVersion !== 2 || typeof value.id !== "string" || !portableId.test(value.id) || !Array.isArray(value.archives) || value.archives.length > 64 || !Array.isArray(value.landmarks) || value.landmarks.length > 4096 || !Number.isInteger(value.minzoom) || value.minzoom < 0 || value.minzoom > 24 || !Number.isInteger(value.maximumVisibleModels) || value.maximumVisibleModels < 1 || value.maximumVisibleModels > 64) {
|
|
60
|
-
throw new Error("invalid landmark manifest");
|
|
61
|
-
}
|
|
62
|
-
const minzoom = value.minzoom;
|
|
63
|
-
const maximumVisibleModels = value.maximumVisibleModels;
|
|
64
|
-
const maximumCachedModels = value.maximumCachedModels === void 0 ? Math.min(128, Math.max(maximumVisibleModels, maximumVisibleModels * 2)) : value.maximumCachedModels;
|
|
65
|
-
if (!Number.isInteger(maximumCachedModels) || maximumCachedModels < maximumVisibleModels || maximumCachedModels > 128) {
|
|
66
|
-
throw new Error("invalid landmark cache limit");
|
|
67
|
-
}
|
|
68
|
-
const archiveIds = /* @__PURE__ */ new Set();
|
|
69
|
-
const archiveMap = /* @__PURE__ */ new Map();
|
|
70
|
-
for (const input of value.archives) {
|
|
71
|
-
const archive = input;
|
|
72
|
-
if (typeof archive?.id !== "string" || !portableId.test(archive.id) || archiveIds.has(archive.id) || typeof archive.url !== "string" || archive.url.length < 1 || archive.url.length > 2048 || !Number.isSafeInteger(archive.bytes) || archive.bytes < 1 || archive.bytes > 8 * 1024 * 1024 * 1024 || typeof archive.sha256 !== "string" || !sha256.test(archive.sha256)) {
|
|
73
|
-
throw new Error("invalid landmark archive");
|
|
74
|
-
}
|
|
75
|
-
const url = new URL(archive.url, manifestUrl);
|
|
76
|
-
if (url.protocol !== "http:" && url.protocol !== "https:" || url.username !== "" || url.password !== "") {
|
|
77
|
-
throw new Error("invalid landmark archive URL");
|
|
78
|
-
}
|
|
79
|
-
const normalized = {
|
|
80
|
-
bytes: archive.bytes,
|
|
81
|
-
id: archive.id,
|
|
82
|
-
sha256: archive.sha256,
|
|
83
|
-
url: url.toString()
|
|
84
|
-
};
|
|
85
|
-
archiveIds.add(archive.id);
|
|
86
|
-
archiveMap.set(archive.id, normalized);
|
|
87
|
-
}
|
|
88
|
-
const landmarkIds = /* @__PURE__ */ new Set();
|
|
89
|
-
const landmarks = value.landmarks.map((input) => {
|
|
90
|
-
const landmark = input;
|
|
91
|
-
const center = landmark?.center;
|
|
92
|
-
const bounds = landmark?.bounds;
|
|
93
|
-
if (typeof landmark?.id !== "string" || !portableId.test(landmark.id) || landmarkIds.has(landmark.id) || !Array.isArray(center) || center.length !== 2 || !center.every(Number.isFinite) || center[0] < -180 || center[0] > 180 || center[1] < -90 || center[1] > 90 || !Array.isArray(bounds) || bounds.length !== 4 || !bounds.every(Number.isFinite) || bounds[0] < -180 || bounds[2] > 180 || bounds[1] < -90 || bounds[3] > 90 || bounds[0] > bounds[2] || bounds[1] > bounds[3] || center[0] < bounds[0] || center[0] > bounds[2] || center[1] < bounds[1] || center[1] > bounds[3] || landmark.priority !== void 0 && !Number.isFinite(landmark.priority) || !Array.isArray(landmark.models) || landmark.models.length < 1 || landmark.models.length > 8) {
|
|
94
|
-
throw new Error("invalid landmark entry");
|
|
95
|
-
}
|
|
96
|
-
landmarkIds.add(landmark.id);
|
|
97
|
-
const models = landmark.models.map((inputModel) => {
|
|
98
|
-
const model = inputModel;
|
|
99
|
-
if (!model) throw new Error("invalid landmark model");
|
|
100
|
-
const archive = typeof model.archiveId === "string" ? archiveMap.get(model.archiveId) : void 0;
|
|
101
|
-
if (!archive || !Number.isInteger(model?.minzoom) || model.minzoom < minzoom || model.minzoom > 24 || !Number.isInteger(model.z) || model.z < 0 || model.z > 26 || !Number.isInteger(model.x) || model.x < 0 || model.x >= 2 ** model.z || !Number.isInteger(model.y) || model.y < 0 || model.y >= 2 ** model.z || !Number.isSafeInteger(model.bytes) || model.bytes < 1 || model.bytes > 64 * 1024 * 1024 || typeof model.sha256 !== "string" || !sha256.test(model.sha256) || typeof model.axisConvention !== "string" || !axisConventions.has(model.axisConvention)) {
|
|
102
|
-
throw new Error("invalid landmark model");
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
archive,
|
|
106
|
-
archiveId: model.archiveId,
|
|
107
|
-
axisConvention: model.axisConvention,
|
|
108
|
-
bytes: model.bytes,
|
|
109
|
-
key: [archive.sha256, model.z, model.x, model.y, model.sha256].join(":"),
|
|
110
|
-
minzoom: model.minzoom,
|
|
111
|
-
sha256: model.sha256,
|
|
112
|
-
x: model.x,
|
|
113
|
-
y: model.y,
|
|
114
|
-
z: model.z
|
|
115
|
-
};
|
|
116
|
-
});
|
|
117
|
-
models.sort((left, right) => left.minzoom - right.minzoom || left.key.localeCompare(right.key));
|
|
118
|
-
if (new Set(models.map((model) => model.minzoom)).size !== models.length) {
|
|
119
|
-
throw new Error("duplicate landmark LOD zoom");
|
|
120
|
-
}
|
|
121
|
-
if (models[0].minzoom !== minzoom) {
|
|
122
|
-
throw new Error("landmark requires a base model");
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
bounds,
|
|
126
|
-
center,
|
|
127
|
-
id: landmark.id,
|
|
128
|
-
models,
|
|
129
|
-
priority: landmark.priority ?? 0
|
|
130
|
-
};
|
|
131
|
-
});
|
|
132
|
-
return {
|
|
133
|
-
archives: [...archiveMap.values()],
|
|
134
|
-
id: value.id,
|
|
135
|
-
landmarks,
|
|
136
|
-
maximumCachedModels,
|
|
137
|
-
maximumVisibleModels,
|
|
138
|
-
minzoom,
|
|
139
|
-
schemaVersion: 2
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
54
|
|
|
143
55
|
// src/preview-scene.ts
|
|
144
56
|
var previewSceneClient = String.raw`
|
|
@@ -230,6 +142,42 @@ function createPreviewSceneThemes(map, readSun) {
|
|
|
230
142
|
};
|
|
231
143
|
}
|
|
232
144
|
|
|
145
|
+
// src/preview-style-reads.ts
|
|
146
|
+
function createPreviewStyleReads(map) {
|
|
147
|
+
let snapshot;
|
|
148
|
+
map.on("style.load", () => {
|
|
149
|
+
snapshot = void 0;
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
metadata() {
|
|
153
|
+
if (!snapshot || snapshot.style !== map.style) {
|
|
154
|
+
const style = map.getStyle();
|
|
155
|
+
if (!style) return void 0;
|
|
156
|
+
snapshot = { style: map.style, metadata: style.metadata };
|
|
157
|
+
}
|
|
158
|
+
return snapshot.metadata;
|
|
159
|
+
},
|
|
160
|
+
// Layers change between style loads, so they are read live, in the shape of the style's own
|
|
161
|
+
// layers. Custom layers have no style definition; getStyle leaves them out too.
|
|
162
|
+
layers() {
|
|
163
|
+
const layers = [];
|
|
164
|
+
for (const id of map.getLayersOrder()) {
|
|
165
|
+
const layer = map.getLayer(id);
|
|
166
|
+
if (!layer || layer.type === "custom") continue;
|
|
167
|
+
layers.push({
|
|
168
|
+
id: layer.id,
|
|
169
|
+
type: layer.type,
|
|
170
|
+
metadata: layer.metadata,
|
|
171
|
+
"source-layer": layer.sourceLayer,
|
|
172
|
+
minzoom: layer.minzoom,
|
|
173
|
+
maxzoom: layer.maxzoom
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return layers;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
233
181
|
// src/preview-html.ts
|
|
234
182
|
function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticPreview, fontFaces = [], fitViewport = false, controls) {
|
|
235
183
|
const styleUrl = preview ? `${basePath}/styles/${preview.mapName}/${preview.themeName}.json${controls ? `?${new URLSearchParams({ controls })}` : ""}` : void 0;
|
|
@@ -341,14 +289,15 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
341
289
|
<div class="badge" id="badge">Tileflow preview</div>
|
|
342
290
|
<div class="status" id="status" role="status"></div>
|
|
343
291
|
<div id="map"></div>
|
|
344
|
-
<script type="importmap">{"imports":{"
|
|
292
|
+
<script type="importmap">{"imports":{"three":"${basePath}/__runtime/three.module.js"}}</script>
|
|
345
293
|
<script type="module">
|
|
346
294
|
import * as maplibregl from "${basePath}/__runtime/maplibre-gl.mjs";
|
|
347
295
|
import {createPreviewObserver} from "${basePath}/__runtime/devtools-observer.js";
|
|
348
296
|
import {loadTileflowStyleFonts} from "${basePath}/__runtime/tileflow-browser.js";
|
|
349
297
|
import {registerTileflowContourProtocol} from "${basePath}/__runtime/tileflow-browser.js";
|
|
350
298
|
import {registerTileflowPmtilesProtocol} from "${basePath}/__runtime/tileflow-browser.js";
|
|
351
|
-
import {
|
|
299
|
+
import {attachTileflowNotices} from "${basePath}/__runtime/tileflow-browser.js";
|
|
300
|
+
import {createTileflowMap3D} from "${basePath}/__runtime/tileflow-browser.js";
|
|
352
301
|
import {readTileflowSunPreset, resolveTileflowSun, tileflowDefaultSunPreset} from "${basePath}/__runtime/tileflow-browser.js";
|
|
353
302
|
|
|
354
303
|
maplibregl.setWorkerUrl("${basePath}/__runtime/maplibre-gl-worker.mjs");
|
|
@@ -358,7 +307,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
358
307
|
if (window.parent !== window) document.documentElement.dataset.devtoolsEmbedded = "true";
|
|
359
308
|
|
|
360
309
|
const initialStatus = ${JSON.stringify(initialStatus)};
|
|
361
|
-
const normalizeTileflowLandmarkManifest = ${normalizeTileflowLandmarkManifest.toString()};
|
|
362
310
|
const readBoundedTileflowResponse = ${readBoundedTileflowResponse.toString()};
|
|
363
311
|
const readBoundedTileflowJsonResponse = ${readBoundedTileflowJsonResponse.toString()};
|
|
364
312
|
const initialGeneration = initialStatus.generation;
|
|
@@ -408,16 +356,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
408
356
|
}
|
|
409
357
|
let threeDimensionalEnabled = readToggleFromUrl("buildings3d", true);
|
|
410
358
|
let treesEnabled = readToggleFromUrl("trees3d", true);
|
|
411
|
-
const landmarkState = {
|
|
412
|
-
active: [],
|
|
413
|
-
cacheLimit: undefined,
|
|
414
|
-
enabled: threeDimensionalEnabled,
|
|
415
|
-
manifestId: undefined,
|
|
416
|
-
loaded: [],
|
|
417
|
-
loading: [],
|
|
418
|
-
errors: []
|
|
419
|
-
};
|
|
420
|
-
globalThis.__tileflowLandmarkState = landmarkState;
|
|
421
359
|
const buildingWireframeMetrics = {
|
|
422
360
|
buildingCount: 0,
|
|
423
361
|
refreshMilliseconds: undefined,
|
|
@@ -426,22 +364,11 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
426
364
|
};
|
|
427
365
|
globalThis.__tileflowBuildingWireframeMetrics = buildingWireframeMetrics;
|
|
428
366
|
let THREE;
|
|
429
|
-
let GLTFLoader;
|
|
430
|
-
let DRACOLoader;
|
|
431
|
-
let MeshoptDecoder;
|
|
432
367
|
let LineMaterial;
|
|
433
368
|
let LineSegments2;
|
|
434
369
|
let LineSegmentsGeometry;
|
|
435
|
-
let PMTiles;
|
|
436
|
-
let FetchSource;
|
|
437
|
-
let landmarkDracoLoader;
|
|
438
370
|
let threeCoreRuntimePromise;
|
|
439
371
|
let buildingWireframeRuntimePromise;
|
|
440
|
-
let landmarkRuntimePromise;
|
|
441
|
-
let pmtilesRuntimePromise;
|
|
442
|
-
const landmarkManifestPromises = new Map();
|
|
443
|
-
const landmarkManifestMaximumBytes = 1024 * 1024;
|
|
444
|
-
const landmarkManifestTimeoutMs = 10000;
|
|
445
372
|
|
|
446
373
|
function loadThreeCoreRuntime() {
|
|
447
374
|
if (!threeCoreRuntimePromise) {
|
|
@@ -470,89 +397,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
470
397
|
return buildingWireframeRuntimePromise;
|
|
471
398
|
}
|
|
472
399
|
|
|
473
|
-
function loadLandmarkRuntime() {
|
|
474
|
-
if (!landmarkRuntimePromise) {
|
|
475
|
-
landmarkRuntimePromise = Promise.all([
|
|
476
|
-
loadThreeCoreRuntime(),
|
|
477
|
-
import("${basePath}/__runtime/three-addons/loaders/GLTFLoader.js"),
|
|
478
|
-
import("${basePath}/__runtime/three-addons/loaders/DRACOLoader.js"),
|
|
479
|
-
import("${basePath}/__runtime/three-addons/libs/meshopt_decoder.module.js"),
|
|
480
|
-
loadPmtilesRuntime()
|
|
481
|
-
]).then(([, gltfLoaderModule, dracoLoaderModule, meshoptDecoderModule, pmtilesModule]) => {
|
|
482
|
-
GLTFLoader = gltfLoaderModule.GLTFLoader;
|
|
483
|
-
DRACOLoader = dracoLoaderModule.DRACOLoader;
|
|
484
|
-
MeshoptDecoder = meshoptDecoderModule.MeshoptDecoder;
|
|
485
|
-
PMTiles = pmtilesModule.PMTiles;
|
|
486
|
-
FetchSource = pmtilesModule.FetchSource;
|
|
487
|
-
landmarkDracoLoader = new DRACOLoader()
|
|
488
|
-
.setDecoderPath("${basePath}/__runtime/three-addons/libs/draco/gltf/")
|
|
489
|
-
.setDecoderConfig({type: "wasm"});
|
|
490
|
-
landmarkDracoLoader.preload();
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
return landmarkRuntimePromise;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function loadPmtilesRuntime() {
|
|
497
|
-
if (!pmtilesRuntimePromise) {
|
|
498
|
-
pmtilesRuntimePromise = import("${basePath}/__runtime/pmtiles.js").then((pmtilesModule) => {
|
|
499
|
-
PMTiles = pmtilesModule.PMTiles;
|
|
500
|
-
FetchSource = pmtilesModule.FetchSource;
|
|
501
|
-
return pmtilesModule;
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
return pmtilesRuntimePromise;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function waitForLandmarkManifest(promise, signal) {
|
|
508
|
-
if (!signal) return promise;
|
|
509
|
-
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
510
|
-
return new Promise((resolve, reject) => {
|
|
511
|
-
const abort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
512
|
-
signal.addEventListener("abort", abort, {once: true});
|
|
513
|
-
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function loadLandmarkManifestCandidate(manifestUrl, signal) {
|
|
518
|
-
const existing = landmarkManifestPromises.get(manifestUrl);
|
|
519
|
-
if (existing) return waitForLandmarkManifest(existing, signal);
|
|
520
|
-
const resolvedUrl = new URL(manifestUrl, location.href);
|
|
521
|
-
if (
|
|
522
|
-
!["http:", "https:"].includes(resolvedUrl.protocol) ||
|
|
523
|
-
resolvedUrl.username ||
|
|
524
|
-
resolvedUrl.password
|
|
525
|
-
) throw new Error("invalid landmark manifest URL");
|
|
526
|
-
const controller = new AbortController();
|
|
527
|
-
const timeout = setTimeout(() => controller.abort(), landmarkManifestTimeoutMs);
|
|
528
|
-
const pending = (async () => {
|
|
529
|
-
try {
|
|
530
|
-
const response = await fetch(resolvedUrl, {
|
|
531
|
-
credentials: "include",
|
|
532
|
-
signal: controller.signal
|
|
533
|
-
});
|
|
534
|
-
if (!response.ok) throw new Error("HTTP " + response.status);
|
|
535
|
-
return readBoundedTileflowJsonResponse(response, landmarkManifestMaximumBytes);
|
|
536
|
-
} finally {
|
|
537
|
-
clearTimeout(timeout);
|
|
538
|
-
}
|
|
539
|
-
})();
|
|
540
|
-
landmarkManifestPromises.set(manifestUrl, pending);
|
|
541
|
-
pending.catch(() => {
|
|
542
|
-
if (landmarkManifestPromises.get(manifestUrl) === pending) {
|
|
543
|
-
landmarkManifestPromises.delete(manifestUrl);
|
|
544
|
-
}
|
|
545
|
-
});
|
|
546
|
-
return waitForLandmarkManifest(pending, signal);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
function loadLandmarkPrerequisites(manifestUrl, signal) {
|
|
550
|
-
return Promise.all([
|
|
551
|
-
loadLandmarkRuntime(),
|
|
552
|
-
loadLandmarkManifestCandidate(manifestUrl, signal)
|
|
553
|
-
]);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
400
|
function percentile(values, percentileValue) {
|
|
557
401
|
if (values.length === 0) return undefined;
|
|
558
402
|
const sorted = [...values].sort((left, right) => left - right);
|
|
@@ -1020,7 +864,10 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
1020
864
|
}
|
|
1021
865
|
|
|
1022
866
|
function updateGlobeBackdrop(map) {
|
|
1023
|
-
|
|
867
|
+
// getProjection reads the style's projection without serializing the style.
|
|
868
|
+
const projection = map.getProjection
|
|
869
|
+
? map.getProjection()?.type
|
|
870
|
+
: map.getStyle?.()?.projection?.type;
|
|
1024
871
|
const container = map.getContainer?.();
|
|
1025
872
|
if (!container || !projection) return;
|
|
1026
873
|
if (projection !== "globe") {
|
|
@@ -1035,7 +882,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
1035
882
|
// Uses the map's declared atmosphere for the preview's space and rim, so a themed globe shows
|
|
1036
883
|
// its own colours; maps without one keep the default backdrop.
|
|
1037
884
|
function applyAtmosphereBackdrop(map, container) {
|
|
1038
|
-
const atmosphere = map.
|
|
885
|
+
const atmosphere = styleReads(map).metadata()?.["tileflow:atmosphere"];
|
|
1039
886
|
const rgba = (hex, alpha) => {
|
|
1040
887
|
const match = /^#([0-9a-f]{6})$/i.exec(String(hex ?? ""));
|
|
1041
888
|
if (!match) return undefined;
|
|
@@ -1511,530 +1358,44 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
1511
1358
|
return zoom >= minzoom && zoom < maxzoom;
|
|
1512
1359
|
}
|
|
1513
1360
|
|
|
1514
|
-
|
|
1515
|
-
const manifestUrl = configLayer.metadata?.["tileflow:landmark-manifest-url"];
|
|
1516
|
-
const {minzoom: landmarkMinimumZoom, maxzoom: landmarkMaximumZoom} =
|
|
1517
|
-
styleLayerZoomRange(configLayer);
|
|
1518
|
-
const abortController = new AbortController();
|
|
1519
|
-
const loaded = new Map();
|
|
1520
|
-
const loading = new Map();
|
|
1521
|
-
const archiveReaders = new Map();
|
|
1522
|
-
const activeLandmarkIds = new Set();
|
|
1523
|
-
let cacheClock = 0;
|
|
1524
|
-
let camera;
|
|
1525
|
-
let renderer;
|
|
1526
|
-
let scene;
|
|
1527
|
-
let group;
|
|
1528
|
-
let sceneOriginMercator;
|
|
1529
|
-
let sceneElevation = 0;
|
|
1530
|
-
let manifest;
|
|
1531
|
-
let fallbackSignature;
|
|
1532
|
-
let queuedRefresh;
|
|
1533
|
-
const mapMatrix = new THREE.Matrix4();
|
|
1534
|
-
const sceneMatrix = new THREE.Matrix4();
|
|
1535
|
-
|
|
1536
|
-
function overlapsViewport(bounds, paddingRatio = 0) {
|
|
1537
|
-
const viewport = map.getBounds();
|
|
1538
|
-
const longitudePadding = Math.abs(viewport.getEast() - viewport.getWest()) * paddingRatio;
|
|
1539
|
-
const latitudePadding = Math.abs(viewport.getNorth() - viewport.getSouth()) * paddingRatio;
|
|
1540
|
-
return !(
|
|
1541
|
-
bounds[2] < viewport.getWest() - longitudePadding ||
|
|
1542
|
-
bounds[0] > viewport.getEast() + longitudePadding ||
|
|
1543
|
-
bounds[3] < viewport.getSouth() - latitudePadding ||
|
|
1544
|
-
bounds[1] > viewport.getNorth() + latitudePadding
|
|
1545
|
-
);
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
function updateLandmarkFallbackFilters() {
|
|
1549
|
-
const readyIds = [...activeLandmarkIds]
|
|
1550
|
-
.filter((id) => loaded.has(id))
|
|
1551
|
-
.sort();
|
|
1552
|
-
const signature = JSON.stringify(readyIds);
|
|
1553
|
-
if (signature === fallbackSignature) return;
|
|
1554
|
-
fallbackSignature = signature;
|
|
1555
|
-
for (const fallback of fallbackLayers) {
|
|
1556
|
-
if (!map.getLayer(fallback.id)) continue;
|
|
1557
|
-
const spatialExclusions = (fallback.spatialFallbacks ?? [])
|
|
1558
|
-
.filter((entry) => readyIds.includes(entry.id))
|
|
1559
|
-
.map((entry) => ["!", ["within", {
|
|
1560
|
-
type: "Polygon",
|
|
1561
|
-
coordinates: [[
|
|
1562
|
-
[entry.bounds[0], entry.bounds[1]],
|
|
1563
|
-
[entry.bounds[2], entry.bounds[1]],
|
|
1564
|
-
[entry.bounds[2], entry.bounds[3]],
|
|
1565
|
-
[entry.bounds[0], entry.bounds[3]],
|
|
1566
|
-
[entry.bounds[0], entry.bounds[1]]
|
|
1567
|
-
]]
|
|
1568
|
-
}]]);
|
|
1569
|
-
map.setFilter(
|
|
1570
|
-
fallback.id,
|
|
1571
|
-
readyIds.length === 0 && spatialExclusions.length === 0
|
|
1572
|
-
? (fallback.filter ?? null)
|
|
1573
|
-
: [
|
|
1574
|
-
"all",
|
|
1575
|
-
...(fallback.filter ? [fallback.filter] : []),
|
|
1576
|
-
[
|
|
1577
|
-
"!",
|
|
1578
|
-
["in", ["get", "landmark_id"], ["literal", readyIds]]
|
|
1579
|
-
],
|
|
1580
|
-
...spatialExclusions
|
|
1581
|
-
]
|
|
1582
|
-
);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
function queueRefresh() {
|
|
1587
|
-
if (queuedRefresh !== undefined) return;
|
|
1588
|
-
queuedRefresh = setTimeout(() => {
|
|
1589
|
-
queuedRefresh = undefined;
|
|
1590
|
-
refresh();
|
|
1591
|
-
}, 100);
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
function ensureSceneOrigin(elevation) {
|
|
1595
|
-
if (sceneOriginMercator) return;
|
|
1596
|
-
sceneElevation = elevation;
|
|
1597
|
-
sceneOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(
|
|
1598
|
-
map.getCenter(),
|
|
1599
|
-
elevation
|
|
1600
|
-
);
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
function localPosition(lng, lat, elevation) {
|
|
1604
|
-
const mercator = maplibregl.MercatorCoordinate.fromLngLat([lng, lat]);
|
|
1605
|
-
const mercatorPerMeter = sceneOriginMercator.meterInMercatorCoordinateUnits();
|
|
1606
|
-
return new THREE.Vector3(
|
|
1607
|
-
(mercator.x - sceneOriginMercator.x) / mercatorPerMeter,
|
|
1608
|
-
elevation - sceneElevation,
|
|
1609
|
-
(sceneOriginMercator.y - mercator.y) / mercatorPerMeter
|
|
1610
|
-
);
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
function disposeModel(model) {
|
|
1614
|
-
const disposed = new Set();
|
|
1615
|
-
group?.remove(model);
|
|
1616
|
-
for (const texture of model.userData?.tileflowDetachedTextures ?? []) {
|
|
1617
|
-
if (!texture?.isTexture || disposed.has(texture)) continue;
|
|
1618
|
-
disposed.add(texture);
|
|
1619
|
-
texture.dispose();
|
|
1620
|
-
}
|
|
1621
|
-
for (const material of model.userData?.tileflowDetachedMaterials ?? []) {
|
|
1622
|
-
if (!material || disposed.has(material)) continue;
|
|
1623
|
-
for (const value of Object.values(material)) {
|
|
1624
|
-
if (value?.isTexture && !disposed.has(value)) {
|
|
1625
|
-
disposed.add(value);
|
|
1626
|
-
value.dispose();
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
disposed.add(material);
|
|
1630
|
-
material.dispose?.();
|
|
1631
|
-
}
|
|
1632
|
-
model.traverse((object) => {
|
|
1633
|
-
if (object.geometry && !disposed.has(object.geometry)) {
|
|
1634
|
-
disposed.add(object.geometry);
|
|
1635
|
-
object.geometry.dispose?.();
|
|
1636
|
-
}
|
|
1637
|
-
const materials = Array.isArray(object.material)
|
|
1638
|
-
? object.material
|
|
1639
|
-
: [object.material];
|
|
1640
|
-
for (const material of materials) {
|
|
1641
|
-
if (!material || disposed.has(material)) continue;
|
|
1642
|
-
for (const value of Object.values(material)) {
|
|
1643
|
-
if (value?.isTexture && !disposed.has(value)) {
|
|
1644
|
-
disposed.add(value);
|
|
1645
|
-
value.dispose();
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
disposed.add(material);
|
|
1649
|
-
material.dispose?.();
|
|
1650
|
-
}
|
|
1651
|
-
});
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
function harmonizeLandmarkModel(model) {
|
|
1655
|
-
const detachedMaterials = new Set();
|
|
1656
|
-
const uniformMaterial = new THREE.MeshStandardMaterial({
|
|
1657
|
-
alphaTest: 0,
|
|
1658
|
-
color: "#EEE4D4",
|
|
1659
|
-
depthWrite: true,
|
|
1660
|
-
metalness: 0,
|
|
1661
|
-
opacity: 1,
|
|
1662
|
-
roughness: 0.9,
|
|
1663
|
-
side: THREE.DoubleSide,
|
|
1664
|
-
transparent: false
|
|
1665
|
-
});
|
|
1666
|
-
model.traverse((object) => {
|
|
1667
|
-
if (!object.isMesh) return;
|
|
1668
|
-
const originalMaterials = Array.isArray(object.material)
|
|
1669
|
-
? object.material
|
|
1670
|
-
: [object.material];
|
|
1671
|
-
for (const material of originalMaterials) {
|
|
1672
|
-
if (material) detachedMaterials.add(material);
|
|
1673
|
-
}
|
|
1674
|
-
object.material = Array.isArray(object.material)
|
|
1675
|
-
? object.material.map(() => uniformMaterial)
|
|
1676
|
-
: uniformMaterial;
|
|
1677
|
-
});
|
|
1678
|
-
model.userData.tileflowDetachedMaterials = [...detachedMaterials];
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
function normalizeLandmarkAxes(sourceModel, axisConvention) {
|
|
1682
|
-
if (axisConvention === "EUN_Y_UP") return sourceModel;
|
|
1683
|
-
if (axisConvention === "ENU_Z_UP") {
|
|
1684
|
-
sourceModel.rotateX(-Math.PI / 2);
|
|
1685
|
-
}
|
|
1686
|
-
const normalizedModel = new THREE.Group();
|
|
1687
|
-
normalizedModel.scale.z = -1;
|
|
1688
|
-
normalizedModel.add(sourceModel);
|
|
1689
|
-
return normalizedModel;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
function unloadLandmark(id) {
|
|
1693
|
-
const entry = loaded.get(id);
|
|
1694
|
-
if (!entry) return;
|
|
1695
|
-
disposeModel(entry.model);
|
|
1696
|
-
loaded.delete(id);
|
|
1697
|
-
landmarkState.loaded = [...loaded.keys()];
|
|
1698
|
-
fallbackSignature = undefined;
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
function enforceCacheLimit() {
|
|
1702
|
-
if (!manifest || loaded.size <= manifest.maximumCachedModels) return;
|
|
1703
|
-
const evictionCandidates = [...loaded.entries()]
|
|
1704
|
-
.filter(([id]) => !activeLandmarkIds.has(id))
|
|
1705
|
-
.sort(
|
|
1706
|
-
([leftId, left], [rightId, right]) =>
|
|
1707
|
-
left.lastUsed - right.lastUsed || leftId.localeCompare(rightId)
|
|
1708
|
-
);
|
|
1709
|
-
while (
|
|
1710
|
-
loaded.size > manifest.maximumCachedModels &&
|
|
1711
|
-
evictionCandidates.length > 0
|
|
1712
|
-
) {
|
|
1713
|
-
unloadLandmark(evictionCandidates.shift()[0]);
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
function landmarkModelAtZoom(landmark, zoom) {
|
|
1718
|
-
let selected = landmark.models[0];
|
|
1719
|
-
for (const candidate of landmark.models) {
|
|
1720
|
-
if (candidate.minzoom > zoom) break;
|
|
1721
|
-
selected = candidate;
|
|
1722
|
-
}
|
|
1723
|
-
return selected;
|
|
1724
|
-
}
|
|
1725
|
-
|
|
1726
|
-
async function sha256Hex(bytes) {
|
|
1727
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
1728
|
-
return [...new Uint8Array(digest)]
|
|
1729
|
-
.map((value) => value.toString(16).padStart(2, "0"))
|
|
1730
|
-
.join("");
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
function archiveReader(archive) {
|
|
1734
|
-
let reader = archiveReaders.get(archive.url);
|
|
1735
|
-
if (!reader) {
|
|
1736
|
-
reader = new PMTiles(
|
|
1737
|
-
new FetchSource(archive.url, new Headers(), "include")
|
|
1738
|
-
);
|
|
1739
|
-
archiveReaders.set(archive.url, reader);
|
|
1740
|
-
}
|
|
1741
|
-
return reader;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
async function loadLandmarkBytes(modelDefinition) {
|
|
1745
|
-
const tile = await archiveReader(modelDefinition.archive).getZxy(
|
|
1746
|
-
modelDefinition.z,
|
|
1747
|
-
modelDefinition.x,
|
|
1748
|
-
modelDefinition.y,
|
|
1749
|
-
abortController.signal
|
|
1750
|
-
);
|
|
1751
|
-
if (!tile) throw new Error("landmark entry is missing from PMTiles");
|
|
1752
|
-
if (tile.data.byteLength !== modelDefinition.bytes) {
|
|
1753
|
-
throw new Error("landmark entry has an unexpected byte length");
|
|
1754
|
-
}
|
|
1755
|
-
if ((await sha256Hex(tile.data)) !== modelDefinition.sha256) {
|
|
1756
|
-
throw new Error("landmark entry has an unexpected SHA-256");
|
|
1757
|
-
}
|
|
1758
|
-
return tile.data;
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
function loadLandmark(landmark, requestedModel) {
|
|
1762
|
-
const cached = loaded.get(landmark.id);
|
|
1763
|
-
const nextModel = cached ? requestedModel : landmark.models[0];
|
|
1764
|
-
if (cached?.modelKey === nextModel.key) {
|
|
1765
|
-
cached.lastUsed = ++cacheClock;
|
|
1766
|
-
cached.model.visible = activeLandmarkIds.has(landmark.id);
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
if (cached) {
|
|
1770
|
-
// Keep the previous LOD visible until its replacement has loaded.
|
|
1771
|
-
cached.lastUsed = ++cacheClock;
|
|
1772
|
-
cached.model.visible = activeLandmarkIds.has(landmark.id);
|
|
1773
|
-
}
|
|
1774
|
-
if (loading.has(landmark.id)) return;
|
|
1775
|
-
loading.set(landmark.id, nextModel.key);
|
|
1776
|
-
landmarkState.loading = [...loading.keys()];
|
|
1777
|
-
const loader = new GLTFLoader();
|
|
1778
|
-
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
1779
|
-
loader.setDRACOLoader(landmarkDracoLoader);
|
|
1780
|
-
loader.setWithCredentials(true);
|
|
1781
|
-
const fail = (error) => {
|
|
1782
|
-
if (loading.get(landmark.id) !== nextModel.key) return;
|
|
1783
|
-
loading.delete(landmark.id);
|
|
1784
|
-
landmarkState.loading = [...loading.keys()];
|
|
1785
|
-
landmarkState.errors = [
|
|
1786
|
-
...landmarkState.errors.filter((item) => item.id !== landmark.id),
|
|
1787
|
-
{id: landmark.id, message: String(error)}
|
|
1788
|
-
];
|
|
1789
|
-
console.warn("Tileflow landmark failed to load", landmark.id, error);
|
|
1790
|
-
};
|
|
1791
|
-
void loadLandmarkBytes(nextModel).then((bytes) => {
|
|
1792
|
-
if (loading.get(landmark.id) !== nextModel.key) return;
|
|
1793
|
-
loader.parse(
|
|
1794
|
-
bytes,
|
|
1795
|
-
new URL(".", nextModel.archive.url).toString(),
|
|
1796
|
-
(gltf) => {
|
|
1797
|
-
if (loading.get(landmark.id) !== nextModel.key) {
|
|
1798
|
-
disposeModel(gltf.scene);
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
loading.delete(landmark.id);
|
|
1802
|
-
landmarkState.loading = [...loading.keys()];
|
|
1803
|
-
if (abortController.signal.aborted) {
|
|
1804
|
-
disposeModel(gltf.scene);
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
const elevation = map.queryTerrainElevation(landmark.center) ?? 0;
|
|
1808
|
-
ensureSceneOrigin(elevation);
|
|
1809
|
-
const model = normalizeLandmarkAxes(
|
|
1810
|
-
gltf.scene,
|
|
1811
|
-
nextModel.axisConvention
|
|
1812
|
-
);
|
|
1813
|
-
model.name = "tileflow-landmark-" + landmark.id;
|
|
1814
|
-
harmonizeLandmarkModel(model);
|
|
1815
|
-
model.position.copy(
|
|
1816
|
-
localPosition(landmark.center[0], landmark.center[1], elevation)
|
|
1817
|
-
);
|
|
1818
|
-
model.traverse((object) => {
|
|
1819
|
-
if (!object.isMesh) return;
|
|
1820
|
-
object.frustumCulled = true;
|
|
1821
|
-
object.castShadow = false;
|
|
1822
|
-
object.receiveShadow = false;
|
|
1823
|
-
});
|
|
1824
|
-
model.visible = activeLandmarkIds.has(landmark.id);
|
|
1825
|
-
if (loaded.has(landmark.id)) unloadLandmark(landmark.id);
|
|
1826
|
-
loaded.set(landmark.id, {
|
|
1827
|
-
lastUsed: ++cacheClock,
|
|
1828
|
-
model,
|
|
1829
|
-
modelKey: nextModel.key
|
|
1830
|
-
});
|
|
1831
|
-
landmarkState.errors = landmarkState.errors.filter(
|
|
1832
|
-
(item) => item.id !== landmark.id
|
|
1833
|
-
);
|
|
1834
|
-
landmarkState.loaded = [...loaded.keys()];
|
|
1835
|
-
group.add(model);
|
|
1836
|
-
enforceCacheLimit();
|
|
1837
|
-
fallbackSignature = undefined;
|
|
1838
|
-
refresh();
|
|
1839
|
-
},
|
|
1840
|
-
fail
|
|
1841
|
-
);
|
|
1842
|
-
}).catch(fail);
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
function refresh() {
|
|
1846
|
-
if (
|
|
1847
|
-
!threeDimensionalEnabled ||
|
|
1848
|
-
!manifest ||
|
|
1849
|
-
!styleLayerIsVisibleAtZoom(configLayer, map.getZoom())
|
|
1850
|
-
) {
|
|
1851
|
-
activeLandmarkIds.clear();
|
|
1852
|
-
landmarkState.active = [];
|
|
1853
|
-
for (const entry of loaded.values()) entry.model.visible = false;
|
|
1854
|
-
updateLandmarkFallbackFilters();
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
const zoom = map.getZoom();
|
|
1858
|
-
const visible = manifest.landmarks
|
|
1859
|
-
.filter((landmark) => overlapsViewport(landmark.bounds))
|
|
1860
|
-
.sort(
|
|
1861
|
-
(left, right) =>
|
|
1862
|
-
right.priority - left.priority || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
|
|
1863
|
-
)
|
|
1864
|
-
.slice(0, manifest.maximumVisibleModels);
|
|
1865
|
-
activeLandmarkIds.clear();
|
|
1866
|
-
for (const landmark of visible) activeLandmarkIds.add(landmark.id);
|
|
1867
|
-
landmarkState.active = visible.map((landmark) => landmark.id);
|
|
1868
|
-
for (const [id, entry] of loaded) {
|
|
1869
|
-
entry.model.visible = activeLandmarkIds.has(id);
|
|
1870
|
-
}
|
|
1871
|
-
for (const landmark of visible) {
|
|
1872
|
-
loadLandmark(landmark, landmarkModelAtZoom(landmark, zoom));
|
|
1873
|
-
}
|
|
1874
|
-
const nearbyCacheSlots = Math.max(
|
|
1875
|
-
0,
|
|
1876
|
-
manifest.maximumCachedModels - visible.length
|
|
1877
|
-
);
|
|
1878
|
-
const nearby = manifest.landmarks
|
|
1879
|
-
.filter(
|
|
1880
|
-
(landmark) =>
|
|
1881
|
-
!activeLandmarkIds.has(landmark.id) &&
|
|
1882
|
-
overlapsViewport(landmark.bounds, 0.35)
|
|
1883
|
-
)
|
|
1884
|
-
.sort(
|
|
1885
|
-
(left, right) =>
|
|
1886
|
-
right.priority - left.priority || (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
|
|
1887
|
-
)
|
|
1888
|
-
.slice(0, nearbyCacheSlots);
|
|
1889
|
-
for (const landmark of nearby) {
|
|
1890
|
-
loadLandmark(landmark, landmark.models[0]);
|
|
1891
|
-
}
|
|
1892
|
-
enforceCacheLimit();
|
|
1893
|
-
updateLandmarkFallbackFilters();
|
|
1894
|
-
map.triggerRepaint();
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
function normalizeManifest(candidate) {
|
|
1898
|
-
return normalizeTileflowLandmarkManifest(candidate, manifestUrl);
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
async function loadManifest() {
|
|
1902
|
-
try {
|
|
1903
|
-
const candidate = await loadLandmarkManifestCandidate(
|
|
1904
|
-
manifestUrl,
|
|
1905
|
-
abortController.signal
|
|
1906
|
-
);
|
|
1907
|
-
if (abortController.signal.aborted) return;
|
|
1908
|
-
manifest = normalizeManifest(candidate);
|
|
1909
|
-
landmarkState.manifestId = candidate.id;
|
|
1910
|
-
landmarkState.cacheLimit = manifest.maximumCachedModels;
|
|
1911
|
-
refresh();
|
|
1912
|
-
} catch (error) {
|
|
1913
|
-
if (error?.name !== "AbortError") {
|
|
1914
|
-
console.warn("Tileflow landmark manifest failed to load", error);
|
|
1915
|
-
}
|
|
1916
|
-
}
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
return {
|
|
1920
|
-
id: "tileflow-landmarks-3d",
|
|
1921
|
-
type: "custom",
|
|
1922
|
-
renderingMode: "3d",
|
|
1923
|
-
minzoom: landmarkMinimumZoom,
|
|
1924
|
-
...(Number.isFinite(landmarkMaximumZoom)
|
|
1925
|
-
? {maxzoom: landmarkMaximumZoom}
|
|
1926
|
-
: {}),
|
|
1927
|
-
metadata: {"tileflow:3d-toggle": "landmark"},
|
|
1928
|
-
onAdd(_map, gl) {
|
|
1929
|
-
camera = new THREE.Camera();
|
|
1930
|
-
scene = new THREE.Scene();
|
|
1931
|
-
scene.rotateX(Math.PI / 2);
|
|
1932
|
-
scene.scale.multiply(new THREE.Vector3(1, 1, -1));
|
|
1933
|
-
group = new THREE.Group();
|
|
1934
|
-
scene.add(group);
|
|
1935
|
-
scene.add(new THREE.AmbientLight(0xffffff, 2.2));
|
|
1936
|
-
scene.add(new THREE.HemisphereLight(0xfff8e8, 0xd8cfbd, 0.8));
|
|
1937
|
-
const sun = new THREE.DirectionalLight(0xffffff, 0.45);
|
|
1938
|
-
sun.position.set(-120, 220, 100);
|
|
1939
|
-
scene.add(sun);
|
|
1940
|
-
renderer = new THREE.WebGLRenderer({
|
|
1941
|
-
canvas: map.getCanvas(),
|
|
1942
|
-
context: gl
|
|
1943
|
-
});
|
|
1944
|
-
renderer.autoClear = false;
|
|
1945
|
-
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
1946
|
-
map.on("move", queueRefresh);
|
|
1947
|
-
map.on("moveend", refresh);
|
|
1948
|
-
map.on("tileflow:3d-toggle", refresh);
|
|
1949
|
-
void loadManifest();
|
|
1950
|
-
},
|
|
1951
|
-
render(_gl, options) {
|
|
1952
|
-
if (
|
|
1953
|
-
!threeDimensionalEnabled ||
|
|
1954
|
-
!sceneOriginMercator ||
|
|
1955
|
-
group.children.length === 0
|
|
1956
|
-
) return;
|
|
1957
|
-
const meter = sceneOriginMercator.meterInMercatorCoordinateUnits();
|
|
1958
|
-
mapMatrix.fromArray(options.defaultProjectionData.mainMatrix);
|
|
1959
|
-
sceneMatrix
|
|
1960
|
-
.makeTranslation(
|
|
1961
|
-
sceneOriginMercator.x,
|
|
1962
|
-
sceneOriginMercator.y,
|
|
1963
|
-
sceneOriginMercator.z
|
|
1964
|
-
)
|
|
1965
|
-
.scale(new THREE.Vector3(meter, -meter, meter));
|
|
1966
|
-
camera.projectionMatrix.copy(mapMatrix).multiply(sceneMatrix);
|
|
1967
|
-
renderer.resetState();
|
|
1968
|
-
renderer.render(scene, camera);
|
|
1969
|
-
},
|
|
1970
|
-
onRemove() {
|
|
1971
|
-
abortController.abort();
|
|
1972
|
-
if (queuedRefresh !== undefined) clearTimeout(queuedRefresh);
|
|
1973
|
-
map.off("move", queueRefresh);
|
|
1974
|
-
map.off("moveend", refresh);
|
|
1975
|
-
map.off("tileflow:3d-toggle", refresh);
|
|
1976
|
-
for (const entry of loaded.values()) disposeModel(entry.model);
|
|
1977
|
-
loaded.clear();
|
|
1978
|
-
for (const fallback of fallbackLayers) {
|
|
1979
|
-
if (map.getLayer(fallback.id)) {
|
|
1980
|
-
map.setFilter(fallback.id, fallback.filter ?? null);
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
renderer?.dispose();
|
|
1984
|
-
}
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
function addLandmarkLayerIfConfigured(map, styleLayers) {
|
|
1989
|
-
const configLayer = styleLayers.find(
|
|
1990
|
-
(layer) => layer.metadata?.["tileflow:landmark-manifest-url"]
|
|
1991
|
-
);
|
|
1992
|
-
if (!configLayer || map.getLayer("tileflow-landmarks-3d")) return;
|
|
1993
|
-
const fallbackLayers = styleLayers
|
|
1994
|
-
.filter((layer) => layer.metadata?.["tileflow:landmark-fallback"] === true)
|
|
1995
|
-
.map((layer) => ({
|
|
1996
|
-
filter: layer.filter,
|
|
1997
|
-
id: layer.id,
|
|
1998
|
-
spatialFallbacks:
|
|
1999
|
-
layer.metadata?.["tileflow:landmark-spatial-fallbacks"] ?? []
|
|
2000
|
-
}));
|
|
2001
|
-
// Landmark custom layers use Mercator world coordinates.
|
|
2002
|
-
if (map.getProjection().type !== "mercator") {
|
|
2003
|
-
map.setProjection({type: "mercator"});
|
|
2004
|
-
}
|
|
2005
|
-
const firstForegroundLabel = styleLayers.find(
|
|
2006
|
-
(layer) =>
|
|
2007
|
-
(layer.id.startsWith("tileflow-label-") &&
|
|
2008
|
-
!layer.id.startsWith("tileflow-label-road-")) ||
|
|
2009
|
-
layer.id === "tileflow-poi" ||
|
|
2010
|
-
layer.id.startsWith("tileflow-poi-")
|
|
2011
|
-
)?.id;
|
|
2012
|
-
const treeLayer = map.getLayer("tileflow-vegetation-trees")
|
|
2013
|
-
? "tileflow-vegetation-trees"
|
|
2014
|
-
: firstForegroundLabel;
|
|
2015
|
-
map.addLayer(createLandmarkLayer(map, configLayer, fallbackLayers), treeLayer);
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// Core's 3D layers, as an application mounts them: the map's style describes how its
|
|
1361
|
+
// Core's 3D scene, as an application mounts it: the map's style describes how its
|
|
2019
1362
|
// buildings, trees and street lights look, and the controls only show and hide them. The
|
|
2020
1363
|
// vegetation control hides all trees when off. The preview shows one theme at a
|
|
2021
1364
|
// time, so the style on screen holds the sun and the design.
|
|
2022
|
-
const coreScene = {
|
|
1365
|
+
const coreScene = {scene: undefined, shown: undefined, buildingsShown: undefined, animation: "0"};
|
|
2023
1366
|
const runtimeLayerVisibility = {};
|
|
2024
1367
|
globalThis.__tileflowPreviewScene = coreScene;
|
|
2025
1368
|
globalThis.__tileflowRuntimeVisibility = (id, visible) => {
|
|
2026
1369
|
if (!['tileflow-buildings-3d','tileflow-trees-3d','tileflow-street-lights'].includes(id)) return undefined;
|
|
2027
|
-
|
|
1370
|
+
const previewMap = globalThis.__tileflowPreviewMap;
|
|
1371
|
+
if (typeof visible === 'boolean') {
|
|
1372
|
+
runtimeLayerVisibility[id] = visible;
|
|
1373
|
+
if (previewMap) { mountCoreScene(previewMap); previewMap.triggerRepaint(); }
|
|
1374
|
+
}
|
|
2028
1375
|
return runtimeLayerVisibility[id] !== false;
|
|
2029
1376
|
};
|
|
2030
1377
|
// Explicit observation capability: controllers stay private to the preview and expose
|
|
2031
1378
|
// only their public state/statistics to the parent inspector.
|
|
2032
|
-
globalThis.__tileflowInspectModels = () => [...(coreScene.buildings?.inspectModels?.() || []), ...(coreScene.
|
|
2033
|
-
globalThis.__tileflowDescribeRuntimeLayers = () =>
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
1379
|
+
globalThis.__tileflowInspectModels = () => [...(coreScene.scene?.buildings?.inspectModels?.() || []), ...(coreScene.scene?.vegetation?.inspectModels?.() || [])];
|
|
1380
|
+
globalThis.__tileflowDescribeRuntimeLayers = () => {
|
|
1381
|
+
const {buildings, vegetation, streetLights} = coreScene.scene ?? {};
|
|
1382
|
+
return {
|
|
1383
|
+
...(buildings ? {"tileflow-buildings-3d": {controller:"Buildings 3D", state:{visible:runtimeLayerVisibility["tileflow-buildings-3d"] !== false, shown:coreScene.buildingsShown, scale:buildings.scale}, stats:buildings.stats}} : {}),
|
|
1384
|
+
...(vegetation ? {"tileflow-trees-3d": {controller:"Vegetation", state:{visible:runtimeLayerVisibility["tileflow-trees-3d"] !== false, representation:vegetation.representation}, stats:vegetation.stats}} : {}),
|
|
1385
|
+
...(streetLights ? {"tileflow-street-lights": {controller:"Street lights", state:{visible:runtimeLayerVisibility["tileflow-street-lights"] !== false, mounted:true}, stats:streetLights.stats}} : {})
|
|
1386
|
+
};
|
|
1387
|
+
};
|
|
1388
|
+
const createPreviewStyleReads = ${createPreviewStyleReads.toString()};
|
|
1389
|
+
const previewStyleReads = new WeakMap();
|
|
1390
|
+
// Style reads for code that runs on every camera move or style event.
|
|
1391
|
+
function styleReads(map) {
|
|
1392
|
+
let reads = previewStyleReads.get(map);
|
|
1393
|
+
if (!reads) {
|
|
1394
|
+
reads = createPreviewStyleReads(map);
|
|
1395
|
+
previewStyleReads.set(map, reads);
|
|
1396
|
+
}
|
|
1397
|
+
return reads;
|
|
1398
|
+
}
|
|
2038
1399
|
const createPreviewSceneThemes = ${createPreviewSceneThemes.toString()};
|
|
2039
1400
|
const coreSceneThemeControllers = new WeakMap();
|
|
2040
1401
|
function coreSceneThemes(map) {
|
|
@@ -2051,26 +1412,34 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2051
1412
|
if (mountingCoreScene) return;
|
|
2052
1413
|
mountingCoreScene = true;
|
|
2053
1414
|
try {
|
|
2054
|
-
const metadata = map.
|
|
2055
|
-
const
|
|
2056
|
-
if (!coreScene.
|
|
2057
|
-
coreScene.buildings = createTileflowBuildings3D({map, controller});
|
|
2058
|
-
}
|
|
2059
|
-
if (!coreScene.trees) {
|
|
1415
|
+
const metadata = styleReads(map).metadata() ?? {};
|
|
1416
|
+
const treesShown = () => treesEnabled && runtimeLayerVisibility["tileflow-trees-3d"] !== false;
|
|
1417
|
+
if (!coreScene.scene) {
|
|
2060
1418
|
if (!previewSearchParameters.has("trees3d")) {
|
|
2061
1419
|
treesEnabled = metadata["tileflow:vegetation-display"]?.visible ?? true;
|
|
2062
1420
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
1421
|
+
// The scene mounts what the style declares; street lights belong to the 3D view.
|
|
1422
|
+
coreScene.scene = createTileflowMap3D({map, controller: coreSceneThemes(map),
|
|
1423
|
+
viewMode: threeDimensionalEnabled ? "3d" : "2d", vegetation: treesShown(),
|
|
1424
|
+
transformRequest: previewTransformRequest});
|
|
1425
|
+
}
|
|
1426
|
+
const scene = coreScene.scene;
|
|
1427
|
+
scene.setVegetation(treesShown());
|
|
1428
|
+
const instant = {transition: {duration: 0}};
|
|
1429
|
+
const animation = previewSearchParameters.get('animation') || '0';
|
|
1430
|
+
const duration = animation === 'fade' ? 800 : Math.min(3000, Math.max(0, Number(animation) || 0));
|
|
1431
|
+
// Only a change of the controls moves the layers, so a style event does not restart them.
|
|
1432
|
+
if (coreScene.shown !== threeDimensionalEnabled || coreScene.animation !== animation) {
|
|
1433
|
+
coreScene.shown = threeDimensionalEnabled;
|
|
1434
|
+
coreScene.buildingsShown = threeDimensionalEnabled;
|
|
1435
|
+
coreScene.animation = animation;
|
|
1436
|
+
void (threeDimensionalEnabled ? scene.show({transition: {duration, mode: animation === "fade" ? "fade" : "grow"}}) : scene.hide(instant));
|
|
2065
1437
|
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
} else if (coreScene.lights && (!threeDimensionalEnabled || !metadata["tileflow:street-lights"])) {
|
|
2072
|
-
coreScene.lights.dispose();
|
|
2073
|
-
coreScene.lights = undefined;
|
|
1438
|
+
// The inspector can hide the buildings alone.
|
|
1439
|
+
const showBuildings = threeDimensionalEnabled && runtimeLayerVisibility["tileflow-buildings-3d"] !== false;
|
|
1440
|
+
if (scene.buildings && coreScene.buildingsShown !== showBuildings) {
|
|
1441
|
+
coreScene.buildingsShown = showBuildings;
|
|
1442
|
+
void (showBuildings ? scene.buildings.show(instant) : scene.buildings.hide(instant));
|
|
2074
1443
|
}
|
|
2075
1444
|
const lightLayer = map.getLayer("tileflow-street-lights");
|
|
2076
1445
|
const lightRenderer = lightLayer?.implementation || lightLayer;
|
|
@@ -2081,16 +1450,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2081
1450
|
};
|
|
2082
1451
|
lightRenderer.__previewVisibility = true;
|
|
2083
1452
|
}
|
|
2084
|
-
const showBuildings = threeDimensionalEnabled && runtimeLayerVisibility["tileflow-buildings-3d"] !== false;
|
|
2085
|
-
const instant = {transition: {duration: 0}};
|
|
2086
|
-
const animation = previewSearchParameters.get('animation') || '0';
|
|
2087
|
-
const duration = animation === 'fade' ? 800 : Math.min(3000, Math.max(0, Number(animation) || 0));
|
|
2088
|
-
// Only a change of the controls moves the layers, so a style event does not restart them.
|
|
2089
|
-
if (coreScene.buildings && (coreScene.buildingsShown !== showBuildings || coreScene.animation !== animation)) {
|
|
2090
|
-
coreScene.buildingsShown = showBuildings;
|
|
2091
|
-
coreScene.animation = animation;
|
|
2092
|
-
void (showBuildings ? coreScene.buildings.show({transition: {duration, mode: animation === "fade" ? "fade" : "grow"}}) : coreScene.buildings.hide(instant));
|
|
2093
|
-
}
|
|
2094
1453
|
} finally {mountingCoreScene = false;}
|
|
2095
1454
|
}
|
|
2096
1455
|
|
|
@@ -2125,7 +1484,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2125
1484
|
if (typeof enabled !== "boolean" || enabled === this.enabled) return;
|
|
2126
1485
|
this.enabled = enabled;
|
|
2127
1486
|
threeDimensionalEnabled = this.enabled;
|
|
2128
|
-
landmarkState.enabled = this.enabled;
|
|
2129
1487
|
this.update();
|
|
2130
1488
|
writeToggleStateToUrl();
|
|
2131
1489
|
this.map.fire("tileflow:3d-toggle", {enabled: this.enabled});
|
|
@@ -2134,11 +1492,12 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2134
1492
|
}
|
|
2135
1493
|
|
|
2136
1494
|
update() {
|
|
2137
|
-
|
|
1495
|
+
const reads = styleReads(this.map);
|
|
1496
|
+
for (const layer of reads.layers()) {
|
|
2138
1497
|
const toggle = layer.metadata?.["tileflow:3d-toggle"];
|
|
2139
1498
|
if (toggle !== "building" && toggle !== "landmark") continue;
|
|
2140
1499
|
// Core's 3D buildings stand in for the compiled extrusion where the style describes them.
|
|
2141
|
-
const replaced = toggle === "building" &&
|
|
1500
|
+
const replaced = toggle === "building" && reads.metadata()?.["tileflow:buildings"];
|
|
2142
1501
|
const visibility = this.enabled && !replaced ? "visible" : "none";
|
|
2143
1502
|
if (this.map.getLayoutProperty(layer.id, "visibility") !== visibility) {
|
|
2144
1503
|
this.map.setLayoutProperty(layer.id, "visibility", visibility);
|
|
@@ -2296,9 +1655,10 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2296
1655
|
...visibleLayerGroups,
|
|
2297
1656
|
].join(",");
|
|
2298
1657
|
}
|
|
2299
|
-
const
|
|
2300
|
-
|
|
2301
|
-
|
|
1658
|
+
const reads = styleReads(map);
|
|
1659
|
+
const vegetationLayers = reads.metadata()?.["tileflow:vegetation-display"]?.flatLayers;
|
|
1660
|
+
for (const layer of reads.layers()) {
|
|
1661
|
+
if (layer.type === "symbol" && map.getLayoutProperty(layer.id, "text-field") !== undefined) {
|
|
2302
1662
|
const hideLabels = !visibleLayerGroups.has("labels");
|
|
2303
1663
|
if (hideLabels && !defaultLayerTextField.has(layer.id)) {
|
|
2304
1664
|
defaultLayerTextField.set(
|
|
@@ -2332,7 +1692,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2332
1692
|
const threeDimensionalToggle = layer.metadata?.["tileflow:3d-toggle"];
|
|
2333
1693
|
const authoredVisibility = defaultLayerVisibility.get(layer.id);
|
|
2334
1694
|
if (!hideLayer) defaultLayerVisibility.delete(layer.id);
|
|
2335
|
-
const styleMetadata =
|
|
1695
|
+
const styleMetadata = reads.metadata() ?? {};
|
|
2336
1696
|
// Core's 3D buildings and trees stand in for compiled extrusions and flat trees.
|
|
2337
1697
|
const replacedByCore =
|
|
2338
1698
|
threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"];
|
|
@@ -2383,6 +1743,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2383
1743
|
...resolveInitialMapOptions(previewMapOptions)
|
|
2384
1744
|
});
|
|
2385
1745
|
globalThis.__tileflowPreviewMap = map;
|
|
1746
|
+
attachTileflowNotices(map);
|
|
2386
1747
|
devtoolsObserver?.attach(map);
|
|
2387
1748
|
installMapBenchmark(map);
|
|
2388
1749
|
if (window.parent === window) map.addControl(new maplibregl.NavigationControl(), "top-right");
|
|
@@ -2406,45 +1767,17 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2406
1767
|
let ensuringThreeDimensionalLayers;
|
|
2407
1768
|
const ensureThreeDimensionalLayers = () => {
|
|
2408
1769
|
if (ensuringThreeDimensionalLayers) return ensuringThreeDimensionalLayers;
|
|
2409
|
-
const
|
|
2410
|
-
const landmarkConfigLayer = currentStyleLayers.find(
|
|
2411
|
-
(layer) => layer.metadata?.["tileflow:landmark-manifest-url"]
|
|
2412
|
-
);
|
|
2413
|
-
const buildingWireframeConfigLayer = currentStyleLayers.find(
|
|
1770
|
+
const buildingWireframeConfigLayer = styleReads(map).layers().find(
|
|
2414
1771
|
(layer) => layer.metadata?.["tileflow:building-wireframe"] === true
|
|
2415
1772
|
);
|
|
2416
|
-
const landmarkManifestUrl =
|
|
2417
|
-
landmarkConfigLayer?.metadata?.["tileflow:landmark-manifest-url"];
|
|
2418
|
-
const landmarkPreloadZoom = landmarkConfigLayer
|
|
2419
|
-
? Math.max(0, styleLayerZoomRange(landmarkConfigLayer).minzoom - 1)
|
|
2420
|
-
: Infinity;
|
|
2421
|
-
const shouldPreloadLandmarks =
|
|
2422
|
-
landmarkManifestUrl &&
|
|
2423
|
-
threeDimensionalEnabled &&
|
|
2424
|
-
map.getZoom() >= landmarkPreloadZoom;
|
|
2425
|
-
const needsLandmarks =
|
|
2426
|
-
landmarkConfigLayer &&
|
|
2427
|
-
threeDimensionalEnabled &&
|
|
2428
|
-
styleLayerIsVisibleAtZoom(landmarkConfigLayer, map.getZoom()) &&
|
|
2429
|
-
!map.getLayer("tileflow-landmarks-3d");
|
|
2430
1773
|
const needsBuildingWireframe =
|
|
2431
1774
|
buildingWireframeConfigLayer &&
|
|
2432
1775
|
threeDimensionalEnabled &&
|
|
2433
1776
|
styleLayerIsVisibleAtZoom(buildingWireframeConfigLayer, map.getZoom()) &&
|
|
2434
1777
|
!map.getLayer("tileflow-buildings-wireframe-3d");
|
|
2435
|
-
if (
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
!shouldPreloadLandmarks
|
|
2439
|
-
) return;
|
|
2440
|
-
const runtimePrerequisites = [
|
|
2441
|
-
shouldPreloadLandmarks
|
|
2442
|
-
? loadLandmarkPrerequisites(landmarkManifestUrl)
|
|
2443
|
-
: loadThreeCoreRuntime(),
|
|
2444
|
-
...(needsBuildingWireframe ? [loadBuildingWireframeRuntime()] : [])
|
|
2445
|
-
];
|
|
2446
|
-
ensuringThreeDimensionalLayers = Promise.all(runtimePrerequisites)
|
|
2447
|
-
.then(async () => {
|
|
1778
|
+
if (!needsBuildingWireframe) return;
|
|
1779
|
+
ensuringThreeDimensionalLayers = loadBuildingWireframeRuntime()
|
|
1780
|
+
.then(() => {
|
|
2448
1781
|
const styleLayers = map.getStyle()?.layers || [];
|
|
2449
1782
|
const buildingWireframeConfigLayer = styleLayers.find(
|
|
2450
1783
|
(layer) => layer.metadata?.["tileflow:building-wireframe"] === true
|
|
@@ -2454,28 +1787,8 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
2454
1787
|
threeDimensionalEnabled &&
|
|
2455
1788
|
styleLayerIsVisibleAtZoom(buildingWireframeConfigLayer, map.getZoom())
|
|
2456
1789
|
) {
|
|
2457
|
-
// A tree-only load may already be in flight when buildings are
|
|
2458
|
-
// enabled. Reassert the line runtime before mounting the layer.
|
|
2459
|
-
await loadBuildingWireframeRuntime();
|
|
2460
1790
|
addBuildingWireframeLayerIfConfigured(map, styleLayers);
|
|
2461
1791
|
}
|
|
2462
|
-
const landmarkConfigLayer = styleLayers.find(
|
|
2463
|
-
(layer) => layer.metadata?.["tileflow:landmark-manifest-url"]
|
|
2464
|
-
);
|
|
2465
|
-
const shouldAddLandmarks =
|
|
2466
|
-
landmarkConfigLayer &&
|
|
2467
|
-
threeDimensionalEnabled &&
|
|
2468
|
-
styleLayerIsVisibleAtZoom(landmarkConfigLayer, map.getZoom()) &&
|
|
2469
|
-
!map.getLayer("tileflow-landmarks-3d");
|
|
2470
|
-
if (shouldAddLandmarks) {
|
|
2471
|
-
// The tree-only path may already be in flight when landmarks
|
|
2472
|
-
// are enabled. Reassert the richer runtime here so a shared
|
|
2473
|
-
// core promise can never expose an undefined GLTF loader.
|
|
2474
|
-
await loadLandmarkPrerequisites(
|
|
2475
|
-
landmarkConfigLayer.metadata["tileflow:landmark-manifest-url"]
|
|
2476
|
-
);
|
|
2477
|
-
addLandmarkLayerIfConfigured(map, styleLayers);
|
|
2478
|
-
}
|
|
2479
1792
|
})
|
|
2480
1793
|
.catch((error) => console.warn("Tileflow 3D runtime failed to load", error))
|
|
2481
1794
|
.finally(() => {
|
|
@@ -2708,7 +2021,6 @@ function resolvePreviewTheme(map, requested) {
|
|
|
2708
2021
|
}
|
|
2709
2022
|
export {
|
|
2710
2023
|
TileflowPreviewSelectionError,
|
|
2711
|
-
normalizeTileflowLandmarkManifest,
|
|
2712
2024
|
readBoundedTileflowJsonResponse,
|
|
2713
2025
|
readBoundedTileflowResponse,
|
|
2714
2026
|
renderTileflowPreviewHtml,
|