@tileflow/dev 0.1.0-alpha.30 → 0.1.0-alpha.32
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 +59 -2
- package/THIRD_PARTY_NOTICES.md +225 -3
- package/dist/artifacts.d.ts +6 -2
- package/dist/artifacts.js +18 -3
- package/dist/{chunk-6MGFMKG3.js → chunk-AQUGMUZW.js} +43 -3
- package/dist/{chunk-L56FLV4R.js → chunk-CM6ZTYDJ.js} +48 -7
- package/dist/{chunk-A2VTI43Y.js → chunk-F4PLTZS4.js} +1007 -19
- package/dist/config.js +1 -1
- package/dist/devtools-browser.js +17353 -0
- package/dist/devtools-observer.js +95 -0
- package/dist/fonts.d.ts +1 -1
- package/dist/icons-B84H8Ymu.d.ts +406 -0
- package/dist/icons.d.ts +3 -337
- package/dist/icons.js +3 -1
- package/dist/inspect.js +1 -1
- package/dist/{preview-assets-MVUJJ5XR.js → preview-assets-KGUICQHG.js} +6 -0
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +251 -1855
- package/dist/server.d.ts +11 -4
- package/dist/server.js +1595 -278
- package/docs/beacon-map-audits.md +102 -0
- package/docs/layers-inspection.md +87 -0
- package/docs/local-comparison-workbench-and-compiler-inspection.md +74 -193
- package/docs/native-artifacts.md +3 -3
- package/package.json +7 -5
- package/ui-fonts/hubot-sans-OFL.txt +93 -0
- package/ui-fonts/hubot-sans.woff2 +0 -0
- package/ui-fonts/inter-OFL.txt +92 -0
- package/ui-fonts/inter.woff2 +0 -0
- package/ui-fonts/jetbrains-mono-OFL.txt +93 -0
- package/ui-fonts/jetbrains-mono.woff2 +0 -0
- package/ui-fonts/mona-sans-OFL.txt +93 -0
- package/ui-fonts/mona-sans.woff2 +0 -0
package/dist/preview.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getFirstTileflowMapName
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-AQUGMUZW.js";
|
|
4
4
|
import "./chunk-D4OE4ITL.js";
|
|
5
5
|
|
|
6
6
|
// src/preview.ts
|
|
@@ -140,11 +140,67 @@ function normalizeTileflowLandmarkManifest(candidate, manifestUrl) {
|
|
|
140
140
|
};
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// src/preview-scene.ts
|
|
144
|
+
var previewSceneClient = String.raw`
|
|
145
|
+
let sceneStyleRevision = 0;
|
|
146
|
+
globalThis.__tileflowApplyScene = async (serialized, isCurrent) => {
|
|
147
|
+
const next = JSON.parse(serialized);
|
|
148
|
+
if (next.style) {
|
|
149
|
+
// getStyle() retains renderer instrumentation on sources. It is not authored
|
|
150
|
+
// style-spec input; MapLibre adds it back from the map's timing option.
|
|
151
|
+
delete next.style.collectResourceTiming;
|
|
152
|
+
for (const source of Object.values(next.style.sources || {})) delete source.collectResourceTiming;
|
|
153
|
+
await loadTileflowStyleFonts(next.style, {fontFaces: previewFontFaces});
|
|
154
|
+
}
|
|
155
|
+
if (!isCurrent()) return false;
|
|
156
|
+
const context = map.getCanvas().getContext("webgl2") || map.getCanvas().getContext("webgl");
|
|
157
|
+
if (context?.isContextLost()) throw new Error("The map's graphics context is recovering. Try the change again.");
|
|
158
|
+
if (next.style) {
|
|
159
|
+
// MapLibre ignores metadata-only diffs. A global-state revision also commits the
|
|
160
|
+
// new 3D design metadata, without replacing sources, tiles or custom layers.
|
|
161
|
+
next.style.state = {...next.style.state, "tileflow:preview-revision": ++sceneStyleRevision};
|
|
162
|
+
const layerOrder = map.getLayersOrder();
|
|
163
|
+
map.setStyle(next.style, {diff:true});
|
|
164
|
+
// Custom layers are omitted from the serialized style. A diff can reinsert the
|
|
165
|
+
// surrounding style layers above them, covering 3D with the flat map. Restore
|
|
166
|
+
// their placement before the nearest surviving successor, without remounting.
|
|
167
|
+
let before;
|
|
168
|
+
for (let index = layerOrder.length - 1; index >= 0; index--) {
|
|
169
|
+
const id = layerOrder[index], layer = map.getLayer(id);
|
|
170
|
+
if (!layer) continue;
|
|
171
|
+
if (layer.type === "custom") map.moveLayer(id, before);
|
|
172
|
+
before = id;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
previewSearchParameters.set("animation", String(next.animation));
|
|
176
|
+
previewSearchParameters.delete("trees3d");
|
|
177
|
+
const visibleTrees = map.getStyle()?.metadata?.["tileflow:vegetation-display"]?.visible ?? true;
|
|
178
|
+
threeDimensionalControl?.setEnabled(next.buildings3d);
|
|
179
|
+
treeControl?.setEnabled(visibleTrees);
|
|
180
|
+
mountCoreScene(map);
|
|
181
|
+
treeControl?.update();
|
|
182
|
+
updateGlobeBackdrop(map);
|
|
183
|
+
const url = new URL(location.href);
|
|
184
|
+
url.searchParams.delete("scene");
|
|
185
|
+
url.searchParams.set("map", next.map);
|
|
186
|
+
url.searchParams.set("theme", next.theme);
|
|
187
|
+
if (Object.keys(next.controls).length) url.searchParams.set("controls", JSON.stringify(next.controls));
|
|
188
|
+
else url.searchParams.delete("controls");
|
|
189
|
+
if (next.animation) url.searchParams.set("animation", String(next.animation));
|
|
190
|
+
else url.searchParams.delete("animation");
|
|
191
|
+
history.replaceState(history.state, "", url.href);
|
|
192
|
+
// Read the current camera here; a slow compile must never undo navigation.
|
|
193
|
+
writeCameraToUrl(map);
|
|
194
|
+
map.triggerRepaint();
|
|
195
|
+
return true;
|
|
196
|
+
};
|
|
197
|
+
`;
|
|
198
|
+
|
|
143
199
|
// src/preview-html.ts
|
|
144
|
-
function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticPreview, fontFaces = []) {
|
|
145
|
-
const styleUrl = preview ? `${basePath}/styles/${preview.mapName}/${preview.themeName}.json` : void 0;
|
|
200
|
+
function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticPreview, fontFaces = [], fitViewport = false, controls) {
|
|
201
|
+
const styleUrl = preview ? `${basePath}/styles/${preview.mapName}/${preview.themeName}.json${controls ? `?${new URLSearchParams({ controls })}` : ""}` : void 0;
|
|
146
202
|
const mapOptions = preview ? previewMapOptions(preview) : void 0;
|
|
147
|
-
const viewportCss = preview?.viewport ? `
|
|
203
|
+
const viewportCss = preview?.viewport && !fitViewport ? `
|
|
148
204
|
html, body { min-height: 100%; margin: 0; }
|
|
149
205
|
body {
|
|
150
206
|
display: grid;
|
|
@@ -168,7 +224,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
168
224
|
${viewportCss}
|
|
169
225
|
body { font-family: ui-sans-serif, system-ui, sans-serif; }
|
|
170
226
|
#map {
|
|
171
|
-
--tileflow-globe-radius: 220px;
|
|
172
227
|
background-color: var(--tileflow-space, #2F5070);
|
|
173
228
|
background-image:
|
|
174
229
|
radial-gradient(circle at 18% 24%, rgba(225, 240, 250, 0.34) 0 1px, transparent 1.4px),
|
|
@@ -184,22 +239,8 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
184
239
|
radial-gradient(circle at 72% 62%, rgba(225, 240, 250, 0.22) 0 1px, transparent 1.3px),
|
|
185
240
|
radial-gradient(circle at 42% 78%, rgba(225, 240, 250, 0.18) 0 0.8px, transparent 1.2px);
|
|
186
241
|
}
|
|
187
|
-
#map.tileflow-globe
|
|
188
|
-
|
|
189
|
-
position: absolute;
|
|
190
|
-
z-index: 0;
|
|
191
|
-
left: 50%;
|
|
192
|
-
top: 50%;
|
|
193
|
-
width: calc(var(--tileflow-globe-radius) * 2);
|
|
194
|
-
height: calc(var(--tileflow-globe-radius) * 2);
|
|
195
|
-
border: 1px solid var(--tileflow-rim-line, rgba(248, 252, 255, 0.88));
|
|
196
|
-
border-radius: 50%;
|
|
197
|
-
box-shadow:
|
|
198
|
-
0 0 9px 3px var(--tileflow-rim-core, rgba(248, 252, 255, 0.9)),
|
|
199
|
-
0 0 28px 10px var(--tileflow-rim-glow, rgba(176, 220, 246, 0.52)),
|
|
200
|
-
0 0 58px 20px var(--tileflow-rim-haze, rgba(132, 195, 232, 0.2));
|
|
201
|
-
pointer-events: none;
|
|
202
|
-
transform: translate(-50%, -50%);
|
|
242
|
+
#map.tileflow-globe .maplibregl-canvas {
|
|
243
|
+
filter: var(--tileflow-globe-rim, none);
|
|
203
244
|
}
|
|
204
245
|
#map.tileflow-globe > .maplibregl-canvas-container {
|
|
205
246
|
position: relative;
|
|
@@ -233,6 +274,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
233
274
|
padding: 12px;
|
|
234
275
|
white-space: pre-wrap;
|
|
235
276
|
}
|
|
277
|
+
html[data-devtools-embedded=true] .status { display:none !important; }
|
|
236
278
|
.maplibregl-ctrl-group .tileflow-3d-toggle,
|
|
237
279
|
.maplibregl-ctrl-group .tileflow-tree-toggle,
|
|
238
280
|
.maplibregl-ctrl-group .tileflow-theme-toggle {
|
|
@@ -253,6 +295,12 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
253
295
|
.maplibregl-ctrl-group .tileflow-theme-toggle:hover {
|
|
254
296
|
background-color: #F1F3F4;
|
|
255
297
|
}
|
|
298
|
+
.maplibregl-ctrl-group { background:#faf6eb; border:1px solid #e0d5c4; border-radius:3px; box-shadow:0 3px 12px #30251f18; }
|
|
299
|
+
.maplibregl-ctrl-group button { color:#30251f; }
|
|
300
|
+
.maplibregl-ctrl-group button + button { border-top-color:#e0d5c4; }
|
|
301
|
+
.maplibregl-ctrl-group .tileflow-3d-toggle,.maplibregl-ctrl-group .tileflow-tree-toggle,.maplibregl-ctrl-group .tileflow-theme-toggle { color:#30251f; font-weight:500; }
|
|
302
|
+
.maplibregl-ctrl-group .tileflow-3d-toggle[aria-pressed="true"],.maplibregl-ctrl-group .tileflow-tree-toggle[aria-pressed="true"] { background:#f0dfe0; color:#8c2e3f; }
|
|
303
|
+
.maplibregl-ctrl-group .tileflow-3d-toggle:hover,.maplibregl-ctrl-group .tileflow-tree-toggle:hover,.maplibregl-ctrl-group .tileflow-theme-toggle:hover { background:#eee4d3; }
|
|
256
304
|
</style>
|
|
257
305
|
</head>
|
|
258
306
|
<body>
|
|
@@ -262,17 +310,25 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
262
310
|
<script type="importmap">{"imports":{"fflate":"${basePath}/__runtime/fflate.js","three":"${basePath}/__runtime/three.module.js"}}</script>
|
|
263
311
|
<script type="module">
|
|
264
312
|
import * as maplibregl from "${basePath}/__runtime/maplibre-gl.mjs";
|
|
313
|
+
import {createPreviewObserver} from "${basePath}/__runtime/devtools-observer.js";
|
|
265
314
|
import {loadTileflowStyleFonts} from "${basePath}/__runtime/tileflow-browser.js";
|
|
266
315
|
import {registerTileflowContourProtocol} from "${basePath}/__runtime/tileflow-browser.js";
|
|
267
316
|
import {registerTileflowPmtilesProtocol} from "${basePath}/__runtime/tileflow-browser.js";
|
|
317
|
+
import {createTileflowBuildings3D, createTileflowStreetLights, createTileflowVegetation} from "${basePath}/__runtime/tileflow-browser.js";
|
|
318
|
+
import {readTileflowSunPreset, resolveTileflowSun, tileflowDefaultSunPreset} from "${basePath}/__runtime/tileflow-browser.js";
|
|
268
319
|
|
|
269
320
|
maplibregl.setWorkerUrl("${basePath}/__runtime/maplibre-gl-worker.mjs");
|
|
270
321
|
|
|
322
|
+
const devtoolsObserver = window.parent !== window ? createPreviewObserver() : undefined;
|
|
323
|
+
globalThis.__tileflowDevtoolsObserver = devtoolsObserver;
|
|
324
|
+
if (window.parent !== window) document.documentElement.dataset.devtoolsEmbedded = "true";
|
|
325
|
+
|
|
271
326
|
const initialStatus = ${JSON.stringify(initialStatus)};
|
|
272
327
|
const normalizeTileflowLandmarkManifest = ${normalizeTileflowLandmarkManifest.toString()};
|
|
273
328
|
const readBoundedTileflowResponse = ${readBoundedTileflowResponse.toString()};
|
|
274
329
|
const readBoundedTileflowJsonResponse = ${readBoundedTileflowJsonResponse.toString()};
|
|
275
330
|
const initialGeneration = initialStatus.generation;
|
|
331
|
+
document.documentElement.dataset.tileflowArtifactGeneration = String(initialStatus.lastGoodGeneration ?? initialGeneration);
|
|
276
332
|
const badge = document.getElementById("badge");
|
|
277
333
|
const status = document.getElementById("status");
|
|
278
334
|
const previewLabel = ${JSON.stringify(preview?.label)};
|
|
@@ -282,64 +338,40 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
282
338
|
const previewThemes = ${serializeInlineJson(preview?.themeNames ?? [])};
|
|
283
339
|
const isSemanticPreview = ${JSON.stringify(isSemanticPreview)};
|
|
284
340
|
const previewFontFaces = ${serializeInlineJson(fontFaces)};
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const treeBackendMode = treeSearchParameters.get("treeBackend") === "three"
|
|
291
|
-
? "three"
|
|
292
|
-
: "webgl2";
|
|
293
|
-
const treeBenchmarkEnabled = treeSearchParameters.get("treeBenchmark") === "1";
|
|
294
|
-
const mapBenchmarkEnabled = treeSearchParameters.get("mapBenchmark") === "1";
|
|
295
|
-
const mapSweepEnabled = treeSearchParameters.get("mapSweep") === "1";
|
|
296
|
-
const mapCompareEnabled = treeSearchParameters.get("mapCompare") === "1";
|
|
297
|
-
const requestedMapCompareLimit = Number(treeSearchParameters.get("mapCompareLimit"));
|
|
341
|
+
const previewSearchParameters = new URL(location.href).searchParams;
|
|
342
|
+
const mapBenchmarkEnabled = previewSearchParameters.get("mapBenchmark") === "1";
|
|
343
|
+
const mapSweepEnabled = previewSearchParameters.get("mapSweep") === "1";
|
|
344
|
+
const mapCompareEnabled = previewSearchParameters.get("mapCompare") === "1";
|
|
345
|
+
const requestedMapCompareLimit = Number(previewSearchParameters.get("mapCompareLimit"));
|
|
298
346
|
const mapCompareLimit =
|
|
299
347
|
Number.isInteger(requestedMapCompareLimit) &&
|
|
300
348
|
requestedMapCompareLimit >= 1 &&
|
|
301
349
|
requestedMapCompareLimit <= 64
|
|
302
350
|
? requestedMapCompareLimit
|
|
303
351
|
: 16;
|
|
304
|
-
const requestedMapWorkerCount = Number(
|
|
352
|
+
const requestedMapWorkerCount = Number(previewSearchParameters.get("mapWorkers"));
|
|
305
353
|
const mapWorkerCountOverride =
|
|
306
354
|
Number.isInteger(requestedMapWorkerCount) &&
|
|
307
355
|
requestedMapWorkerCount >= 1 &&
|
|
308
356
|
requestedMapWorkerCount <= 3
|
|
309
357
|
? requestedMapWorkerCount
|
|
310
358
|
: undefined;
|
|
311
|
-
const mapBenchmarkLongitude = Number(
|
|
312
|
-
const mapBenchmarkLatitude = Number(
|
|
359
|
+
const mapBenchmarkLongitude = Number(previewSearchParameters.get("lng"));
|
|
360
|
+
const mapBenchmarkLatitude = Number(previewSearchParameters.get("lat"));
|
|
313
361
|
const mapBenchmarkCenter =
|
|
314
|
-
|
|
315
|
-
|
|
362
|
+
previewSearchParameters.has("lng") &&
|
|
363
|
+
previewSearchParameters.has("lat") &&
|
|
316
364
|
Number.isFinite(mapBenchmarkLongitude) &&
|
|
317
365
|
Number.isFinite(mapBenchmarkLatitude)
|
|
318
366
|
? [mapBenchmarkLongitude, mapBenchmarkLatitude]
|
|
319
367
|
: undefined;
|
|
320
368
|
function readToggleFromUrl(name, fallback) {
|
|
321
|
-
const values =
|
|
369
|
+
const values = previewSearchParameters.getAll(name);
|
|
322
370
|
if (values.length !== 1) return fallback;
|
|
323
371
|
if (values[0] === "on") return true;
|
|
324
372
|
if (values[0] === "off") return false;
|
|
325
373
|
return fallback;
|
|
326
374
|
}
|
|
327
|
-
const treeFrameSamples = [];
|
|
328
|
-
const treeMetrics = {
|
|
329
|
-
buildMilliseconds: undefined,
|
|
330
|
-
backend: treeBackendMode,
|
|
331
|
-
frameP95Milliseconds: undefined,
|
|
332
|
-
mode: treeRendererMode,
|
|
333
|
-
queryMilliseconds: undefined,
|
|
334
|
-
refreshMilliseconds: undefined,
|
|
335
|
-
renderCalls: 0,
|
|
336
|
-
renderMilliseconds: undefined,
|
|
337
|
-
renderedTriangles: 0,
|
|
338
|
-
selectionMilliseconds: undefined,
|
|
339
|
-
terrainMilliseconds: undefined,
|
|
340
|
-
treeCount: 0
|
|
341
|
-
};
|
|
342
|
-
globalThis.__tileflowTreeMetrics = treeMetrics;
|
|
343
375
|
let threeDimensionalEnabled = readToggleFromUrl("buildings3d", false);
|
|
344
376
|
let treesEnabled = readToggleFromUrl("trees3d", true);
|
|
345
377
|
const landmarkState = {
|
|
@@ -493,26 +525,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
493
525
|
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * percentileValue) - 1)];
|
|
494
526
|
}
|
|
495
527
|
|
|
496
|
-
function updateTreeStatus(message, details = {}) {
|
|
497
|
-
globalThis.__tileflowTreeState = {message, ...details};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (treeBenchmarkEnabled) {
|
|
501
|
-
let previousFrameTime;
|
|
502
|
-
function sampleTreeFrame(frameTime) {
|
|
503
|
-
if (previousFrameTime !== undefined) {
|
|
504
|
-
treeFrameSamples.push(frameTime - previousFrameTime);
|
|
505
|
-
}
|
|
506
|
-
previousFrameTime = frameTime;
|
|
507
|
-
if (treeFrameSamples.length < 360) {
|
|
508
|
-
requestAnimationFrame(sampleTreeFrame);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
treeMetrics.frameP95Milliseconds = percentile(treeFrameSamples, 0.95);
|
|
512
|
-
}
|
|
513
|
-
requestAnimationFrame(sampleTreeFrame);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
528
|
function installMapBenchmark(map) {
|
|
517
529
|
if (!mapBenchmarkEnabled) return;
|
|
518
530
|
let activeRun;
|
|
@@ -982,14 +994,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
982
994
|
return;
|
|
983
995
|
}
|
|
984
996
|
|
|
985
|
-
// A Web Mercator world is 512 * 2^zoom pixels wide. Dividing its
|
|
986
|
-
// circumference by 2\u03C0 closely tracks MapLibre's on-screen globe radius.
|
|
987
|
-
const radius = Math.min(
|
|
988
|
-
4096,
|
|
989
|
-
((512 * Math.pow(2, map.getZoom())) / (2 * Math.PI)) * 0.91
|
|
990
|
-
);
|
|
991
997
|
container.classList.add("tileflow-globe");
|
|
992
|
-
container.style.setProperty("--tileflow-globe-radius", radius.toFixed(2) + "px");
|
|
993
998
|
applyAtmosphereBackdrop(map, container);
|
|
994
999
|
}
|
|
995
1000
|
|
|
@@ -1003,12 +1008,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
1003
1008
|
const value = Number.parseInt(match[1], 16);
|
|
1004
1009
|
return "rgba(" + (value >> 16) + ", " + ((value >> 8) & 255) + ", " + (value & 255) + ", " + alpha + ")";
|
|
1005
1010
|
};
|
|
1011
|
+
// Use the rendered canvas alpha, as the shared browser atmosphere does.
|
|
1012
|
+
// A zoom-only circle cannot track the globe's latitude, viewport or pitch.
|
|
1013
|
+
const opacity = Math.max(0, Math.min(1, (6 - map.getZoom()) / 3));
|
|
1014
|
+
const core = rgba(atmosphere?.horizonColor ?? "#F8FCFF", opacity * 0.65);
|
|
1015
|
+
const glow = rgba(atmosphere?.horizonColor ?? "#B0DCF6", opacity * 0.4);
|
|
1006
1016
|
const properties = {
|
|
1007
1017
|
"--tileflow-space": rgba(atmosphere?.spaceColor, 1),
|
|
1008
|
-
"--tileflow-rim
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
"--tileflow-rim-haze": rgba(atmosphere?.skyColor, 0.35),
|
|
1018
|
+
"--tileflow-globe-rim": opacity > 0
|
|
1019
|
+
? "drop-shadow(0 0 2px " + core + ") drop-shadow(0 0 12px " + glow + ")"
|
|
1020
|
+
: "none",
|
|
1012
1021
|
};
|
|
1013
1022
|
for (const [name, value] of Object.entries(properties)) {
|
|
1014
1023
|
if (value) container.style.setProperty(name, value);
|
|
@@ -1456,1719 +1465,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
1456
1465
|
map.addLayer(createBuildingWireframeLayer(map, styleLayer), layerAboveBuildings);
|
|
1457
1466
|
}
|
|
1458
1467
|
|
|
1459
|
-
function createTreeLayer(map, styleLayer) {
|
|
1460
|
-
const initialTreeInstanceCapacity = 64;
|
|
1461
|
-
const maximumCachedTrees = 12000;
|
|
1462
|
-
const sourceRefreshIntervalMilliseconds = 120;
|
|
1463
|
-
const viewportBufferPixels = 96;
|
|
1464
|
-
const treeLods = [
|
|
1465
|
-
{
|
|
1466
|
-
densityCellPixels: 12,
|
|
1467
|
-
name: "low",
|
|
1468
|
-
untilZoom: 16.5
|
|
1469
|
-
},
|
|
1470
|
-
{
|
|
1471
|
-
densityCellPixels: 8,
|
|
1472
|
-
name: "medium",
|
|
1473
|
-
untilZoom: 17.5
|
|
1474
|
-
},
|
|
1475
|
-
{
|
|
1476
|
-
densityCellPixels: 0,
|
|
1477
|
-
name: "full",
|
|
1478
|
-
untilZoom: Infinity
|
|
1479
|
-
}
|
|
1480
|
-
];
|
|
1481
|
-
const sourceId = styleLayer.source;
|
|
1482
|
-
const heightField = styleLayer.metadata?.["tileflow:tree-height-field"] ?? "height";
|
|
1483
|
-
const crownField = styleLayer.metadata?.["tileflow:tree-crown-field"] ?? "diameter_crown";
|
|
1484
|
-
const genusField = styleLayer.metadata?.["tileflow:tree-genus-field"] ?? "genus";
|
|
1485
|
-
const leafTypeField = styleLayer.metadata?.["tileflow:tree-leaf-type-field"] ?? "leaf_type";
|
|
1486
|
-
const speciesField = styleLayer.metadata?.["tileflow:tree-species-field"] ?? "species";
|
|
1487
|
-
const heightScaleValue = Number(
|
|
1488
|
-
styleLayer.metadata?.["tileflow:tree-height-scale"] ?? 1
|
|
1489
|
-
);
|
|
1490
|
-
const crownScaleValue = Number(
|
|
1491
|
-
styleLayer.metadata?.["tileflow:tree-crown-scale"] ?? 1
|
|
1492
|
-
);
|
|
1493
|
-
const heightScale = Number.isFinite(heightScaleValue) && heightScaleValue > 0
|
|
1494
|
-
? heightScaleValue
|
|
1495
|
-
: 1;
|
|
1496
|
-
const crownScale = Number.isFinite(crownScaleValue) && crownScaleValue > 0
|
|
1497
|
-
? crownScaleValue
|
|
1498
|
-
: 1;
|
|
1499
|
-
let scene;
|
|
1500
|
-
let sceneElevation;
|
|
1501
|
-
let sceneOriginMercator;
|
|
1502
|
-
let camera;
|
|
1503
|
-
let renderer;
|
|
1504
|
-
let treeGroup;
|
|
1505
|
-
let treeMaterial;
|
|
1506
|
-
let treeGeometries = [];
|
|
1507
|
-
let treeMeshes = [];
|
|
1508
|
-
let treeMeshCapacities = [];
|
|
1509
|
-
let treeShadowGeometry;
|
|
1510
|
-
let treeShadowMesh;
|
|
1511
|
-
let treeShadowCapacity = 0;
|
|
1512
|
-
let treeShadowMaterial;
|
|
1513
|
-
let activeTreeBackend = treeBackendMode;
|
|
1514
|
-
let rawGl;
|
|
1515
|
-
let rawProgram;
|
|
1516
|
-
let rawMatrixUniform;
|
|
1517
|
-
let rawTreeResources = [];
|
|
1518
|
-
let rawShadowProgram;
|
|
1519
|
-
let rawShadowMatrixUniform;
|
|
1520
|
-
let rawShadowOpacityUniform;
|
|
1521
|
-
let rawShadowResource;
|
|
1522
|
-
let rawTreesVisible = false;
|
|
1523
|
-
const fallbackCircleOpacity = styleLayer.paint?.["circle-opacity"] ?? 0.82;
|
|
1524
|
-
const fallbackCircleStrokeOpacity =
|
|
1525
|
-
styleLayer.paint?.["circle-stroke-opacity"] ?? 0.55;
|
|
1526
|
-
let fallbackVisible = true;
|
|
1527
|
-
let emptyRefreshAttempts = 0;
|
|
1528
|
-
let emptyRefreshTimer;
|
|
1529
|
-
let lastRefreshTime = 0;
|
|
1530
|
-
let lastTreeSelectionSignature;
|
|
1531
|
-
let refreshQueued = false;
|
|
1532
|
-
let refreshTimer;
|
|
1533
|
-
let renderedTreeCount = 0;
|
|
1534
|
-
let sourceSettled = false;
|
|
1535
|
-
const treeStateCache = new Map();
|
|
1536
|
-
const matrix = new THREE.Matrix4();
|
|
1537
|
-
const mapMatrix = new THREE.Matrix4();
|
|
1538
|
-
const sceneMatrix = new THREE.Matrix4();
|
|
1539
|
-
const combinedMatrix = new THREE.Matrix4();
|
|
1540
|
-
const position = new THREE.Vector3();
|
|
1541
|
-
const scale = new THREE.Vector3();
|
|
1542
|
-
const crownRotation = new THREE.Quaternion();
|
|
1543
|
-
const yAxis = new THREE.Vector3(0, 1, 0);
|
|
1544
|
-
const instanceTint = new THREE.Color();
|
|
1545
|
-
const shadowMatrix = new THREE.Matrix4();
|
|
1546
|
-
const shadowPosition = new THREE.Vector3();
|
|
1547
|
-
const shadowScale = new THREE.Vector3();
|
|
1548
|
-
const shadowRotation = new THREE.Quaternion();
|
|
1549
|
-
const treeShadowLiftMeters = 0.04;
|
|
1550
|
-
const treeShadowOpacity = 0.17;
|
|
1551
|
-
const barkColor = new THREE.Color(
|
|
1552
|
-
styleLayer.metadata?.["tileflow:tree-bark-color"] ?? "#8F8E79"
|
|
1553
|
-
);
|
|
1554
|
-
const broadleafColorValues =
|
|
1555
|
-
styleLayer.metadata?.["tileflow:tree-broadleaf-colors"];
|
|
1556
|
-
const broadleafPalette = (
|
|
1557
|
-
Array.isArray(broadleafColorValues) && broadleafColorValues.length > 0
|
|
1558
|
-
? broadleafColorValues
|
|
1559
|
-
: ["#87BA8C", "#98C89A", "#AAD4A7", "#B8DDB1"]
|
|
1560
|
-
).map((color) => new THREE.Color(color));
|
|
1561
|
-
const coniferColorValues = styleLayer.metadata?.["tileflow:tree-conifer-colors"];
|
|
1562
|
-
const coniferPalette = (
|
|
1563
|
-
Array.isArray(coniferColorValues) && coniferColorValues.length > 0
|
|
1564
|
-
? coniferColorValues
|
|
1565
|
-
: ["#77A77E", "#87B58A", "#98C399"]
|
|
1566
|
-
).map((color) => new THREE.Color(color));
|
|
1567
|
-
const palmPalette = ["#82B77C", "#91C487", "#A0D093"].map(
|
|
1568
|
-
(color) => new THREE.Color(color)
|
|
1569
|
-
);
|
|
1570
|
-
|
|
1571
|
-
function addGeometryPart(parts, geometry, partPosition, partScale, rotation, color) {
|
|
1572
|
-
const partMatrix = new THREE.Matrix4().compose(
|
|
1573
|
-
new THREE.Vector3(...partPosition),
|
|
1574
|
-
rotation,
|
|
1575
|
-
new THREE.Vector3(...partScale)
|
|
1576
|
-
);
|
|
1577
|
-
geometry.applyMatrix4(partMatrix);
|
|
1578
|
-
if (!geometry.getAttribute("normal")) geometry.computeVertexNormals();
|
|
1579
|
-
const renderGeometry = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
1580
|
-
const positions = renderGeometry.getAttribute("position").array;
|
|
1581
|
-
const normals = renderGeometry.getAttribute("normal").array;
|
|
1582
|
-
const colors = new Float32Array((positions.length / 3) * 3);
|
|
1583
|
-
const indices = Uint32Array.from(
|
|
1584
|
-
{length: positions.length / 3},
|
|
1585
|
-
(_value, index) => index
|
|
1586
|
-
);
|
|
1587
|
-
for (let index = 0; index < colors.length; index += 3) {
|
|
1588
|
-
colors[index] = color.r;
|
|
1589
|
-
colors[index + 1] = color.g;
|
|
1590
|
-
colors[index + 2] = color.b;
|
|
1591
|
-
}
|
|
1592
|
-
parts.push({
|
|
1593
|
-
colors,
|
|
1594
|
-
indices,
|
|
1595
|
-
normals: new Float32Array(normals),
|
|
1596
|
-
positions: new Float32Array(positions)
|
|
1597
|
-
});
|
|
1598
|
-
if (renderGeometry !== geometry) renderGeometry.dispose();
|
|
1599
|
-
geometry.dispose();
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
function addTaperedBranchPart(
|
|
1603
|
-
parts,
|
|
1604
|
-
start,
|
|
1605
|
-
end,
|
|
1606
|
-
baseRadius,
|
|
1607
|
-
tipRadius,
|
|
1608
|
-
radialSegments = 6
|
|
1609
|
-
) {
|
|
1610
|
-
const startPoint = new THREE.Vector3(...start);
|
|
1611
|
-
const endPoint = new THREE.Vector3(...end);
|
|
1612
|
-
const direction = new THREE.Vector3().subVectors(endPoint, startPoint);
|
|
1613
|
-
const branchRotation = new THREE.Quaternion().setFromUnitVectors(
|
|
1614
|
-
yAxis,
|
|
1615
|
-
direction.clone().normalize()
|
|
1616
|
-
);
|
|
1617
|
-
const center = startPoint.clone().add(endPoint).multiplyScalar(0.5);
|
|
1618
|
-
addGeometryPart(
|
|
1619
|
-
parts,
|
|
1620
|
-
new THREE.CylinderGeometry(tipRadius, baseRadius, 1, radialSegments, 1, true),
|
|
1621
|
-
[center.x, center.y, center.z],
|
|
1622
|
-
[1, direction.length(), 1],
|
|
1623
|
-
branchRotation,
|
|
1624
|
-
barkColor
|
|
1625
|
-
);
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
function createOrganicCrownGeometry(seed, simple) {
|
|
1629
|
-
const geometry = new THREE.SphereGeometry(
|
|
1630
|
-
0.5,
|
|
1631
|
-
simple ? 8 : 12,
|
|
1632
|
-
simple ? 5 : 7
|
|
1633
|
-
);
|
|
1634
|
-
const positions = geometry.getAttribute("position");
|
|
1635
|
-
for (let index = 0; index < positions.count; index += 1) {
|
|
1636
|
-
const x = positions.getX(index);
|
|
1637
|
-
const y = positions.getY(index);
|
|
1638
|
-
const z = positions.getZ(index);
|
|
1639
|
-
const angle = Math.atan2(z, x);
|
|
1640
|
-
const normalizedHeight = Math.max(-1, Math.min(1, y * 2));
|
|
1641
|
-
const latitudeWeight = 1 - normalizedHeight * normalizedHeight;
|
|
1642
|
-
const ripple =
|
|
1643
|
-
1 +
|
|
1644
|
-
Math.sin(angle * 3 + seed * 1.73) * 0.065 * latitudeWeight +
|
|
1645
|
-
Math.sin(angle * 5 - seed * 0.91) * 0.035 * latitudeWeight +
|
|
1646
|
-
Math.sin(y * Math.PI * 4 + seed * 0.63) * 0.025;
|
|
1647
|
-
positions.setXYZ(
|
|
1648
|
-
index,
|
|
1649
|
-
x * ripple,
|
|
1650
|
-
y * (1 + Math.cos(angle * 2 + seed) * 0.035),
|
|
1651
|
-
z * ripple
|
|
1652
|
-
);
|
|
1653
|
-
}
|
|
1654
|
-
positions.needsUpdate = true;
|
|
1655
|
-
geometry.deleteAttribute("normal");
|
|
1656
|
-
geometry.computeVertexNormals();
|
|
1657
|
-
return geometry;
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
function createScallopedConeGeometry(seed, simple) {
|
|
1661
|
-
const geometry = new THREE.ConeGeometry(
|
|
1662
|
-
0.5,
|
|
1663
|
-
1,
|
|
1664
|
-
simple ? 8 : 12,
|
|
1665
|
-
simple ? 2 : 3
|
|
1666
|
-
);
|
|
1667
|
-
const positions = geometry.getAttribute("position");
|
|
1668
|
-
for (let index = 0; index < positions.count; index += 1) {
|
|
1669
|
-
const x = positions.getX(index);
|
|
1670
|
-
const y = positions.getY(index);
|
|
1671
|
-
const z = positions.getZ(index);
|
|
1672
|
-
const angle = Math.atan2(z, x);
|
|
1673
|
-
const progress = Math.max(0, Math.min(1, y + 0.5));
|
|
1674
|
-
const ripple =
|
|
1675
|
-
1 +
|
|
1676
|
-
Math.sin(angle * 3 + seed * 1.31) * 0.08 +
|
|
1677
|
-
Math.sin(angle * 5 - seed * 0.77) * 0.045 +
|
|
1678
|
-
Math.sin(progress * Math.PI * 3 + seed) * 0.035;
|
|
1679
|
-
positions.setXYZ(
|
|
1680
|
-
index,
|
|
1681
|
-
x * ripple,
|
|
1682
|
-
y + Math.sin(angle * 2 + seed) * (1 - progress) * 0.018,
|
|
1683
|
-
z * ripple
|
|
1684
|
-
);
|
|
1685
|
-
}
|
|
1686
|
-
positions.needsUpdate = true;
|
|
1687
|
-
geometry.deleteAttribute("normal");
|
|
1688
|
-
geometry.computeVertexNormals();
|
|
1689
|
-
return geometry;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
function createCurvedPalmFrondGeometry(seed, simple, fan) {
|
|
1693
|
-
const segments = simple ? 5 : 8;
|
|
1694
|
-
const length = fan ? 0.49 + (seed % 3) * 0.025 : 0.58 + (seed % 3) * 0.03;
|
|
1695
|
-
const droop = fan ? 0.16 + (seed % 2) * 0.025 : 0.25 + (seed % 3) * 0.025;
|
|
1696
|
-
const sway = (((seed * 37) % 7) - 3) * 0.008;
|
|
1697
|
-
const widthScale = fan ? 0.17 : 0.024;
|
|
1698
|
-
const centerAt = (progress) => [
|
|
1699
|
-
length * progress,
|
|
1700
|
-
Math.sin(Math.PI * progress) * (fan ? 0.08 : 0.11) -
|
|
1701
|
-
droop * Math.pow(progress, 1.7),
|
|
1702
|
-
sway * Math.sin(Math.PI * progress)
|
|
1703
|
-
];
|
|
1704
|
-
const points = [];
|
|
1705
|
-
for (let step = 0; step <= segments; step += 1) {
|
|
1706
|
-
const progress = step / segments;
|
|
1707
|
-
const envelope = Math.pow(Math.sin(Math.PI * progress), 0.7) * (1 - 0.25 * progress);
|
|
1708
|
-
const width = Math.max(0.008, widthScale * envelope);
|
|
1709
|
-
const [centerX, centerY, centerZ] = centerAt(progress);
|
|
1710
|
-
points.push(
|
|
1711
|
-
[centerX, centerY, centerZ - width],
|
|
1712
|
-
[centerX, centerY, centerZ + width]
|
|
1713
|
-
);
|
|
1714
|
-
}
|
|
1715
|
-
const trianglePositions = [];
|
|
1716
|
-
const appendTriangle = (first, second, third) => {
|
|
1717
|
-
trianglePositions.push(...first, ...second, ...third);
|
|
1718
|
-
};
|
|
1719
|
-
const appendDoubleSidedQuad = (leftStart, rightStart, leftEnd, rightEnd) => {
|
|
1720
|
-
appendTriangle(leftStart, rightStart, leftEnd);
|
|
1721
|
-
appendTriangle(rightStart, rightEnd, leftEnd);
|
|
1722
|
-
appendTriangle(leftEnd, rightStart, leftStart);
|
|
1723
|
-
appendTriangle(leftEnd, rightEnd, rightStart);
|
|
1724
|
-
};
|
|
1725
|
-
for (let segment = 0; segment < segments; segment += 1) {
|
|
1726
|
-
const leftStart = points[segment * 2];
|
|
1727
|
-
const rightStart = points[segment * 2 + 1];
|
|
1728
|
-
const leftEnd = points[(segment + 1) * 2];
|
|
1729
|
-
const rightEnd = points[(segment + 1) * 2 + 1];
|
|
1730
|
-
appendDoubleSidedQuad(leftStart, rightStart, leftEnd, rightEnd);
|
|
1731
|
-
}
|
|
1732
|
-
if (!fan) {
|
|
1733
|
-
const leafletCount = simple ? 4 : 7;
|
|
1734
|
-
for (let leaflet = 0; leaflet < leafletCount; leaflet += 1) {
|
|
1735
|
-
const progress = 0.17 + (leaflet / (leafletCount - 1)) * 0.66;
|
|
1736
|
-
const [centerX, centerY, centerZ] = centerAt(progress);
|
|
1737
|
-
const leafletLength =
|
|
1738
|
-
(0.075 + Math.sin(Math.PI * progress) * 0.095) *
|
|
1739
|
-
(0.94 + ((leaflet + seed) % 3) * 0.035);
|
|
1740
|
-
const leafletWidth = 0.018 + Math.sin(Math.PI * progress) * 0.012;
|
|
1741
|
-
const leafletBack = 0.018 + ((leaflet + seed) % 2) * 0.012;
|
|
1742
|
-
const leafletDrop = 0.022 + progress * 0.045;
|
|
1743
|
-
for (const side of [-1, 1]) {
|
|
1744
|
-
const base = [centerX - 0.014, centerY, centerZ + side * 0.007];
|
|
1745
|
-
const midCenter = [
|
|
1746
|
-
centerX + 0.012,
|
|
1747
|
-
centerY - leafletDrop * 0.35,
|
|
1748
|
-
centerZ + side * leafletLength * 0.58
|
|
1749
|
-
];
|
|
1750
|
-
const midLeft = [
|
|
1751
|
-
midCenter[0] - leafletWidth,
|
|
1752
|
-
midCenter[1],
|
|
1753
|
-
midCenter[2] - side * leafletWidth * 0.14
|
|
1754
|
-
];
|
|
1755
|
-
const midRight = [
|
|
1756
|
-
midCenter[0] + leafletWidth,
|
|
1757
|
-
midCenter[1],
|
|
1758
|
-
midCenter[2] + side * leafletWidth * 0.14
|
|
1759
|
-
];
|
|
1760
|
-
const tip = [
|
|
1761
|
-
centerX - leafletBack,
|
|
1762
|
-
centerY - leafletDrop,
|
|
1763
|
-
centerZ + side * leafletLength
|
|
1764
|
-
];
|
|
1765
|
-
appendTriangle(base, midRight, midLeft);
|
|
1766
|
-
appendTriangle(midLeft, midRight, tip);
|
|
1767
|
-
appendTriangle(midLeft, midRight, base);
|
|
1768
|
-
appendTriangle(tip, midRight, midLeft);
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
const geometry = new THREE.BufferGeometry();
|
|
1773
|
-
geometry.setAttribute(
|
|
1774
|
-
"position",
|
|
1775
|
-
new THREE.Float32BufferAttribute(trianglePositions, 3)
|
|
1776
|
-
);
|
|
1777
|
-
geometry.computeVertexNormals();
|
|
1778
|
-
return geometry;
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
function mergeGeometryParts(parts) {
|
|
1782
|
-
const positionLength = parts.reduce((total, part) => total + part.positions.length, 0);
|
|
1783
|
-
const positions = new Float32Array(positionLength);
|
|
1784
|
-
const normals = new Float32Array(positionLength);
|
|
1785
|
-
const colors = new Float32Array(positionLength);
|
|
1786
|
-
const indexLength = parts.reduce((total, part) => total + part.indices.length, 0);
|
|
1787
|
-
const indices = new Uint32Array(indexLength);
|
|
1788
|
-
let offset = 0;
|
|
1789
|
-
let indexOffset = 0;
|
|
1790
|
-
let vertexOffset = 0;
|
|
1791
|
-
for (const part of parts) {
|
|
1792
|
-
positions.set(part.positions, offset);
|
|
1793
|
-
normals.set(part.normals, offset);
|
|
1794
|
-
colors.set(part.colors, offset);
|
|
1795
|
-
for (let index = 0; index < part.indices.length; index += 1) {
|
|
1796
|
-
indices[indexOffset + index] = part.indices[index] + vertexOffset;
|
|
1797
|
-
}
|
|
1798
|
-
indexOffset += part.indices.length;
|
|
1799
|
-
vertexOffset += part.positions.length / 3;
|
|
1800
|
-
offset += part.positions.length;
|
|
1801
|
-
}
|
|
1802
|
-
const geometry = new THREE.BufferGeometry();
|
|
1803
|
-
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
1804
|
-
geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
|
|
1805
|
-
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
|
1806
|
-
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
|
|
1807
|
-
geometry.computeBoundingBox();
|
|
1808
|
-
geometry.computeBoundingSphere();
|
|
1809
|
-
return geometry;
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
function createBroadleafTreeGeometry(variant, geometryVariant, simple) {
|
|
1813
|
-
const parts = [];
|
|
1814
|
-
const trunkTops = [
|
|
1815
|
-
[0.008, 0.56, -0.004],
|
|
1816
|
-
[-0.012, 0.51, 0.008],
|
|
1817
|
-
[0.006, 0.62, 0.004]
|
|
1818
|
-
];
|
|
1819
|
-
const trunkTopBase = trunkTops[variant] ?? trunkTops[0];
|
|
1820
|
-
const trunkTop = [
|
|
1821
|
-
trunkTopBase[0] + (geometryVariant === 0 ? -0.008 : 0.012),
|
|
1822
|
-
trunkTopBase[1] + (geometryVariant === 0 ? -0.012 : 0.016),
|
|
1823
|
-
trunkTopBase[2] + (geometryVariant === 0 ? 0.006 : -0.01)
|
|
1824
|
-
];
|
|
1825
|
-
addTaperedBranchPart(
|
|
1826
|
-
parts,
|
|
1827
|
-
[0, 0, 0],
|
|
1828
|
-
trunkTop,
|
|
1829
|
-
0.04 + variant * 0.001,
|
|
1830
|
-
0.022,
|
|
1831
|
-
simple ? 6 : 8
|
|
1832
|
-
);
|
|
1833
|
-
|
|
1834
|
-
const branchTargetLayouts = [
|
|
1835
|
-
[
|
|
1836
|
-
[-0.2, 0.72, 0.03],
|
|
1837
|
-
[0.01, 0.79, -0.04],
|
|
1838
|
-
[0.21, 0.72, -0.02],
|
|
1839
|
-
[-0.03, 0.7, 0.18],
|
|
1840
|
-
[0.06, 0.73, -0.17]
|
|
1841
|
-
],
|
|
1842
|
-
[
|
|
1843
|
-
[-0.27, 0.68, 0.02],
|
|
1844
|
-
[-0.09, 0.76, -0.08],
|
|
1845
|
-
[0.25, 0.69, -0.03],
|
|
1846
|
-
[0.06, 0.72, 0.2],
|
|
1847
|
-
[0.13, 0.79, -0.13]
|
|
1848
|
-
],
|
|
1849
|
-
[
|
|
1850
|
-
[-0.15, 0.75, 0.02],
|
|
1851
|
-
[0.04, 0.86, -0.04],
|
|
1852
|
-
[0.16, 0.77, 0.01],
|
|
1853
|
-
[0.02, 0.8, 0.15]
|
|
1854
|
-
]
|
|
1855
|
-
];
|
|
1856
|
-
const branchTargets = branchTargetLayouts[variant] ?? branchTargetLayouts[0];
|
|
1857
|
-
const branchCount = simple ? Math.min(4, branchTargets.length) : branchTargets.length;
|
|
1858
|
-
for (let branch = 0; branch < branchCount; branch += 1) {
|
|
1859
|
-
const branchTarget = branchTargets[branch];
|
|
1860
|
-
const shelteredBranchTarget = [
|
|
1861
|
-
branchTarget[0] * 0.86,
|
|
1862
|
-
branchTarget[1] - 0.07,
|
|
1863
|
-
branchTarget[2] * 0.86
|
|
1864
|
-
];
|
|
1865
|
-
addTaperedBranchPart(
|
|
1866
|
-
parts,
|
|
1867
|
-
[trunkTop[0] * 0.55, 0.38 + branch * 0.018, trunkTop[2] * 0.55],
|
|
1868
|
-
shelteredBranchTarget,
|
|
1869
|
-
0.022 - branch * 0.0015,
|
|
1870
|
-
0.0095 - branch * 0.00065,
|
|
1871
|
-
simple ? 5 : 7
|
|
1872
|
-
);
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
const roundCrownLayout = [
|
|
1876
|
-
{position: [0, 0.8, 0.01], scale: [0.72, 0.39, 0.66]},
|
|
1877
|
-
{position: [-0.27, 0.73, 0.05], scale: [0.45, 0.26, 0.42]},
|
|
1878
|
-
{position: [0.27, 0.75, -0.06], scale: [0.44, 0.25, 0.41]},
|
|
1879
|
-
{position: [-0.08, 0.7, 0.24], scale: [0.4, 0.23, 0.37]},
|
|
1880
|
-
{position: [0.1, 0.72, -0.24], scale: [0.41, 0.24, 0.38]}
|
|
1881
|
-
];
|
|
1882
|
-
const openCrownLayout = [
|
|
1883
|
-
{position: [-0.29, 0.72, 0.03], scale: [0.47, 0.22, 0.39]},
|
|
1884
|
-
{position: [-0.08, 0.83, -0.1], scale: [0.44, 0.24, 0.36]},
|
|
1885
|
-
{position: [0.22, 0.79, -0.04], scale: [0.49, 0.22, 0.4]},
|
|
1886
|
-
{position: [0.31, 0.67, 0.09], scale: [0.4, 0.19, 0.34]},
|
|
1887
|
-
{position: [0.04, 0.68, 0.22], scale: [0.45, 0.21, 0.38]},
|
|
1888
|
-
{position: [-0.19, 0.65, 0.18], scale: [0.38, 0.18, 0.32]}
|
|
1889
|
-
];
|
|
1890
|
-
const tallCrownLayout = [
|
|
1891
|
-
{position: [0, 0.79, 0.01], scale: [0.53, 0.58, 0.48]},
|
|
1892
|
-
{position: [-0.19, 0.76, 0.05], scale: [0.34, 0.4, 0.32]},
|
|
1893
|
-
{position: [0.18, 0.8, -0.05], scale: [0.34, 0.42, 0.31]},
|
|
1894
|
-
{position: [-0.05, 0.94, -0.08], scale: [0.32, 0.34, 0.3]},
|
|
1895
|
-
{position: [0.06, 0.69, 0.15], scale: [0.32, 0.35, 0.3]}
|
|
1896
|
-
];
|
|
1897
|
-
const crownLayouts = [roundCrownLayout, openCrownLayout, tallCrownLayout];
|
|
1898
|
-
const crownLayout = crownLayouts[variant] ?? crownLayouts[0];
|
|
1899
|
-
const lobeCount = simple ? Math.max(4, crownLayout.length - 1) : crownLayout.length;
|
|
1900
|
-
const crownColorPatterns = [
|
|
1901
|
-
[2, 1, 3, 0, 1],
|
|
1902
|
-
[1, 2, 0, 3, 1, 2],
|
|
1903
|
-
[2, 1, 3, 1, 2]
|
|
1904
|
-
];
|
|
1905
|
-
const crownColorPattern = crownColorPatterns[variant] ?? crownColorPatterns[0];
|
|
1906
|
-
for (let lobe = 0; lobe < lobeCount; lobe += 1) {
|
|
1907
|
-
const lobeShape = crownLayout[lobe];
|
|
1908
|
-
const lobeSeed = variant * 23 + geometryVariant * 11 + lobe * 3;
|
|
1909
|
-
const lobePosition = [
|
|
1910
|
-
lobeShape.position[0] + Math.sin(lobeSeed * 1.7) * 0.018,
|
|
1911
|
-
lobeShape.position[1] + Math.cos(lobeSeed * 1.1) * 0.012,
|
|
1912
|
-
lobeShape.position[2] + Math.cos(lobeSeed * 1.9) * 0.018
|
|
1913
|
-
];
|
|
1914
|
-
addGeometryPart(
|
|
1915
|
-
parts,
|
|
1916
|
-
createOrganicCrownGeometry(lobeSeed, simple),
|
|
1917
|
-
lobePosition,
|
|
1918
|
-
lobeShape.scale,
|
|
1919
|
-
new THREE.Quaternion().setFromEuler(
|
|
1920
|
-
new THREE.Euler(
|
|
1921
|
-
((lobe % 3) - 1) * 0.09,
|
|
1922
|
-
variant * 0.41 + geometryVariant * 0.27 + lobe * 0.37,
|
|
1923
|
-
(((lobe + variant + geometryVariant) % 3) - 1) * 0.11
|
|
1924
|
-
)
|
|
1925
|
-
),
|
|
1926
|
-
broadleafPalette[
|
|
1927
|
-
(crownColorPattern[lobe] + geometryVariant * 2) % broadleafPalette.length
|
|
1928
|
-
]
|
|
1929
|
-
);
|
|
1930
|
-
}
|
|
1931
|
-
const crownCapShapes = [
|
|
1932
|
-
{position: [0.01, 0.9, 0], scale: [0.3, 0.18, 0.28]},
|
|
1933
|
-
{position: [0, 0.84, 0.01], scale: [0.29, 0.17, 0.27]},
|
|
1934
|
-
{position: [0, 0.96, 0], scale: [0.24, 0.22, 0.23]}
|
|
1935
|
-
];
|
|
1936
|
-
const crownCapShape = crownCapShapes[variant] ?? crownCapShapes[0];
|
|
1937
|
-
addGeometryPart(
|
|
1938
|
-
parts,
|
|
1939
|
-
createOrganicCrownGeometry(149 + variant * 17 + geometryVariant * 7, simple),
|
|
1940
|
-
crownCapShape.position,
|
|
1941
|
-
crownCapShape.scale,
|
|
1942
|
-
new THREE.Quaternion().setFromAxisAngle(
|
|
1943
|
-
yAxis,
|
|
1944
|
-
variant * 0.31 + geometryVariant * 0.43
|
|
1945
|
-
),
|
|
1946
|
-
broadleafPalette[(1 + variant + geometryVariant * 2) % broadleafPalette.length]
|
|
1947
|
-
);
|
|
1948
|
-
return mergeGeometryParts(parts);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
function createConiferTreeGeometry(variant, geometryVariant, simple) {
|
|
1952
|
-
const parts = [];
|
|
1953
|
-
const columnar = variant === 1;
|
|
1954
|
-
addTaperedBranchPart(
|
|
1955
|
-
parts,
|
|
1956
|
-
[0, 0, 0],
|
|
1957
|
-
[variant === 0 ? 0.006 : -0.006, columnar ? 0.54 : 0.43, 0],
|
|
1958
|
-
0.035,
|
|
1959
|
-
0.018,
|
|
1960
|
-
simple ? 6 : 8
|
|
1961
|
-
);
|
|
1962
|
-
if (columnar) {
|
|
1963
|
-
const columnarCrownLayout = [
|
|
1964
|
-
{position: [-0.01, 0.5, 0.01], scale: [0.38, 0.48, 0.36]},
|
|
1965
|
-
{position: [0.01, 0.66, -0.01], scale: [0.42, 0.5, 0.39]},
|
|
1966
|
-
{position: [-0.015, 0.8, 0.01], scale: [0.34, 0.42, 0.32]},
|
|
1967
|
-
{position: [0.008, 0.91, 0], scale: [0.22, 0.28, 0.21]}
|
|
1968
|
-
];
|
|
1969
|
-
const massCount = simple ? 3 : columnarCrownLayout.length;
|
|
1970
|
-
for (let mass = 0; mass < massCount; mass += 1) {
|
|
1971
|
-
const crownMass = columnarCrownLayout[mass];
|
|
1972
|
-
addGeometryPart(
|
|
1973
|
-
parts,
|
|
1974
|
-
createOrganicCrownGeometry(71 + geometryVariant * 13 + mass * 5, simple),
|
|
1975
|
-
crownMass.position,
|
|
1976
|
-
crownMass.scale,
|
|
1977
|
-
new THREE.Quaternion().setFromAxisAngle(yAxis, mass * 0.43),
|
|
1978
|
-
coniferPalette[(mass + 1) % coniferPalette.length]
|
|
1979
|
-
);
|
|
1980
|
-
}
|
|
1981
|
-
return mergeGeometryParts(parts);
|
|
1982
|
-
}
|
|
1983
|
-
const tierCount = simple ? 3 : 5;
|
|
1984
|
-
for (let tier = 0; tier < tierCount; tier += 1) {
|
|
1985
|
-
const progress = tier / (tierCount - 1);
|
|
1986
|
-
const tierDiameter = 0.94 - progress * 0.5;
|
|
1987
|
-
const tierHeight = 0.35 - progress * 0.075;
|
|
1988
|
-
addGeometryPart(
|
|
1989
|
-
parts,
|
|
1990
|
-
createScallopedConeGeometry(geometryVariant * 19 + tier * 7, simple),
|
|
1991
|
-
[
|
|
1992
|
-
Math.sin(tier * 2.1 + geometryVariant) * 0.012,
|
|
1993
|
-
0.45 + progress * 0.44,
|
|
1994
|
-
Math.cos(tier * 1.7 + geometryVariant) * 0.012
|
|
1995
|
-
],
|
|
1996
|
-
[tierDiameter, tierHeight, tierDiameter],
|
|
1997
|
-
new THREE.Quaternion().setFromAxisAngle(
|
|
1998
|
-
yAxis,
|
|
1999
|
-
variant * 0.6 + geometryVariant * 0.29 + tier * 0.31
|
|
2000
|
-
),
|
|
2001
|
-
coniferPalette[(variant + tier) % coniferPalette.length]
|
|
2002
|
-
);
|
|
2003
|
-
}
|
|
2004
|
-
return mergeGeometryParts(parts);
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
function createPalmTreeGeometry(geometryVariant, simple) {
|
|
2008
|
-
const parts = [];
|
|
2009
|
-
const fan = geometryVariant === 1;
|
|
2010
|
-
const lowerTrunkTop = [fan ? -0.014 : 0.012, 0.42, fan ? 0.006 : -0.004];
|
|
2011
|
-
const upperTrunkTop = [fan ? 0.014 : -0.012, fan ? 0.73 : 0.78, fan ? -0.008 : 0.012];
|
|
2012
|
-
addTaperedBranchPart(parts, [0, 0, 0], lowerTrunkTop, 0.032, 0.026, simple ? 7 : 9);
|
|
2013
|
-
addTaperedBranchPart(
|
|
2014
|
-
parts,
|
|
2015
|
-
lowerTrunkTop,
|
|
2016
|
-
upperTrunkTop,
|
|
2017
|
-
0.026,
|
|
2018
|
-
0.016,
|
|
2019
|
-
simple ? 7 : 9
|
|
2020
|
-
);
|
|
2021
|
-
addGeometryPart(
|
|
2022
|
-
parts,
|
|
2023
|
-
createOrganicCrownGeometry(113 + geometryVariant * 17, simple),
|
|
2024
|
-
[upperTrunkTop[0], upperTrunkTop[1] + 0.018, upperTrunkTop[2]],
|
|
2025
|
-
[fan ? 0.19 : 0.16, 0.1, fan ? 0.19 : 0.16],
|
|
2026
|
-
new THREE.Quaternion(),
|
|
2027
|
-
palmPalette[1]
|
|
2028
|
-
);
|
|
2029
|
-
const frondCount = simple ? (fan ? 8 : 7) : (fan ? 12 : 11);
|
|
2030
|
-
for (let frond = 0; frond < frondCount; frond += 1) {
|
|
2031
|
-
const angle =
|
|
2032
|
-
(frond / frondCount) * Math.PI * 2 +
|
|
2033
|
-
(((frond * 5 + geometryVariant) % 3) - 1) * 0.045;
|
|
2034
|
-
addGeometryPart(
|
|
2035
|
-
parts,
|
|
2036
|
-
createCurvedPalmFrondGeometry(frond + geometryVariant * 17, simple, fan),
|
|
2037
|
-
[upperTrunkTop[0], upperTrunkTop[1] + 0.018, upperTrunkTop[2]],
|
|
2038
|
-
[0.94 + (frond % 3) * 0.035, 0.96 + (frond % 2) * 0.035, 1],
|
|
2039
|
-
new THREE.Quaternion().setFromEuler(
|
|
2040
|
-
new THREE.Euler(
|
|
2041
|
-
(((frond * 7 + geometryVariant) % 3) - 1) * 0.045,
|
|
2042
|
-
-angle,
|
|
2043
|
-
(((frond * 5 + geometryVariant) % 5) - 2) * 0.022
|
|
2044
|
-
)
|
|
2045
|
-
),
|
|
2046
|
-
palmPalette[(frond + geometryVariant) % palmPalette.length]
|
|
2047
|
-
);
|
|
2048
|
-
}
|
|
2049
|
-
return mergeGeometryParts(parts);
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
function createTreeGeometries() {
|
|
2053
|
-
const simple = treeRendererMode === "simple";
|
|
2054
|
-
return [
|
|
2055
|
-
createBroadleafTreeGeometry(0, 0, simple),
|
|
2056
|
-
createBroadleafTreeGeometry(0, 1, simple),
|
|
2057
|
-
createBroadleafTreeGeometry(1, 0, simple),
|
|
2058
|
-
createBroadleafTreeGeometry(1, 1, simple),
|
|
2059
|
-
createBroadleafTreeGeometry(2, 0, simple),
|
|
2060
|
-
createBroadleafTreeGeometry(2, 1, simple),
|
|
2061
|
-
createConiferTreeGeometry(0, 0, simple),
|
|
2062
|
-
createConiferTreeGeometry(1, 0, simple),
|
|
2063
|
-
createPalmTreeGeometry(0, simple),
|
|
2064
|
-
createPalmTreeGeometry(1, simple)
|
|
2065
|
-
];
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
function compileRawShader(gl, type, source) {
|
|
2069
|
-
const shader = gl.createShader(type);
|
|
2070
|
-
if (!shader) throw new Error("Unable to create tree shader");
|
|
2071
|
-
gl.shaderSource(shader, source);
|
|
2072
|
-
gl.compileShader(shader);
|
|
2073
|
-
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
2074
|
-
const message = gl.getShaderInfoLog(shader) || "unknown shader error";
|
|
2075
|
-
gl.deleteShader(shader);
|
|
2076
|
-
throw new Error(message);
|
|
2077
|
-
}
|
|
2078
|
-
return shader;
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
function createRawTreeProgram(gl) {
|
|
2082
|
-
const vertexShader = compileRawShader(
|
|
2083
|
-
gl,
|
|
2084
|
-
gl.VERTEX_SHADER,
|
|
2085
|
-
"#version 300 es\\n" +
|
|
2086
|
-
"precision highp float;\\n" +
|
|
2087
|
-
"layout(location=0) in vec3 a_position;\\n" +
|
|
2088
|
-
"layout(location=1) in vec3 a_normal;\\n" +
|
|
2089
|
-
"layout(location=2) in vec3 a_color;\\n" +
|
|
2090
|
-
"layout(location=3) in vec3 a_instance_position;\\n" +
|
|
2091
|
-
"layout(location=4) in vec3 a_instance_scale;\\n" +
|
|
2092
|
-
"layout(location=5) in vec2 a_instance_rotation;\\n" +
|
|
2093
|
-
"layout(location=6) in float a_instance_tint;\\n" +
|
|
2094
|
-
"uniform mat4 u_matrix;\\n" +
|
|
2095
|
-
"out vec3 v_color;\\n" +
|
|
2096
|
-
"void main() {\\n" +
|
|
2097
|
-
" vec2 horizontal = a_position.xz * a_instance_scale.xz;\\n" +
|
|
2098
|
-
" vec2 rotated = vec2(\\n" +
|
|
2099
|
-
" horizontal.x * a_instance_rotation.x - horizontal.y * a_instance_rotation.y,\\n" +
|
|
2100
|
-
" horizontal.x * a_instance_rotation.y + horizontal.y * a_instance_rotation.x\\n" +
|
|
2101
|
-
" );\\n" +
|
|
2102
|
-
" vec3 scaledNormal = normalize(a_normal / max(a_instance_scale, vec3(0.001)));\\n" +
|
|
2103
|
-
" vec2 normalHorizontal = vec2(\\n" +
|
|
2104
|
-
" scaledNormal.x * a_instance_rotation.x - scaledNormal.z * a_instance_rotation.y,\\n" +
|
|
2105
|
-
" scaledNormal.x * a_instance_rotation.y + scaledNormal.z * a_instance_rotation.x\\n" +
|
|
2106
|
-
" );\\n" +
|
|
2107
|
-
" vec3 mapNormal = normalize(vec3(normalHorizontal.x, normalHorizontal.y, scaledNormal.y));\\n" +
|
|
2108
|
-
" float sky = mapNormal.z * 0.5 + 0.5;\\n" +
|
|
2109
|
-
" float key = max(dot(mapNormal, normalize(vec3(-0.35, -0.5, 0.78))), 0.0);\\n" +
|
|
2110
|
-
" float heightOcclusion = mix(0.84, 1.0, smoothstep(0.16, 0.88, a_position.y));\\n" +
|
|
2111
|
-
" float light = (0.67 + sky * 0.18 + key * 0.23) * heightOcclusion;\\n" +
|
|
2112
|
-
" v_color = a_color * light * a_instance_tint;\\n" +
|
|
2113
|
-
" vec3 mapPosition = vec3(\\n" +
|
|
2114
|
-
" a_instance_position.x + rotated.x,\\n" +
|
|
2115
|
-
" a_instance_position.z + rotated.y,\\n" +
|
|
2116
|
-
" a_instance_position.y + a_position.y * a_instance_scale.y\\n" +
|
|
2117
|
-
" );\\n" +
|
|
2118
|
-
" gl_Position = u_matrix * vec4(mapPosition, 1.0);\\n" +
|
|
2119
|
-
"}\\n"
|
|
2120
|
-
);
|
|
2121
|
-
const fragmentShader = compileRawShader(
|
|
2122
|
-
gl,
|
|
2123
|
-
gl.FRAGMENT_SHADER,
|
|
2124
|
-
"#version 300 es\\n" +
|
|
2125
|
-
"precision highp float;\\n" +
|
|
2126
|
-
"in vec3 v_color;\\n" +
|
|
2127
|
-
"out vec4 out_color;\\n" +
|
|
2128
|
-
"vec3 linearToSrgb(vec3 color) {\\n" +
|
|
2129
|
-
" vec3 lower = color * 12.92;\\n" +
|
|
2130
|
-
" vec3 upper = 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055;\\n" +
|
|
2131
|
-
" return mix(lower, upper, step(vec3(0.0031308), color));\\n" +
|
|
2132
|
-
"}\\n" +
|
|
2133
|
-
"void main() { out_color = vec4(linearToSrgb(v_color), 1.0); }\\n"
|
|
2134
|
-
);
|
|
2135
|
-
const program = gl.createProgram();
|
|
2136
|
-
if (!program) throw new Error("Unable to create tree program");
|
|
2137
|
-
gl.attachShader(program, vertexShader);
|
|
2138
|
-
gl.attachShader(program, fragmentShader);
|
|
2139
|
-
gl.linkProgram(program);
|
|
2140
|
-
gl.deleteShader(vertexShader);
|
|
2141
|
-
gl.deleteShader(fragmentShader);
|
|
2142
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
2143
|
-
const message = gl.getProgramInfoLog(program) || "unknown program error";
|
|
2144
|
-
gl.deleteProgram(program);
|
|
2145
|
-
throw new Error(message);
|
|
2146
|
-
}
|
|
2147
|
-
return program;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
function createRawTreeShadowProgram(gl) {
|
|
2151
|
-
const vertexShader = compileRawShader(
|
|
2152
|
-
gl,
|
|
2153
|
-
gl.VERTEX_SHADER,
|
|
2154
|
-
"#version 300 es\\n" +
|
|
2155
|
-
"precision highp float;\\n" +
|
|
2156
|
-
"layout(location=0) in vec2 a_corner;\\n" +
|
|
2157
|
-
"layout(location=1) in vec4 a_instance;\\n" +
|
|
2158
|
-
"uniform mat4 u_matrix;\\n" +
|
|
2159
|
-
"out vec2 v_shadow_coordinate;\\n" +
|
|
2160
|
-
"void main() {\\n" +
|
|
2161
|
-
" v_shadow_coordinate = a_corner;\\n" +
|
|
2162
|
-
" vec2 shadowOffset = vec2(-0.28, 0.34) * a_instance.w;\\n" +
|
|
2163
|
-
" vec2 shadowPosition = a_corner * vec2(1.0, 0.76) * a_instance.w + shadowOffset;\\n" +
|
|
2164
|
-
" vec3 mapPosition = vec3(\\n" +
|
|
2165
|
-
" a_instance.x + shadowPosition.x,\\n" +
|
|
2166
|
-
" a_instance.z + shadowPosition.y,\\n" +
|
|
2167
|
-
" a_instance.y + 0.04\\n" +
|
|
2168
|
-
" );\\n" +
|
|
2169
|
-
" gl_Position = u_matrix * vec4(mapPosition, 1.0);\\n" +
|
|
2170
|
-
"}\\n"
|
|
2171
|
-
);
|
|
2172
|
-
const fragmentShader = compileRawShader(
|
|
2173
|
-
gl,
|
|
2174
|
-
gl.FRAGMENT_SHADER,
|
|
2175
|
-
"#version 300 es\\n" +
|
|
2176
|
-
"precision mediump float;\\n" +
|
|
2177
|
-
"in vec2 v_shadow_coordinate;\\n" +
|
|
2178
|
-
"uniform float u_opacity;\\n" +
|
|
2179
|
-
"out vec4 out_color;\\n" +
|
|
2180
|
-
"void main() {\\n" +
|
|
2181
|
-
" float radiusSquared = dot(v_shadow_coordinate, v_shadow_coordinate);\\n" +
|
|
2182
|
-
" if (radiusSquared >= 1.0) discard;\\n" +
|
|
2183
|
-
" float coverage = 1.0 - smoothstep(0.90, 1.0, radiusSquared);\\n" +
|
|
2184
|
-
" float alpha = u_opacity * coverage;\\n" +
|
|
2185
|
-
" out_color = vec4(0.18, 0.21, 0.17, alpha);\\n" +
|
|
2186
|
-
"}\\n"
|
|
2187
|
-
);
|
|
2188
|
-
const program = gl.createProgram();
|
|
2189
|
-
if (!program) throw new Error("Unable to create tree shadow program");
|
|
2190
|
-
gl.attachShader(program, vertexShader);
|
|
2191
|
-
gl.attachShader(program, fragmentShader);
|
|
2192
|
-
gl.linkProgram(program);
|
|
2193
|
-
gl.deleteShader(vertexShader);
|
|
2194
|
-
gl.deleteShader(fragmentShader);
|
|
2195
|
-
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
2196
|
-
const message = gl.getProgramInfoLog(program) || "unknown tree shadow program error";
|
|
2197
|
-
gl.deleteProgram(program);
|
|
2198
|
-
throw new Error(message);
|
|
2199
|
-
}
|
|
2200
|
-
return program;
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
function createRawTreeShadowResource(gl) {
|
|
2204
|
-
const vertexArray = gl.createVertexArray();
|
|
2205
|
-
const cornerBuffer = gl.createBuffer();
|
|
2206
|
-
const instanceBuffer = gl.createBuffer();
|
|
2207
|
-
if (!vertexArray || !cornerBuffer || !instanceBuffer) {
|
|
2208
|
-
throw new Error("Unable to allocate tree shadow buffers");
|
|
2209
|
-
}
|
|
2210
|
-
gl.bindVertexArray(vertexArray);
|
|
2211
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
|
|
2212
|
-
gl.bufferData(
|
|
2213
|
-
gl.ARRAY_BUFFER,
|
|
2214
|
-
new Float32Array([-1, -1, 1, -1, 1, 1, -1, 1]),
|
|
2215
|
-
gl.STATIC_DRAW
|
|
2216
|
-
);
|
|
2217
|
-
gl.enableVertexAttribArray(0);
|
|
2218
|
-
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
|
2219
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
2220
|
-
gl.bufferData(gl.ARRAY_BUFFER, 0, gl.DYNAMIC_DRAW);
|
|
2221
|
-
gl.enableVertexAttribArray(1);
|
|
2222
|
-
gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 16, 0);
|
|
2223
|
-
gl.vertexAttribDivisor(1, 1);
|
|
2224
|
-
gl.bindVertexArray(null);
|
|
2225
|
-
return {cornerBuffer, count: 0, instanceBuffer, vertexArray};
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
function createRawTreeResource(gl, geometry) {
|
|
2229
|
-
const vertexArray = gl.createVertexArray();
|
|
2230
|
-
const positionBuffer = gl.createBuffer();
|
|
2231
|
-
const normalBuffer = gl.createBuffer();
|
|
2232
|
-
const colorBuffer = gl.createBuffer();
|
|
2233
|
-
const indexBuffer = gl.createBuffer();
|
|
2234
|
-
const instanceBuffer = gl.createBuffer();
|
|
2235
|
-
if (
|
|
2236
|
-
!vertexArray ||
|
|
2237
|
-
!positionBuffer ||
|
|
2238
|
-
!normalBuffer ||
|
|
2239
|
-
!colorBuffer ||
|
|
2240
|
-
!indexBuffer ||
|
|
2241
|
-
!instanceBuffer
|
|
2242
|
-
) {
|
|
2243
|
-
throw new Error("Unable to allocate tree buffers");
|
|
2244
|
-
}
|
|
2245
|
-
gl.bindVertexArray(vertexArray);
|
|
2246
|
-
const attributes = [
|
|
2247
|
-
[0, positionBuffer, geometry.getAttribute("position").array],
|
|
2248
|
-
[1, normalBuffer, geometry.getAttribute("normal").array],
|
|
2249
|
-
[2, colorBuffer, geometry.getAttribute("color").array]
|
|
2250
|
-
];
|
|
2251
|
-
for (const [location, buffer, values] of attributes) {
|
|
2252
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
|
2253
|
-
gl.bufferData(gl.ARRAY_BUFFER, values, gl.STATIC_DRAW);
|
|
2254
|
-
gl.enableVertexAttribArray(location);
|
|
2255
|
-
gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
|
|
2256
|
-
}
|
|
2257
|
-
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
2258
|
-
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.index.array, gl.STATIC_DRAW);
|
|
2259
|
-
gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
|
|
2260
|
-
gl.bufferData(gl.ARRAY_BUFFER, 0, gl.DYNAMIC_DRAW);
|
|
2261
|
-
gl.enableVertexAttribArray(3);
|
|
2262
|
-
gl.vertexAttribPointer(3, 3, gl.FLOAT, false, 36, 0);
|
|
2263
|
-
gl.vertexAttribDivisor(3, 1);
|
|
2264
|
-
gl.enableVertexAttribArray(4);
|
|
2265
|
-
gl.vertexAttribPointer(4, 3, gl.FLOAT, false, 36, 12);
|
|
2266
|
-
gl.vertexAttribDivisor(4, 1);
|
|
2267
|
-
gl.enableVertexAttribArray(5);
|
|
2268
|
-
gl.vertexAttribPointer(5, 2, gl.FLOAT, false, 36, 24);
|
|
2269
|
-
gl.vertexAttribDivisor(5, 1);
|
|
2270
|
-
gl.enableVertexAttribArray(6);
|
|
2271
|
-
gl.vertexAttribPointer(6, 1, gl.FLOAT, false, 36, 32);
|
|
2272
|
-
gl.vertexAttribDivisor(6, 1);
|
|
2273
|
-
gl.bindVertexArray(null);
|
|
2274
|
-
return {
|
|
2275
|
-
colorBuffer,
|
|
2276
|
-
count: 0,
|
|
2277
|
-
indexBuffer,
|
|
2278
|
-
indexCount: geometry.index.count,
|
|
2279
|
-
instanceBuffer,
|
|
2280
|
-
normalBuffer,
|
|
2281
|
-
positionBuffer,
|
|
2282
|
-
vertexArray
|
|
2283
|
-
};
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
function disposeRawTreeResources() {
|
|
2287
|
-
if (!rawGl) return;
|
|
2288
|
-
for (const resource of rawTreeResources) {
|
|
2289
|
-
rawGl.deleteBuffer(resource.positionBuffer);
|
|
2290
|
-
rawGl.deleteBuffer(resource.normalBuffer);
|
|
2291
|
-
rawGl.deleteBuffer(resource.colorBuffer);
|
|
2292
|
-
rawGl.deleteBuffer(resource.indexBuffer);
|
|
2293
|
-
rawGl.deleteBuffer(resource.instanceBuffer);
|
|
2294
|
-
rawGl.deleteVertexArray(resource.vertexArray);
|
|
2295
|
-
}
|
|
2296
|
-
rawTreeResources = [];
|
|
2297
|
-
if (rawProgram) rawGl.deleteProgram(rawProgram);
|
|
2298
|
-
if (rawShadowResource) {
|
|
2299
|
-
rawGl.deleteBuffer(rawShadowResource.cornerBuffer);
|
|
2300
|
-
rawGl.deleteBuffer(rawShadowResource.instanceBuffer);
|
|
2301
|
-
rawGl.deleteVertexArray(rawShadowResource.vertexArray);
|
|
2302
|
-
}
|
|
2303
|
-
if (rawShadowProgram) rawGl.deleteProgram(rawShadowProgram);
|
|
2304
|
-
rawProgram = undefined;
|
|
2305
|
-
rawMatrixUniform = undefined;
|
|
2306
|
-
rawShadowProgram = undefined;
|
|
2307
|
-
rawShadowMatrixUniform = undefined;
|
|
2308
|
-
rawShadowOpacityUniform = undefined;
|
|
2309
|
-
rawShadowResource = undefined;
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
function numberProperty(properties, name) {
|
|
2313
|
-
const value = Number(properties?.[name]);
|
|
2314
|
-
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
function stableTreeKey(feature, lng, lat) {
|
|
2318
|
-
const properties = feature.properties ?? {};
|
|
2319
|
-
const sourceFeatureId =
|
|
2320
|
-
properties.osm_id ??
|
|
2321
|
-
properties.osmId ??
|
|
2322
|
-
properties.id ??
|
|
2323
|
-
properties["@id"] ??
|
|
2324
|
-
feature.id;
|
|
2325
|
-
return sourceFeatureId === undefined || sourceFeatureId === null
|
|
2326
|
-
? lng.toFixed(6) + ":" + lat.toFixed(6)
|
|
2327
|
-
: String(sourceFeatureId);
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
function stableUnit(key, salt = 0) {
|
|
2331
|
-
let hash = (2166136261 ^ salt) >>> 0;
|
|
2332
|
-
for (let index = 0; index < key.length; index += 1) {
|
|
2333
|
-
hash ^= key.charCodeAt(index);
|
|
2334
|
-
hash = Math.imul(hash, 16777619);
|
|
2335
|
-
}
|
|
2336
|
-
hash += hash << 13;
|
|
2337
|
-
hash ^= hash >>> 7;
|
|
2338
|
-
hash += hash << 3;
|
|
2339
|
-
hash ^= hash >>> 17;
|
|
2340
|
-
hash += hash << 5;
|
|
2341
|
-
return (hash >>> 0) / 4294967296;
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
function treeLodForZoom(zoom) {
|
|
2345
|
-
return treeLods.find((lod) => zoom < lod.untilZoom) ?? treeLods.at(-1);
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
function setFallbackVisible(visible) {
|
|
2349
|
-
if (fallbackVisible === visible || !map.getLayer(styleLayer.id)) return;
|
|
2350
|
-
map.setPaintProperty(
|
|
2351
|
-
styleLayer.id,
|
|
2352
|
-
"circle-opacity",
|
|
2353
|
-
visible ? fallbackCircleOpacity : 0
|
|
2354
|
-
);
|
|
2355
|
-
map.setPaintProperty(
|
|
2356
|
-
styleLayer.id,
|
|
2357
|
-
"circle-stroke-opacity",
|
|
2358
|
-
visible ? fallbackCircleStrokeOpacity : 0
|
|
2359
|
-
);
|
|
2360
|
-
fallbackVisible = visible;
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
function updateFallbackVisibility() {
|
|
2364
|
-
// A non-empty query can still represent only one loaded tile. Keep
|
|
2365
|
-
// the vector circles underneath until MapLibre reports the complete
|
|
2366
|
-
// source settled, otherwise a partial 3D batch erases neighbouring
|
|
2367
|
-
// tiles at source boundaries.
|
|
2368
|
-
setFallbackVisible(!sourceSettled || renderedTreeCount === 0);
|
|
2369
|
-
}
|
|
2370
|
-
|
|
2371
|
-
function treeSelectionSignature(trees, lodName) {
|
|
2372
|
-
let hash = 2166136261;
|
|
2373
|
-
for (const tree of trees) {
|
|
2374
|
-
for (let index = 0; index < tree.key.length; index += 1) {
|
|
2375
|
-
hash ^= tree.key.charCodeAt(index);
|
|
2376
|
-
hash = Math.imul(hash, 16777619);
|
|
2377
|
-
}
|
|
2378
|
-
hash ^= 0xff;
|
|
2379
|
-
hash = Math.imul(hash, 16777619);
|
|
2380
|
-
}
|
|
2381
|
-
return lodName + ":" + trees.length + ":" + (hash >>> 0);
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
function selectVisibleTrees(features, lod) {
|
|
2385
|
-
const worldSize = 512 * Math.pow(2, map.getZoom());
|
|
2386
|
-
const seen = new Set();
|
|
2387
|
-
const candidates = [];
|
|
2388
|
-
for (const feature of features) {
|
|
2389
|
-
if (feature.geometry?.type !== "Point") continue;
|
|
2390
|
-
const [lng, lat] = feature.geometry.coordinates;
|
|
2391
|
-
const key = stableTreeKey(feature, lng, lat);
|
|
2392
|
-
if (seen.has(key)) continue;
|
|
2393
|
-
seen.add(key);
|
|
2394
|
-
const latitudeSine = Math.sin(
|
|
2395
|
-
(Math.max(-85.051129, Math.min(85.051129, lat)) * Math.PI) / 180
|
|
2396
|
-
);
|
|
2397
|
-
const worldX = ((lng + 180) / 360) * worldSize;
|
|
2398
|
-
const worldY =
|
|
2399
|
-
(0.5 - Math.log((1 + latitudeSine) / (1 - latitudeSine)) / (4 * Math.PI)) *
|
|
2400
|
-
worldSize;
|
|
2401
|
-
candidates.push({feature, key, lat, lng, worldX, worldY});
|
|
2402
|
-
}
|
|
2403
|
-
candidates.sort((left, right) =>
|
|
2404
|
-
left.key < right.key ? -1 : left.key > right.key ? 1 : 0
|
|
2405
|
-
);
|
|
2406
|
-
if (lod.densityCellPixels === 0) return candidates;
|
|
2407
|
-
const occupiedCells = new Set();
|
|
2408
|
-
const selected = [];
|
|
2409
|
-
for (const candidate of candidates) {
|
|
2410
|
-
const cellKey =
|
|
2411
|
-
Math.floor(candidate.worldX / lod.densityCellPixels) +
|
|
2412
|
-
":" +
|
|
2413
|
-
Math.floor(candidate.worldY / lod.densityCellPixels);
|
|
2414
|
-
if (occupiedCells.has(cellKey)) continue;
|
|
2415
|
-
occupiedCells.add(cellKey);
|
|
2416
|
-
selected.push(candidate);
|
|
2417
|
-
}
|
|
2418
|
-
return selected;
|
|
2419
|
-
}
|
|
2420
|
-
|
|
2421
|
-
function nextTreeInstanceCapacity(requiredCapacity) {
|
|
2422
|
-
let capacity = initialTreeInstanceCapacity;
|
|
2423
|
-
while (capacity < requiredCapacity) capacity *= 2;
|
|
2424
|
-
return capacity;
|
|
2425
|
-
}
|
|
2426
|
-
|
|
2427
|
-
function createThreeTreeMesh(geometry, capacity) {
|
|
2428
|
-
const treeMesh = new THREE.InstancedMesh(geometry, treeMaterial, capacity);
|
|
2429
|
-
treeMesh.count = 0;
|
|
2430
|
-
treeMesh.frustumCulled = false;
|
|
2431
|
-
return treeMesh;
|
|
2432
|
-
}
|
|
2433
|
-
|
|
2434
|
-
function ensureThreeTreeMeshCapacity(variantIndex, requiredCapacity) {
|
|
2435
|
-
if (treeMeshCapacities[variantIndex] >= requiredCapacity) return;
|
|
2436
|
-
const currentTreeMesh = treeMeshes[variantIndex];
|
|
2437
|
-
const treeMesh = createThreeTreeMesh(
|
|
2438
|
-
treeGeometries[variantIndex],
|
|
2439
|
-
nextTreeInstanceCapacity(requiredCapacity)
|
|
2440
|
-
);
|
|
2441
|
-
treeGroup.remove(currentTreeMesh);
|
|
2442
|
-
currentTreeMesh.dispose();
|
|
2443
|
-
treeMeshes[variantIndex] = treeMesh;
|
|
2444
|
-
treeMeshCapacities[variantIndex] = treeMesh.instanceMatrix.count;
|
|
2445
|
-
treeGroup.add(treeMesh);
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
function ensureThreeTreeShadowCapacity(requiredCapacity) {
|
|
2449
|
-
if (treeShadowCapacity >= requiredCapacity) return;
|
|
2450
|
-
const shadowMesh = new THREE.InstancedMesh(
|
|
2451
|
-
treeShadowGeometry,
|
|
2452
|
-
treeShadowMaterial,
|
|
2453
|
-
nextTreeInstanceCapacity(requiredCapacity)
|
|
2454
|
-
);
|
|
2455
|
-
shadowMesh.count = 0;
|
|
2456
|
-
shadowMesh.frustumCulled = false;
|
|
2457
|
-
shadowMesh.renderOrder = 1;
|
|
2458
|
-
treeGroup.remove(treeShadowMesh);
|
|
2459
|
-
treeShadowMesh.dispose();
|
|
2460
|
-
treeShadowMesh = shadowMesh;
|
|
2461
|
-
treeShadowCapacity = shadowMesh.instanceMatrix.count;
|
|
2462
|
-
treeGroup.add(shadowMesh);
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
function rememberTreeState(key, state) {
|
|
2466
|
-
if (treeStateCache.size >= maximumCachedTrees) {
|
|
2467
|
-
treeStateCache.delete(treeStateCache.keys().next().value);
|
|
2468
|
-
}
|
|
2469
|
-
treeStateCache.set(key, state);
|
|
2470
|
-
return state;
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2473
|
-
const treeFormDimensions = {
|
|
2474
|
-
round: {crownMaximum: 0.9, crownMinimum: 0.66, heightMaximum: 12, heightMinimum: 6},
|
|
2475
|
-
open: {crownMaximum: 1.05, crownMinimum: 0.8, heightMaximum: 10, heightMinimum: 5},
|
|
2476
|
-
tall: {crownMaximum: 0.68, crownMinimum: 0.48, heightMaximum: 17, heightMinimum: 9},
|
|
2477
|
-
conifer: {crownMaximum: 0.66, crownMinimum: 0.46, heightMaximum: 16, heightMinimum: 7},
|
|
2478
|
-
columnar: {crownMaximum: 0.36, crownMinimum: 0.22, heightMaximum: 18, heightMinimum: 9},
|
|
2479
|
-
palm: {crownMaximum: 0.5, crownMinimum: 0.34, heightMaximum: 16, heightMinimum: 8}
|
|
2480
|
-
};
|
|
2481
|
-
|
|
2482
|
-
function resolveTreeState(feature, key, lng, lat, treeElevation) {
|
|
2483
|
-
const cached = treeStateCache.get(key);
|
|
2484
|
-
if (cached) return cached;
|
|
2485
|
-
const variation = stableUnit(key, 1);
|
|
2486
|
-
const form = resolveTreeForm(feature.properties, key);
|
|
2487
|
-
const dimensions = treeFormDimensions[form] ?? treeFormDimensions.round;
|
|
2488
|
-
const baseHeight = Math.min(
|
|
2489
|
-
32,
|
|
2490
|
-
numberProperty(feature.properties, heightField) ??
|
|
2491
|
-
dimensions.heightMinimum +
|
|
2492
|
-
variation * (dimensions.heightMaximum - dimensions.heightMinimum)
|
|
2493
|
-
);
|
|
2494
|
-
const baseCrownDiameter = Math.min(
|
|
2495
|
-
20,
|
|
2496
|
-
numberProperty(feature.properties, crownField) ??
|
|
2497
|
-
baseHeight *
|
|
2498
|
-
(dimensions.crownMinimum +
|
|
2499
|
-
stableUnit(key, 2) * (dimensions.crownMaximum - dimensions.crownMinimum))
|
|
2500
|
-
);
|
|
2501
|
-
return rememberTreeState(key, {
|
|
2502
|
-
colorGain: 0.94 + stableUnit(key, 13) * 0.12,
|
|
2503
|
-
crownAspect: 0.92 + stableUnit(key, 11) * 0.16,
|
|
2504
|
-
crownDiameter:
|
|
2505
|
-
baseCrownDiameter * crownScale * (0.94 + stableUnit(key, 5) * 0.12),
|
|
2506
|
-
form,
|
|
2507
|
-
height: baseHeight * heightScale,
|
|
2508
|
-
lat,
|
|
2509
|
-
lng,
|
|
2510
|
-
treeElevation,
|
|
2511
|
-
variantIndex: treeVariantIndex(form, key)
|
|
2512
|
-
});
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
function ensureSceneOrigin(elevation) {
|
|
2516
|
-
if (sceneOriginMercator) return;
|
|
2517
|
-
const center = map.getCenter();
|
|
2518
|
-
sceneElevation = elevation;
|
|
2519
|
-
sceneOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(
|
|
2520
|
-
center,
|
|
2521
|
-
sceneElevation
|
|
2522
|
-
);
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
function localTreePosition(lng, lat, elevation) {
|
|
2526
|
-
const mercator = maplibregl.MercatorCoordinate.fromLngLat([lng, lat]);
|
|
2527
|
-
const mercatorPerMeter = sceneOriginMercator.meterInMercatorCoordinateUnits();
|
|
2528
|
-
return new THREE.Vector3(
|
|
2529
|
-
(mercator.x - sceneOriginMercator.x) / mercatorPerMeter,
|
|
2530
|
-
elevation - sceneElevation,
|
|
2531
|
-
(sceneOriginMercator.y - mercator.y) / mercatorPerMeter
|
|
2532
|
-
);
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
function createTerrainSampler(trees, fallbackElevation) {
|
|
2536
|
-
const gridSize = 5;
|
|
2537
|
-
const longitudes = trees.map((tree) => tree.lng);
|
|
2538
|
-
const latitudes = trees.map((tree) => tree.lat);
|
|
2539
|
-
const minimumLongitude = Math.min(...longitudes);
|
|
2540
|
-
const maximumLongitude = Math.max(...longitudes);
|
|
2541
|
-
const minimumLatitude = Math.min(...latitudes);
|
|
2542
|
-
const maximumLatitude = Math.max(...latitudes);
|
|
2543
|
-
const longitudeSpan = Math.max(1e-9, maximumLongitude - minimumLongitude);
|
|
2544
|
-
const latitudeSpan = Math.max(1e-9, maximumLatitude - minimumLatitude);
|
|
2545
|
-
const elevations = new Float32Array(gridSize * gridSize);
|
|
2546
|
-
for (let y = 0; y < gridSize; y += 1) {
|
|
2547
|
-
for (let x = 0; x < gridSize; x += 1) {
|
|
2548
|
-
const longitude = minimumLongitude + (x / (gridSize - 1)) * longitudeSpan;
|
|
2549
|
-
const latitude = minimumLatitude + (y / (gridSize - 1)) * latitudeSpan;
|
|
2550
|
-
elevations[y * gridSize + x] =
|
|
2551
|
-
map.queryTerrainElevation([longitude, latitude]) ?? fallbackElevation;
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2554
|
-
return (longitude, latitude) => {
|
|
2555
|
-
const gridX = Math.max(
|
|
2556
|
-
0,
|
|
2557
|
-
Math.min(gridSize - 1, ((longitude - minimumLongitude) / longitudeSpan) * (gridSize - 1))
|
|
2558
|
-
);
|
|
2559
|
-
const gridY = Math.max(
|
|
2560
|
-
0,
|
|
2561
|
-
Math.min(gridSize - 1, ((latitude - minimumLatitude) / latitudeSpan) * (gridSize - 1))
|
|
2562
|
-
);
|
|
2563
|
-
const x0 = Math.floor(gridX);
|
|
2564
|
-
const y0 = Math.floor(gridY);
|
|
2565
|
-
const x1 = Math.min(gridSize - 1, x0 + 1);
|
|
2566
|
-
const y1 = Math.min(gridSize - 1, y0 + 1);
|
|
2567
|
-
const xMix = gridX - x0;
|
|
2568
|
-
const yMix = gridY - y0;
|
|
2569
|
-
const bottom = elevations[y0 * gridSize + x0] * (1 - xMix) +
|
|
2570
|
-
elevations[y0 * gridSize + x1] * xMix;
|
|
2571
|
-
const top = elevations[y1 * gridSize + x0] * (1 - xMix) +
|
|
2572
|
-
elevations[y1 * gridSize + x1] * xMix;
|
|
2573
|
-
return bottom * (1 - yMix) + top * yMix;
|
|
2574
|
-
};
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
function botanicalName(properties) {
|
|
2578
|
-
return [properties?.[genusField], properties?.[speciesField]]
|
|
2579
|
-
.filter(Boolean)
|
|
2580
|
-
.join(" ")
|
|
2581
|
-
.toLowerCase();
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
function resolveTreeForm(properties, key) {
|
|
2585
|
-
const leafType = String(properties?.[leafTypeField] ?? "").toLowerCase();
|
|
2586
|
-
const botanical = botanicalName(properties);
|
|
2587
|
-
if (
|
|
2588
|
-
leafType.includes("palm") ||
|
|
2589
|
-
/(?:^| )(?:archontophoenix|arecaceae|butia|chamaerops|cocos|howea|livistona|phoenix|sabal|syagrus|trachycarpus|washingtonia)(?: |$)/.test(
|
|
2590
|
-
botanical
|
|
2591
|
-
)
|
|
2592
|
-
) return "palm";
|
|
2593
|
-
if (/(?:^| )(?:cupressus|cypress)(?: |$)/.test(botanical)) return "columnar";
|
|
2594
|
-
if (
|
|
2595
|
-
leafType.includes("needle") ||
|
|
2596
|
-
/(?:^| )(?:abies|cedrus|juniperus|picea|pinus|taxodium|taxus)(?: |$)/.test(botanical)
|
|
2597
|
-
) return "conifer";
|
|
2598
|
-
if (
|
|
2599
|
-
/(?:^| )(?:arbutus|olea|quercus|tamarix)(?: |$)/.test(botanical)
|
|
2600
|
-
) return "open";
|
|
2601
|
-
if (
|
|
2602
|
-
/(?:^| )(?:celtis|ginkgo|platanus|populus|ulmus)(?: |$)/.test(botanical)
|
|
2603
|
-
) return "tall";
|
|
2604
|
-
return ["round", "open", "tall"][Math.floor(stableUnit(key, 67) * 3)];
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
function treeVariantIndex(form, key) {
|
|
2608
|
-
const alternate = Math.floor(stableUnit(key, 101) * 2);
|
|
2609
|
-
const base = {
|
|
2610
|
-
round: 0,
|
|
2611
|
-
open: 2,
|
|
2612
|
-
tall: 4,
|
|
2613
|
-
conifer: 6,
|
|
2614
|
-
columnar: 7,
|
|
2615
|
-
palm: 8
|
|
2616
|
-
}[form] ?? 0;
|
|
2617
|
-
return base + (["round", "open", "tall", "palm"].includes(form) ? alternate : 0);
|
|
2618
|
-
}
|
|
2619
|
-
|
|
2620
|
-
function refresh() {
|
|
2621
|
-
const refreshStartedAt = performance.now();
|
|
2622
|
-
refreshQueued = false;
|
|
2623
|
-
lastRefreshTime = performance.now();
|
|
2624
|
-
if (
|
|
2625
|
-
activeTreeBackend === "three"
|
|
2626
|
-
? !treeGroup || treeMeshes.length === 0
|
|
2627
|
-
: rawTreeResources.length === 0
|
|
2628
|
-
) return;
|
|
2629
|
-
if (!treesEnabled) {
|
|
2630
|
-
if (treeGroup) treeGroup.visible = false;
|
|
2631
|
-
rawTreesVisible = false;
|
|
2632
|
-
setFallbackVisible(false);
|
|
2633
|
-
return;
|
|
2634
|
-
}
|
|
2635
|
-
if (map.getZoom() < 15.5) {
|
|
2636
|
-
for (const treeMesh of treeMeshes) treeMesh.count = 0;
|
|
2637
|
-
for (const resource of rawTreeResources) resource.count = 0;
|
|
2638
|
-
if (treeShadowMesh) treeShadowMesh.count = 0;
|
|
2639
|
-
if (rawShadowResource) rawShadowResource.count = 0;
|
|
2640
|
-
if (treeGroup) treeGroup.visible = false;
|
|
2641
|
-
rawTreesVisible = false;
|
|
2642
|
-
renderedTreeCount = 0;
|
|
2643
|
-
treeMetrics.treeCount = 0;
|
|
2644
|
-
treeMetrics.refreshMilliseconds = performance.now() - refreshStartedAt;
|
|
2645
|
-
lastTreeSelectionSignature = undefined;
|
|
2646
|
-
setFallbackVisible(true);
|
|
2647
|
-
updateTreeStatus("zoom too low", {phase: "refresh", treeCount: 0});
|
|
2648
|
-
map.triggerRepaint();
|
|
2649
|
-
return;
|
|
2650
|
-
}
|
|
2651
|
-
let features;
|
|
2652
|
-
const queryStartedAt = performance.now();
|
|
2653
|
-
try {
|
|
2654
|
-
const canvas = map.getCanvas();
|
|
2655
|
-
const viewportWidth = canvas.clientWidth || canvas.width;
|
|
2656
|
-
const viewportHeight = canvas.clientHeight || canvas.height;
|
|
2657
|
-
features = map.queryRenderedFeatures(
|
|
2658
|
-
[
|
|
2659
|
-
[-viewportBufferPixels, -viewportBufferPixels],
|
|
2660
|
-
[
|
|
2661
|
-
viewportWidth + viewportBufferPixels,
|
|
2662
|
-
viewportHeight + viewportBufferPixels
|
|
2663
|
-
]
|
|
2664
|
-
],
|
|
2665
|
-
{layers: [styleLayer.id]}
|
|
2666
|
-
);
|
|
2667
|
-
} catch (error) {
|
|
2668
|
-
updateTreeStatus("error reading source", {
|
|
2669
|
-
error: String(error),
|
|
2670
|
-
phase: "refresh",
|
|
2671
|
-
treeCount: 0
|
|
2672
|
-
});
|
|
2673
|
-
return;
|
|
2674
|
-
}
|
|
2675
|
-
treeMetrics.queryMilliseconds = performance.now() - queryStartedAt;
|
|
2676
|
-
sourceSettled = map.isSourceLoaded(sourceId);
|
|
2677
|
-
const lod = treeLodForZoom(map.getZoom());
|
|
2678
|
-
const selectionStartedAt = performance.now();
|
|
2679
|
-
const visibleTrees = selectVisibleTrees(features, lod);
|
|
2680
|
-
treeMetrics.selectionMilliseconds = performance.now() - selectionStartedAt;
|
|
2681
|
-
if (visibleTrees.length === 0) {
|
|
2682
|
-
updateFallbackVisibility();
|
|
2683
|
-
if (emptyRefreshAttempts < 12) {
|
|
2684
|
-
emptyRefreshAttempts += 1;
|
|
2685
|
-
clearTimeout(emptyRefreshTimer);
|
|
2686
|
-
emptyRefreshTimer = setTimeout(queueRefresh, 150);
|
|
2687
|
-
}
|
|
2688
|
-
updateTreeStatus("waiting for tiles", {
|
|
2689
|
-
lod: lod.name,
|
|
2690
|
-
phase: "refresh",
|
|
2691
|
-
sourceFeatureCount: features.length,
|
|
2692
|
-
sourceLoaded: sourceSettled,
|
|
2693
|
-
treeCount: renderedTreeCount
|
|
2694
|
-
});
|
|
2695
|
-
map.triggerRepaint();
|
|
2696
|
-
return;
|
|
2697
|
-
}
|
|
2698
|
-
const selectionSignature = treeSelectionSignature(visibleTrees, lod.name);
|
|
2699
|
-
if (sourceSettled && selectionSignature === lastTreeSelectionSignature) {
|
|
2700
|
-
if (treeGroup) treeGroup.visible = true;
|
|
2701
|
-
rawTreesVisible = true;
|
|
2702
|
-
treeMetrics.refreshMilliseconds = performance.now() - refreshStartedAt;
|
|
2703
|
-
updateFallbackVisibility();
|
|
2704
|
-
map.triggerRepaint();
|
|
2705
|
-
return;
|
|
2706
|
-
}
|
|
2707
|
-
// The viewport elevation is only a fallback. Each tree uses its own
|
|
2708
|
-
// terrain sample so sloped ground cannot clip different parts of the
|
|
2709
|
-
// crown as the camera center moves.
|
|
2710
|
-
const viewportElevation = map.queryTerrainElevation(map.getCenter()) ?? 0;
|
|
2711
|
-
ensureSceneOrigin(viewportElevation);
|
|
2712
|
-
const terrainStartedAt = performance.now();
|
|
2713
|
-
const terrainElevationAt = createTerrainSampler(visibleTrees, viewportElevation);
|
|
2714
|
-
treeMetrics.terrainMilliseconds = performance.now() - terrainStartedAt;
|
|
2715
|
-
const buildStartedAt = performance.now();
|
|
2716
|
-
let count = 0;
|
|
2717
|
-
const variantCount = activeTreeBackend === "three"
|
|
2718
|
-
? treeMeshes.length
|
|
2719
|
-
: rawTreeResources.length;
|
|
2720
|
-
const variantCounts = Array.from({length: variantCount}, () => 0);
|
|
2721
|
-
const rawInstanceValues = Array.from({length: variantCount}, () => []);
|
|
2722
|
-
const rawShadowInstanceValues = [];
|
|
2723
|
-
for (const {feature, key, lat, lng} of visibleTrees) {
|
|
2724
|
-
const treeState = resolveTreeState(
|
|
2725
|
-
feature,
|
|
2726
|
-
key,
|
|
2727
|
-
lng,
|
|
2728
|
-
lat,
|
|
2729
|
-
terrainElevationAt(lng, lat)
|
|
2730
|
-
);
|
|
2731
|
-
const {
|
|
2732
|
-
colorGain,
|
|
2733
|
-
crownAspect,
|
|
2734
|
-
crownDiameter,
|
|
2735
|
-
height,
|
|
2736
|
-
lat: treeLat,
|
|
2737
|
-
lng: treeLng,
|
|
2738
|
-
treeElevation,
|
|
2739
|
-
variantIndex
|
|
2740
|
-
} = treeState;
|
|
2741
|
-
const treePosition = localTreePosition(treeLng, treeLat, treeElevation);
|
|
2742
|
-
const rotation = stableUnit(key, 3) * Math.PI * 2;
|
|
2743
|
-
const crownWidth = crownDiameter * crownAspect;
|
|
2744
|
-
const crownDepth = crownDiameter / crownAspect;
|
|
2745
|
-
const shadowRadius = Math.max(0.75, Math.min(5.5, crownDiameter * 0.42));
|
|
2746
|
-
if (activeTreeBackend === "three") {
|
|
2747
|
-
ensureThreeTreeMeshCapacity(
|
|
2748
|
-
variantIndex,
|
|
2749
|
-
variantCounts[variantIndex] + 1
|
|
2750
|
-
);
|
|
2751
|
-
ensureThreeTreeShadowCapacity(count + 1);
|
|
2752
|
-
crownRotation.setFromAxisAngle(yAxis, rotation);
|
|
2753
|
-
position.copy(treePosition);
|
|
2754
|
-
scale.set(crownWidth, height, crownDepth);
|
|
2755
|
-
matrix.compose(position, crownRotation, scale);
|
|
2756
|
-
const treeMesh = treeMeshes[variantIndex];
|
|
2757
|
-
treeMesh.setMatrixAt(variantCounts[variantIndex], matrix);
|
|
2758
|
-
instanceTint.setRGB(colorGain, colorGain, colorGain);
|
|
2759
|
-
treeMesh.setColorAt(variantCounts[variantIndex], instanceTint);
|
|
2760
|
-
shadowPosition.copy(treePosition);
|
|
2761
|
-
shadowPosition.x -= shadowRadius * 0.28;
|
|
2762
|
-
shadowPosition.y += treeShadowLiftMeters;
|
|
2763
|
-
shadowPosition.z += shadowRadius * 0.34;
|
|
2764
|
-
shadowScale.set(shadowRadius, 1, shadowRadius * 0.76);
|
|
2765
|
-
shadowMatrix.compose(shadowPosition, shadowRotation, shadowScale);
|
|
2766
|
-
treeShadowMesh.setMatrixAt(count, shadowMatrix);
|
|
2767
|
-
} else {
|
|
2768
|
-
rawInstanceValues[variantIndex].push(
|
|
2769
|
-
treePosition.x,
|
|
2770
|
-
treePosition.y,
|
|
2771
|
-
treePosition.z,
|
|
2772
|
-
crownWidth,
|
|
2773
|
-
height,
|
|
2774
|
-
crownDepth,
|
|
2775
|
-
Math.cos(rotation),
|
|
2776
|
-
Math.sin(rotation),
|
|
2777
|
-
colorGain
|
|
2778
|
-
);
|
|
2779
|
-
rawShadowInstanceValues.push(
|
|
2780
|
-
treePosition.x,
|
|
2781
|
-
treePosition.y,
|
|
2782
|
-
treePosition.z,
|
|
2783
|
-
shadowRadius
|
|
2784
|
-
);
|
|
2785
|
-
}
|
|
2786
|
-
variantCounts[variantIndex] += 1;
|
|
2787
|
-
count += 1;
|
|
2788
|
-
}
|
|
2789
|
-
if (activeTreeBackend === "three") {
|
|
2790
|
-
for (let variantIndex = 0; variantIndex < treeMeshes.length; variantIndex += 1) {
|
|
2791
|
-
const treeMesh = treeMeshes[variantIndex];
|
|
2792
|
-
treeMesh.count = variantCounts[variantIndex];
|
|
2793
|
-
treeMesh.instanceMatrix.needsUpdate = true;
|
|
2794
|
-
if (treeMesh.instanceColor) treeMesh.instanceColor.needsUpdate = true;
|
|
2795
|
-
}
|
|
2796
|
-
treeShadowMesh.count = count;
|
|
2797
|
-
treeShadowMesh.instanceMatrix.needsUpdate = true;
|
|
2798
|
-
} else {
|
|
2799
|
-
for (let variantIndex = 0; variantIndex < rawTreeResources.length; variantIndex += 1) {
|
|
2800
|
-
const resource = rawTreeResources[variantIndex];
|
|
2801
|
-
resource.count = variantCounts[variantIndex];
|
|
2802
|
-
rawGl.bindBuffer(rawGl.ARRAY_BUFFER, resource.instanceBuffer);
|
|
2803
|
-
rawGl.bufferData(
|
|
2804
|
-
rawGl.ARRAY_BUFFER,
|
|
2805
|
-
new Float32Array(rawInstanceValues[variantIndex]),
|
|
2806
|
-
rawGl.DYNAMIC_DRAW
|
|
2807
|
-
);
|
|
2808
|
-
}
|
|
2809
|
-
rawShadowResource.count = count;
|
|
2810
|
-
rawGl.bindBuffer(rawGl.ARRAY_BUFFER, rawShadowResource.instanceBuffer);
|
|
2811
|
-
rawGl.bufferData(
|
|
2812
|
-
rawGl.ARRAY_BUFFER,
|
|
2813
|
-
new Float32Array(rawShadowInstanceValues),
|
|
2814
|
-
rawGl.DYNAMIC_DRAW
|
|
2815
|
-
);
|
|
2816
|
-
rawGl.bindBuffer(rawGl.ARRAY_BUFFER, null);
|
|
2817
|
-
}
|
|
2818
|
-
treeMetrics.buildMilliseconds = performance.now() - buildStartedAt;
|
|
2819
|
-
if (treeGroup) treeGroup.visible = true;
|
|
2820
|
-
rawTreesVisible = true;
|
|
2821
|
-
renderedTreeCount = count;
|
|
2822
|
-
treeMetrics.treeCount = count;
|
|
2823
|
-
treeMetrics.refreshMilliseconds = performance.now() - refreshStartedAt;
|
|
2824
|
-
lastTreeSelectionSignature = selectionSignature;
|
|
2825
|
-
emptyRefreshAttempts = 0;
|
|
2826
|
-
clearTimeout(emptyRefreshTimer);
|
|
2827
|
-
emptyRefreshTimer = undefined;
|
|
2828
|
-
updateTreeStatus(count + " 3D trees", {
|
|
2829
|
-
lod: lod.name,
|
|
2830
|
-
phase: "refresh",
|
|
2831
|
-
renderer: treeRendererMode,
|
|
2832
|
-
sourceFeatureCount: features.length,
|
|
2833
|
-
sourceLoaded: sourceSettled,
|
|
2834
|
-
treeCount: count
|
|
2835
|
-
});
|
|
2836
|
-
updateFallbackVisibility();
|
|
2837
|
-
if (!sourceSettled) {
|
|
2838
|
-
clearTimeout(emptyRefreshTimer);
|
|
2839
|
-
emptyRefreshTimer = setTimeout(() => queueRefresh(true), 150);
|
|
2840
|
-
}
|
|
2841
|
-
map.triggerRepaint();
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
function queueRefresh(immediate = false) {
|
|
2845
|
-
if (refreshQueued) return;
|
|
2846
|
-
if (immediate && refreshTimer !== undefined) {
|
|
2847
|
-
clearTimeout(refreshTimer);
|
|
2848
|
-
refreshTimer = undefined;
|
|
2849
|
-
}
|
|
2850
|
-
if (!immediate) {
|
|
2851
|
-
if (refreshTimer !== undefined) return;
|
|
2852
|
-
const elapsed = performance.now() - lastRefreshTime;
|
|
2853
|
-
const delay = Math.max(0, sourceRefreshIntervalMilliseconds - elapsed);
|
|
2854
|
-
if (delay > 0) {
|
|
2855
|
-
refreshTimer = setTimeout(() => {
|
|
2856
|
-
refreshTimer = undefined;
|
|
2857
|
-
queueRefresh(true);
|
|
2858
|
-
}, delay);
|
|
2859
|
-
return;
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
refreshQueued = true;
|
|
2863
|
-
requestAnimationFrame(refresh);
|
|
2864
|
-
}
|
|
2865
|
-
|
|
2866
|
-
function handleMoveStart() {
|
|
2867
|
-
if (!treesEnabled) return;
|
|
2868
|
-
sourceSettled = false;
|
|
2869
|
-
updateFallbackVisibility();
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
function handleMoveEnd() {
|
|
2873
|
-
if (!treesEnabled) return;
|
|
2874
|
-
emptyRefreshAttempts = 0;
|
|
2875
|
-
sourceSettled = map.isSourceLoaded(sourceId);
|
|
2876
|
-
updateFallbackVisibility();
|
|
2877
|
-
queueRefresh(true);
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
function handleSourceLoading(event) {
|
|
2881
|
-
if (!treesEnabled || event.sourceId !== sourceId) return;
|
|
2882
|
-
sourceSettled = false;
|
|
2883
|
-
updateFallbackVisibility();
|
|
2884
|
-
}
|
|
2885
|
-
|
|
2886
|
-
function handleSourceData(event) {
|
|
2887
|
-
if (!treesEnabled || event.sourceId !== sourceId) return;
|
|
2888
|
-
sourceSettled = map.isSourceLoaded(sourceId);
|
|
2889
|
-
if (map.isMoving()) return;
|
|
2890
|
-
queueRefresh();
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
function handleTreesToggle() {
|
|
2894
|
-
clearTimeout(emptyRefreshTimer);
|
|
2895
|
-
clearTimeout(refreshTimer);
|
|
2896
|
-
emptyRefreshTimer = undefined;
|
|
2897
|
-
refreshTimer = undefined;
|
|
2898
|
-
if (!treesEnabled) {
|
|
2899
|
-
if (treeGroup) treeGroup.visible = false;
|
|
2900
|
-
rawTreesVisible = false;
|
|
2901
|
-
setFallbackVisible(false);
|
|
2902
|
-
updateTreeStatus("disabled", {phase: "toggle", treeCount: renderedTreeCount});
|
|
2903
|
-
map.triggerRepaint();
|
|
2904
|
-
return;
|
|
2905
|
-
}
|
|
2906
|
-
if (treeGroup) treeGroup.visible = renderedTreeCount > 0;
|
|
2907
|
-
rawTreesVisible = renderedTreeCount > 0;
|
|
2908
|
-
updateFallbackVisibility();
|
|
2909
|
-
updateTreeStatus(
|
|
2910
|
-
renderedTreeCount > 0 ? renderedTreeCount + " 3D trees" : "enabling",
|
|
2911
|
-
{phase: "toggle", treeCount: renderedTreeCount}
|
|
2912
|
-
);
|
|
2913
|
-
queueRefresh(true);
|
|
2914
|
-
map.triggerRepaint();
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
return {
|
|
2918
|
-
id: "tileflow-vegetation-trees-3d",
|
|
2919
|
-
type: "custom",
|
|
2920
|
-
renderingMode: "3d",
|
|
2921
|
-
onAdd(_map, gl) {
|
|
2922
|
-
updateTreeStatus("layer mounted", {phase: "onAdd", treeCount: 0});
|
|
2923
|
-
const geometries = createTreeGeometries();
|
|
2924
|
-
if (
|
|
2925
|
-
activeTreeBackend === "webgl2" &&
|
|
2926
|
-
typeof gl.createVertexArray === "function" &&
|
|
2927
|
-
typeof gl.drawArraysInstanced === "function"
|
|
2928
|
-
) {
|
|
2929
|
-
rawGl = gl;
|
|
2930
|
-
rawProgram = createRawTreeProgram(gl);
|
|
2931
|
-
rawMatrixUniform = gl.getUniformLocation(rawProgram, "u_matrix");
|
|
2932
|
-
rawShadowProgram = createRawTreeShadowProgram(gl);
|
|
2933
|
-
rawShadowMatrixUniform = gl.getUniformLocation(rawShadowProgram, "u_matrix");
|
|
2934
|
-
rawShadowOpacityUniform = gl.getUniformLocation(rawShadowProgram, "u_opacity");
|
|
2935
|
-
rawShadowResource = createRawTreeShadowResource(gl);
|
|
2936
|
-
rawTreeResources = geometries.map((geometry) =>
|
|
2937
|
-
createRawTreeResource(gl, geometry)
|
|
2938
|
-
);
|
|
2939
|
-
for (const geometry of geometries) geometry.dispose();
|
|
2940
|
-
} else {
|
|
2941
|
-
activeTreeBackend = "three";
|
|
2942
|
-
treeMetrics.backend = activeTreeBackend;
|
|
2943
|
-
treeGeometries = geometries;
|
|
2944
|
-
camera = new THREE.Camera();
|
|
2945
|
-
scene = new THREE.Scene();
|
|
2946
|
-
scene.rotateX(Math.PI / 2);
|
|
2947
|
-
scene.scale.multiply(new THREE.Vector3(1, 1, -1));
|
|
2948
|
-
scene.add(new THREE.HemisphereLight(0xf4fff1, 0x808878, 1.35));
|
|
2949
|
-
const sunlight = new THREE.DirectionalLight(0xfff3d8, 0.88);
|
|
2950
|
-
sunlight.position.set(-2, -3, 6);
|
|
2951
|
-
scene.add(sunlight);
|
|
2952
|
-
treeGroup = new THREE.Group();
|
|
2953
|
-
treeMaterial = new THREE.MeshLambertMaterial({
|
|
2954
|
-
flatShading: false,
|
|
2955
|
-
vertexColors: true
|
|
2956
|
-
});
|
|
2957
|
-
treeMeshes = geometries.map((geometry) =>
|
|
2958
|
-
createThreeTreeMesh(geometry, initialTreeInstanceCapacity)
|
|
2959
|
-
);
|
|
2960
|
-
treeMeshCapacities = geometries.map(() => initialTreeInstanceCapacity);
|
|
2961
|
-
treeShadowGeometry = new THREE.PlaneGeometry(2, 2);
|
|
2962
|
-
treeShadowGeometry.rotateX(-Math.PI / 2);
|
|
2963
|
-
treeShadowMaterial = new THREE.ShaderMaterial({
|
|
2964
|
-
depthTest: true,
|
|
2965
|
-
depthWrite: false,
|
|
2966
|
-
fragmentShader:
|
|
2967
|
-
"varying vec2 v_shadow_coordinate;\\n" +
|
|
2968
|
-
"uniform float u_opacity;\\n" +
|
|
2969
|
-
"void main() {\\n" +
|
|
2970
|
-
" float radiusSquared = dot(v_shadow_coordinate, v_shadow_coordinate);\\n" +
|
|
2971
|
-
" if (radiusSquared >= 1.0) discard;\\n" +
|
|
2972
|
-
" float coverage = 1.0 - smoothstep(0.90, 1.0, radiusSquared);\\n" +
|
|
2973
|
-
" gl_FragColor = vec4(0.18, 0.21, 0.17, u_opacity * coverage);\\n" +
|
|
2974
|
-
"}\\n",
|
|
2975
|
-
side: THREE.DoubleSide,
|
|
2976
|
-
toneMapped: false,
|
|
2977
|
-
transparent: true,
|
|
2978
|
-
uniforms: {u_opacity: {value: treeShadowOpacity}},
|
|
2979
|
-
vertexShader:
|
|
2980
|
-
"varying vec2 v_shadow_coordinate;\\n" +
|
|
2981
|
-
"void main() {\\n" +
|
|
2982
|
-
" v_shadow_coordinate = uv * 2.0 - 1.0;\\n" +
|
|
2983
|
-
" vec4 instancePosition = instanceMatrix * vec4(position, 1.0);\\n" +
|
|
2984
|
-
" gl_Position = projectionMatrix * modelViewMatrix * instancePosition;\\n" +
|
|
2985
|
-
"}\\n"
|
|
2986
|
-
});
|
|
2987
|
-
treeShadowMesh = new THREE.InstancedMesh(
|
|
2988
|
-
treeShadowGeometry,
|
|
2989
|
-
treeShadowMaterial,
|
|
2990
|
-
initialTreeInstanceCapacity
|
|
2991
|
-
);
|
|
2992
|
-
treeShadowCapacity = initialTreeInstanceCapacity;
|
|
2993
|
-
treeShadowMesh.count = 0;
|
|
2994
|
-
treeShadowMesh.frustumCulled = false;
|
|
2995
|
-
treeShadowMesh.renderOrder = 1;
|
|
2996
|
-
treeGroup.visible = false;
|
|
2997
|
-
treeGroup.add(...treeMeshes, treeShadowMesh);
|
|
2998
|
-
scene.add(treeGroup);
|
|
2999
|
-
renderer = new THREE.WebGLRenderer({
|
|
3000
|
-
canvas: map.getCanvas(),
|
|
3001
|
-
context: gl
|
|
3002
|
-
});
|
|
3003
|
-
renderer.autoClear = false;
|
|
3004
|
-
}
|
|
3005
|
-
sourceSettled = map.isSourceLoaded(sourceId);
|
|
3006
|
-
map.on("movestart", handleMoveStart);
|
|
3007
|
-
map.on("moveend", handleMoveEnd);
|
|
3008
|
-
map.on("sourcedataloading", handleSourceLoading);
|
|
3009
|
-
map.on("sourcedata", handleSourceData);
|
|
3010
|
-
map.on("tileflow:trees-toggle", handleTreesToggle);
|
|
3011
|
-
if (treesEnabled) queueRefresh(true);
|
|
3012
|
-
else handleTreesToggle();
|
|
3013
|
-
},
|
|
3014
|
-
render(_gl, options) {
|
|
3015
|
-
if (
|
|
3016
|
-
!treesEnabled ||
|
|
3017
|
-
!sceneOriginMercator ||
|
|
3018
|
-
(activeTreeBackend === "three" ? !treeGroup?.visible : !rawTreesVisible)
|
|
3019
|
-
) return;
|
|
3020
|
-
const renderStartedAt = performance.now();
|
|
3021
|
-
const meter = sceneOriginMercator.meterInMercatorCoordinateUnits();
|
|
3022
|
-
mapMatrix.fromArray(options.defaultProjectionData.mainMatrix);
|
|
3023
|
-
sceneMatrix
|
|
3024
|
-
.makeTranslation(
|
|
3025
|
-
sceneOriginMercator.x,
|
|
3026
|
-
sceneOriginMercator.y,
|
|
3027
|
-
sceneOriginMercator.z
|
|
3028
|
-
)
|
|
3029
|
-
.scale(new THREE.Vector3(meter, -meter, meter));
|
|
3030
|
-
combinedMatrix.copy(mapMatrix).multiply(sceneMatrix);
|
|
3031
|
-
if (activeTreeBackend === "three") {
|
|
3032
|
-
camera.projectionMatrix.copy(combinedMatrix);
|
|
3033
|
-
renderer.resetState();
|
|
3034
|
-
renderer.render(scene, camera);
|
|
3035
|
-
treeMetrics.renderCalls = renderer.info.render.calls;
|
|
3036
|
-
treeMetrics.renderedTriangles = renderer.info.render.triangles;
|
|
3037
|
-
} else {
|
|
3038
|
-
rawGl.useProgram(rawProgram);
|
|
3039
|
-
rawGl.uniformMatrix4fv(rawMatrixUniform, false, combinedMatrix.elements);
|
|
3040
|
-
rawGl.enable(rawGl.DEPTH_TEST);
|
|
3041
|
-
rawGl.depthFunc(rawGl.LEQUAL);
|
|
3042
|
-
rawGl.depthMask(true);
|
|
3043
|
-
rawGl.enable(rawGl.CULL_FACE);
|
|
3044
|
-
rawGl.cullFace(rawGl.BACK);
|
|
3045
|
-
rawGl.frontFace(rawGl.CCW);
|
|
3046
|
-
rawGl.disable(rawGl.BLEND);
|
|
3047
|
-
let renderCalls = 0;
|
|
3048
|
-
let renderedTriangles = 0;
|
|
3049
|
-
for (const resource of rawTreeResources) {
|
|
3050
|
-
if (resource.count === 0) continue;
|
|
3051
|
-
rawGl.bindVertexArray(resource.vertexArray);
|
|
3052
|
-
rawGl.drawElementsInstanced(
|
|
3053
|
-
rawGl.TRIANGLES,
|
|
3054
|
-
resource.indexCount,
|
|
3055
|
-
rawGl.UNSIGNED_INT,
|
|
3056
|
-
0,
|
|
3057
|
-
resource.count
|
|
3058
|
-
);
|
|
3059
|
-
renderCalls += 1;
|
|
3060
|
-
renderedTriangles += (resource.indexCount / 3) * resource.count;
|
|
3061
|
-
}
|
|
3062
|
-
rawGl.bindVertexArray(null);
|
|
3063
|
-
if (rawShadowResource.count > 0) {
|
|
3064
|
-
rawGl.useProgram(rawShadowProgram);
|
|
3065
|
-
rawGl.uniformMatrix4fv(
|
|
3066
|
-
rawShadowMatrixUniform,
|
|
3067
|
-
false,
|
|
3068
|
-
combinedMatrix.elements
|
|
3069
|
-
);
|
|
3070
|
-
rawGl.uniform1f(rawShadowOpacityUniform, treeShadowOpacity);
|
|
3071
|
-
rawGl.depthMask(false);
|
|
3072
|
-
rawGl.disable(rawGl.CULL_FACE);
|
|
3073
|
-
rawGl.enable(rawGl.BLEND);
|
|
3074
|
-
rawGl.blendEquation(rawGl.FUNC_ADD);
|
|
3075
|
-
rawGl.blendFuncSeparate(
|
|
3076
|
-
rawGl.SRC_ALPHA,
|
|
3077
|
-
rawGl.ONE_MINUS_SRC_ALPHA,
|
|
3078
|
-
rawGl.ONE,
|
|
3079
|
-
rawGl.ONE_MINUS_SRC_ALPHA
|
|
3080
|
-
);
|
|
3081
|
-
rawGl.bindVertexArray(rawShadowResource.vertexArray);
|
|
3082
|
-
rawGl.drawArraysInstanced(
|
|
3083
|
-
rawGl.TRIANGLE_FAN,
|
|
3084
|
-
0,
|
|
3085
|
-
4,
|
|
3086
|
-
rawShadowResource.count
|
|
3087
|
-
);
|
|
3088
|
-
rawGl.bindVertexArray(null);
|
|
3089
|
-
rawGl.depthMask(true);
|
|
3090
|
-
rawGl.disable(rawGl.BLEND);
|
|
3091
|
-
rawGl.enable(rawGl.CULL_FACE);
|
|
3092
|
-
renderCalls += 1;
|
|
3093
|
-
renderedTriangles += rawShadowResource.count * 2;
|
|
3094
|
-
}
|
|
3095
|
-
treeMetrics.renderCalls = renderCalls;
|
|
3096
|
-
treeMetrics.renderedTriangles = renderedTriangles;
|
|
3097
|
-
}
|
|
3098
|
-
treeMetrics.renderMilliseconds = performance.now() - renderStartedAt;
|
|
3099
|
-
},
|
|
3100
|
-
onRemove() {
|
|
3101
|
-
clearTimeout(emptyRefreshTimer);
|
|
3102
|
-
clearTimeout(refreshTimer);
|
|
3103
|
-
map.off("movestart", handleMoveStart);
|
|
3104
|
-
map.off("moveend", handleMoveEnd);
|
|
3105
|
-
map.off("sourcedataloading", handleSourceLoading);
|
|
3106
|
-
map.off("sourcedata", handleSourceData);
|
|
3107
|
-
map.off("tileflow:trees-toggle", handleTreesToggle);
|
|
3108
|
-
if (!fallbackVisible && map.getLayer(styleLayer.id)) {
|
|
3109
|
-
map.setPaintProperty(
|
|
3110
|
-
styleLayer.id,
|
|
3111
|
-
"circle-opacity",
|
|
3112
|
-
fallbackCircleOpacity
|
|
3113
|
-
);
|
|
3114
|
-
map.setPaintProperty(
|
|
3115
|
-
styleLayer.id,
|
|
3116
|
-
"circle-stroke-opacity",
|
|
3117
|
-
fallbackCircleStrokeOpacity
|
|
3118
|
-
);
|
|
3119
|
-
}
|
|
3120
|
-
for (const treeMesh of treeMeshes) {
|
|
3121
|
-
treeMesh.dispose();
|
|
3122
|
-
treeMesh.geometry.dispose();
|
|
3123
|
-
}
|
|
3124
|
-
treeMeshes = [];
|
|
3125
|
-
treeMeshCapacities = [];
|
|
3126
|
-
treeGeometries = [];
|
|
3127
|
-
treeShadowMesh?.dispose();
|
|
3128
|
-
treeShadowMesh?.geometry.dispose();
|
|
3129
|
-
treeShadowMesh = undefined;
|
|
3130
|
-
treeShadowGeometry = undefined;
|
|
3131
|
-
treeShadowCapacity = 0;
|
|
3132
|
-
treeShadowMaterial?.dispose();
|
|
3133
|
-
treeShadowMaterial = undefined;
|
|
3134
|
-
treeMaterial?.dispose();
|
|
3135
|
-
renderer?.dispose();
|
|
3136
|
-
disposeRawTreeResources();
|
|
3137
|
-
}
|
|
3138
|
-
};
|
|
3139
|
-
}
|
|
3140
|
-
|
|
3141
|
-
function addTreeLayerIfConfigured(map, styleLayers) {
|
|
3142
|
-
const styleLayer = styleLayers.find(
|
|
3143
|
-
(layer) => layer.id === "tileflow-vegetation-trees"
|
|
3144
|
-
);
|
|
3145
|
-
if (
|
|
3146
|
-
!styleLayer ||
|
|
3147
|
-
styleLayer.metadata?.["tileflow:vegetation-mode"] !== "3d" ||
|
|
3148
|
-
map.getLayer("tileflow-vegetation-trees-3d")
|
|
3149
|
-
) return;
|
|
3150
|
-
if (treeRendererMode === "circle") {
|
|
3151
|
-
treeMetrics.treeCount = 0;
|
|
3152
|
-
updateTreeStatus("native circles", {
|
|
3153
|
-
phase: "configured",
|
|
3154
|
-
renderer: treeRendererMode,
|
|
3155
|
-
treeCount: 0
|
|
3156
|
-
});
|
|
3157
|
-
return;
|
|
3158
|
-
}
|
|
3159
|
-
updateTreeStatus("configured", {phase: "configured", treeCount: 0});
|
|
3160
|
-
// Three.js consumes Mercator world coordinates directly. MapLibre's
|
|
3161
|
-
// plain custom-layer matrix only supports those coordinates in the
|
|
3162
|
-
// Mercator projection; globe requires a projection-aware shader.
|
|
3163
|
-
if (map.getProjection().type !== "mercator") {
|
|
3164
|
-
map.setProjection({type: "mercator"});
|
|
3165
|
-
}
|
|
3166
|
-
if (map.getLayer("tileflow-vegetation-trees-3d")) return;
|
|
3167
|
-
const styleLayerIndex = styleLayers.findIndex((layer) => layer.id === styleLayer.id);
|
|
3168
|
-
const layerAboveTrees = styleLayers[styleLayerIndex + 1]?.id;
|
|
3169
|
-
map.addLayer(createTreeLayer(map, styleLayer), layerAboveTrees);
|
|
3170
|
-
}
|
|
3171
|
-
|
|
3172
1468
|
function styleLayerZoomRange(layer) {
|
|
3173
1469
|
return {
|
|
3174
1470
|
minzoom: Number.isFinite(layer?.minzoom) ? layer.minzoom : 0,
|
|
@@ -3668,8 +1964,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3668
1964
|
spatialFallbacks:
|
|
3669
1965
|
layer.metadata?.["tileflow:landmark-spatial-fallbacks"] ?? []
|
|
3670
1966
|
}));
|
|
3671
|
-
// Landmark custom layers use Mercator world coordinates.
|
|
3672
|
-
// instead of relying on the optional detailed-tree renderer to do it.
|
|
1967
|
+
// Landmark custom layers use Mercator world coordinates.
|
|
3673
1968
|
if (map.getProjection().type !== "mercator") {
|
|
3674
1969
|
map.setProjection({type: "mercator"});
|
|
3675
1970
|
}
|
|
@@ -3677,16 +1972,94 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3677
1972
|
(layer) =>
|
|
3678
1973
|
(layer.id.startsWith("tileflow-label-") &&
|
|
3679
1974
|
!layer.id.startsWith("tileflow-label-road-")) ||
|
|
1975
|
+
layer.id === "tileflow-poi" ||
|
|
3680
1976
|
layer.id.startsWith("tileflow-poi-")
|
|
3681
1977
|
)?.id;
|
|
3682
|
-
const treeLayer = map.getLayer("tileflow-vegetation-trees
|
|
3683
|
-
? "tileflow-vegetation-trees
|
|
3684
|
-
:
|
|
3685
|
-
? "tileflow-vegetation-trees"
|
|
3686
|
-
: firstForegroundLabel;
|
|
1978
|
+
const treeLayer = map.getLayer("tileflow-vegetation-trees")
|
|
1979
|
+
? "tileflow-vegetation-trees"
|
|
1980
|
+
: firstForegroundLabel;
|
|
3687
1981
|
map.addLayer(createLandmarkLayer(map, configLayer, fallbackLayers), treeLayer);
|
|
3688
1982
|
}
|
|
3689
1983
|
|
|
1984
|
+
// Core's 3D layers, as an application mounts them: the map's style describes how its
|
|
1985
|
+
// buildings, trees and street lights look, and the controls only show and hide them. The
|
|
1986
|
+
// vegetation control hides all trees when off. The preview shows one theme at a
|
|
1987
|
+
// time, so the style on screen holds the sun and the design.
|
|
1988
|
+
const coreScene = {buildings: undefined, trees: undefined, lights: undefined, buildingsShown: false, animation: "0"};
|
|
1989
|
+
const runtimeLayerVisibility = {};
|
|
1990
|
+
globalThis.__tileflowPreviewScene = coreScene;
|
|
1991
|
+
globalThis.__tileflowRuntimeVisibility = (id, visible) => {
|
|
1992
|
+
if (!['tileflow-buildings-3d','tileflow-trees-3d','tileflow-street-lights'].includes(id)) return undefined;
|
|
1993
|
+
if (typeof visible === 'boolean') { runtimeLayerVisibility[id]=visible; mountCoreScene(map); map.triggerRepaint(); }
|
|
1994
|
+
return runtimeLayerVisibility[id] !== false;
|
|
1995
|
+
};
|
|
1996
|
+
// Explicit observation capability: controllers stay private to the preview and expose
|
|
1997
|
+
// only their public state/statistics to the parent inspector.
|
|
1998
|
+
globalThis.__tileflowInspectModels = () => [...(coreScene.buildings?.inspectModels?.() || []), ...(coreScene.trees?.inspectModels?.() || [])];
|
|
1999
|
+
globalThis.__tileflowDescribeRuntimeLayers = () => ({
|
|
2000
|
+
...(coreScene.buildings ? {"tileflow-buildings-3d": {controller:"Buildings 3D", state:{visible:runtimeLayerVisibility["tileflow-buildings-3d"] !== false, shown:coreScene.buildingsShown, scale:coreScene.buildings.scale}, stats:coreScene.buildings.stats}} : {}),
|
|
2001
|
+
...(coreScene.trees ? {"tileflow-trees-3d": {controller:"Vegetation", state:{visible:runtimeLayerVisibility["tileflow-trees-3d"] !== false, representation:coreScene.trees.representation}, stats:coreScene.trees.stats}} : {}),
|
|
2002
|
+
...(coreScene.lights ? {"tileflow-street-lights": {controller:"Street lights", state:{visible:runtimeLayerVisibility["tileflow-street-lights"] !== false, mounted:true}, stats:coreScene.lights.stats}} : {})
|
|
2003
|
+
});
|
|
2004
|
+
function coreSceneThemes(map) {
|
|
2005
|
+
return {
|
|
2006
|
+
getSun() {
|
|
2007
|
+
const preset = readTileflowSunPreset(map.getStyle()?.metadata);
|
|
2008
|
+
return resolveTileflowSun(preset ?? tileflowDefaultSunPreset);
|
|
2009
|
+
},
|
|
2010
|
+
getThemeMetadata(key) {
|
|
2011
|
+
return {values: [map.getStyle()?.metadata?.[key]], position: 0};
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
let mountingCoreScene = false;
|
|
2016
|
+
function mountCoreScene(map) {
|
|
2017
|
+
if (mountingCoreScene) return;
|
|
2018
|
+
mountingCoreScene = true;
|
|
2019
|
+
try {
|
|
2020
|
+
const metadata = map.getStyle()?.metadata ?? {};
|
|
2021
|
+
const controller = coreSceneThemes(map);
|
|
2022
|
+
if (!coreScene.buildings && metadata["tileflow:buildings"]) {
|
|
2023
|
+
coreScene.buildings = createTileflowBuildings3D({map, controller});
|
|
2024
|
+
}
|
|
2025
|
+
if (!coreScene.trees) {
|
|
2026
|
+
if (!previewSearchParameters.has("trees3d")) {
|
|
2027
|
+
treesEnabled = metadata["tileflow:vegetation-display"]?.visible ?? true;
|
|
2028
|
+
}
|
|
2029
|
+
coreScene.trees = createTileflowVegetation({map, controller, visible: treesEnabled,
|
|
2030
|
+
viewMode: threeDimensionalEnabled ? "3d" : "2d", transformRequest: previewTransformRequest});
|
|
2031
|
+
}
|
|
2032
|
+
coreScene.trees.setVisible(treesEnabled && runtimeLayerVisibility["tileflow-trees-3d"] !== false);
|
|
2033
|
+
coreScene.trees.setViewMode(threeDimensionalEnabled ? "3d" : "2d");
|
|
2034
|
+
// Street lights have no capture fallback, so they shine only with the 3D control on.
|
|
2035
|
+
if (!coreScene.lights && threeDimensionalEnabled && metadata["tileflow:street-lights"]) {
|
|
2036
|
+
coreScene.lights = createTileflowStreetLights({map, controller});
|
|
2037
|
+
} else if (coreScene.lights && !threeDimensionalEnabled) {
|
|
2038
|
+
coreScene.lights.dispose();
|
|
2039
|
+
coreScene.lights = undefined;
|
|
2040
|
+
}
|
|
2041
|
+
const lightLayer = map.getLayer("tileflow-street-lights");
|
|
2042
|
+
const lightRenderer = lightLayer?.implementation || lightLayer;
|
|
2043
|
+
if (lightRenderer?.render && !lightRenderer.__previewVisibility) {
|
|
2044
|
+
const render = lightRenderer.render;
|
|
2045
|
+
lightRenderer.render = function(...args) {
|
|
2046
|
+
if (runtimeLayerVisibility["tileflow-street-lights"] !== false) return render.apply(this,args);
|
|
2047
|
+
};
|
|
2048
|
+
lightRenderer.__previewVisibility = true;
|
|
2049
|
+
}
|
|
2050
|
+
const showBuildings = threeDimensionalEnabled && runtimeLayerVisibility["tileflow-buildings-3d"] !== false;
|
|
2051
|
+
const instant = {transition: {duration: 0}};
|
|
2052
|
+
const animation = previewSearchParameters.get('animation') || '0';
|
|
2053
|
+
const duration = animation === 'fade' ? 800 : Math.min(3000, Math.max(0, Number(animation) || 0));
|
|
2054
|
+
// Only a change of the controls moves the layers, so a style event does not restart them.
|
|
2055
|
+
if (coreScene.buildings && (coreScene.buildingsShown !== showBuildings || coreScene.animation !== animation)) {
|
|
2056
|
+
coreScene.buildingsShown = showBuildings;
|
|
2057
|
+
coreScene.animation = animation;
|
|
2058
|
+
void (showBuildings ? coreScene.buildings.show({transition: {duration, mode: animation === "fade" ? "fade" : "grow"}}) : coreScene.buildings.hide(instant));
|
|
2059
|
+
}
|
|
2060
|
+
} finally {mountingCoreScene = false;}
|
|
2061
|
+
}
|
|
2062
|
+
|
|
3690
2063
|
class ThreeDimensionalControl {
|
|
3691
2064
|
constructor() {
|
|
3692
2065
|
this.enabled = threeDimensionalEnabled;
|
|
@@ -3722,6 +2095,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3722
2095
|
this.update();
|
|
3723
2096
|
writeToggleStateToUrl();
|
|
3724
2097
|
this.map.fire("tileflow:3d-toggle", {enabled: this.enabled});
|
|
2098
|
+
mountCoreScene(this.map);
|
|
3725
2099
|
this.map.triggerRepaint();
|
|
3726
2100
|
}
|
|
3727
2101
|
|
|
@@ -3729,11 +2103,14 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3729
2103
|
for (const layer of this.map.getStyle()?.layers || []) {
|
|
3730
2104
|
const toggle = layer.metadata?.["tileflow:3d-toggle"];
|
|
3731
2105
|
if (toggle !== "building" && toggle !== "landmark") continue;
|
|
3732
|
-
|
|
2106
|
+
// Core's 3D buildings stand in for the compiled extrusion where the style describes them.
|
|
2107
|
+
const replaced = toggle === "building" && this.map.getStyle()?.metadata?.["tileflow:buildings"];
|
|
2108
|
+
const visibility = this.enabled && !replaced ? "visible" : "none";
|
|
3733
2109
|
if (this.map.getLayoutProperty(layer.id, "visibility") !== visibility) {
|
|
3734
2110
|
this.map.setLayoutProperty(layer.id, "visibility", visibility);
|
|
3735
2111
|
}
|
|
3736
2112
|
}
|
|
2113
|
+
if (this.map.isStyleLoaded()) mountCoreScene(this.map);
|
|
3737
2114
|
this.button.textContent = this.enabled ? "3D ON" : "3D OFF";
|
|
3738
2115
|
this.button.setAttribute("aria-pressed", String(this.enabled));
|
|
3739
2116
|
this.button.title = this.enabled ? "Disable 3D buildings" : "Enable 3D buildings";
|
|
@@ -3780,22 +2157,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3780
2157
|
this.update();
|
|
3781
2158
|
writeToggleStateToUrl();
|
|
3782
2159
|
this.map.fire("tileflow:trees-toggle", {enabled: this.enabled});
|
|
2160
|
+
mountCoreScene(this.map);
|
|
3783
2161
|
this.map.triggerRepaint();
|
|
3784
2162
|
}
|
|
3785
2163
|
|
|
3786
2164
|
update() {
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
);
|
|
3790
|
-
if (treeLayer) {
|
|
3791
|
-
const visibility = this.enabled ? "visible" : "none";
|
|
3792
|
-
if (this.map.getLayoutProperty(treeLayer.id, "visibility") !== visibility) {
|
|
3793
|
-
this.map.setLayoutProperty(treeLayer.id, "visibility", visibility);
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
2165
|
+
if (this.map.isStyleLoaded()) mountCoreScene(this.map);
|
|
2166
|
+
this.enabled = treesEnabled;
|
|
3796
2167
|
this.button.textContent = this.enabled ? "TREES ON" : "TREES OFF";
|
|
3797
2168
|
this.button.setAttribute("aria-pressed", String(this.enabled));
|
|
3798
|
-
this.button.title = this.enabled ? "Hide
|
|
2169
|
+
this.button.title = this.enabled ? "Hide vegetation" : "Show vegetation";
|
|
3799
2170
|
}
|
|
3800
2171
|
|
|
3801
2172
|
onRemove() {
|
|
@@ -3891,24 +2262,21 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3891
2262
|
...visibleLayerGroups,
|
|
3892
2263
|
].join(",");
|
|
3893
2264
|
}
|
|
2265
|
+
const vegetationLayers = map.getStyle()?.metadata?.["tileflow:vegetation-display"]?.flatLayers;
|
|
3894
2266
|
for (const layer of map.getStyle()?.layers || []) {
|
|
3895
|
-
if (!defaultLayerVisibility.has(layer.id)) {
|
|
3896
|
-
defaultLayerVisibility.set(
|
|
3897
|
-
layer.id,
|
|
3898
|
-
map.getLayoutProperty(layer.id, "visibility") || "visible"
|
|
3899
|
-
);
|
|
3900
|
-
}
|
|
3901
2267
|
if (layer.type === "symbol" && layer.layout?.["text-field"] !== undefined) {
|
|
3902
|
-
|
|
2268
|
+
const hideLabels = !visibleLayerGroups.has("labels");
|
|
2269
|
+
if (hideLabels && !defaultLayerTextField.has(layer.id)) {
|
|
3903
2270
|
defaultLayerTextField.set(
|
|
3904
2271
|
layer.id,
|
|
3905
2272
|
map.getLayoutProperty(layer.id, "text-field")
|
|
3906
2273
|
);
|
|
3907
2274
|
}
|
|
3908
|
-
const
|
|
3909
|
-
|
|
3910
|
-
|
|
2275
|
+
const restoreText = !hideLabels && defaultLayerTextField.has(layer.id);
|
|
2276
|
+
const textField = hideLabels ? "" : defaultLayerTextField.get(layer.id);
|
|
2277
|
+
if (restoreText) defaultLayerTextField.delete(layer.id);
|
|
3911
2278
|
if (
|
|
2279
|
+
(hideLabels || restoreText) &&
|
|
3912
2280
|
!previewLayoutValuesEqual(
|
|
3913
2281
|
map.getLayoutProperty(layer.id, "text-field"),
|
|
3914
2282
|
textField
|
|
@@ -3917,14 +2285,29 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3917
2285
|
map.setLayoutProperty(layer.id, "text-field", textField);
|
|
3918
2286
|
}
|
|
3919
2287
|
}
|
|
2288
|
+
// The shared vegetation controller owns every flat representation.
|
|
2289
|
+
if (vegetationLayers?.some((entry) => entry.id === layer.id.split("::")[0])) continue;
|
|
3920
2290
|
const groups = previewLayerGroups(layer);
|
|
2291
|
+
const hideLayer = groups.some((group) => !visibleLayerGroups.has(group));
|
|
2292
|
+
// Group controls only own the properties they mask. Ordinary style events must
|
|
2293
|
+
// preserve temporary DevTools edits, including visibility and label text.
|
|
2294
|
+
if (hideLayer && !defaultLayerVisibility.has(layer.id)) {
|
|
2295
|
+
defaultLayerVisibility.set(layer.id, map.getLayoutProperty(layer.id, "visibility") || "visible");
|
|
2296
|
+
}
|
|
2297
|
+
if (!hideLayer && !defaultLayerVisibility.has(layer.id)) continue;
|
|
3921
2298
|
const threeDimensionalToggle = layer.metadata?.["tileflow:3d-toggle"];
|
|
3922
2299
|
const authoredVisibility = defaultLayerVisibility.get(layer.id);
|
|
3923
|
-
|
|
3924
|
-
|
|
2300
|
+
if (!hideLayer) defaultLayerVisibility.delete(layer.id);
|
|
2301
|
+
const styleMetadata = map.getStyle()?.metadata ?? {};
|
|
2302
|
+
// Core's 3D buildings and trees stand in for compiled extrusions and flat trees.
|
|
2303
|
+
const replacedByCore =
|
|
2304
|
+
threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"];
|
|
2305
|
+
const toggleVisibility = replacedByCore
|
|
2306
|
+
? "none"
|
|
2307
|
+
: threeDimensionalToggle === "building" || threeDimensionalToggle === "landmark"
|
|
3925
2308
|
? (threeDimensionalEnabled ? "visible" : "none")
|
|
3926
2309
|
: authoredVisibility;
|
|
3927
|
-
const visibility =
|
|
2310
|
+
const visibility = hideLayer
|
|
3928
2311
|
? "none"
|
|
3929
2312
|
: toggleVisibility;
|
|
3930
2313
|
if (map.getLayoutProperty(layer.id, "visibility") !== visibility) {
|
|
@@ -3939,6 +2322,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3939
2322
|
map.fire("tileflow:visible-layer-groups", {groups: [...visibleLayerGroups]});
|
|
3940
2323
|
}
|
|
3941
2324
|
|
|
2325
|
+
function previewTransformRequest(url) {
|
|
2326
|
+
try {
|
|
2327
|
+
return new URL(url, location.href).hostname === "dev-tiles.tileflow.dev"
|
|
2328
|
+
? {credentials: "include", url}
|
|
2329
|
+
: {url};
|
|
2330
|
+
} catch {
|
|
2331
|
+
return {url};
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
|
|
3942
2335
|
if (styleUrl) {
|
|
3943
2336
|
// Dense vector tiles finish in bursts with multiple workers, causing several
|
|
3944
2337
|
// bucket uploads in one frame. One worker keeps navigation smooth; benchmarks
|
|
@@ -3949,23 +2342,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3949
2342
|
registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
|
|
3950
2343
|
registerTileflowPmtilesProtocol({addProtocol: maplibregl.addProtocol});
|
|
3951
2344
|
const map = new maplibregl.Map({
|
|
3952
|
-
collectResourceTiming: mapBenchmarkEnabled,
|
|
2345
|
+
collectResourceTiming: mapBenchmarkEnabled || Boolean(devtoolsObserver),
|
|
3953
2346
|
container: "map",
|
|
3954
2347
|
style: styleUrl,
|
|
3955
|
-
transformRequest:
|
|
3956
|
-
try {
|
|
3957
|
-
return new URL(url, location.href).hostname === "dev-tiles.tileflow.dev"
|
|
3958
|
-
? {credentials: "include", url}
|
|
3959
|
-
: {url};
|
|
3960
|
-
} catch {
|
|
3961
|
-
return {url};
|
|
3962
|
-
}
|
|
3963
|
-
},
|
|
2348
|
+
transformRequest: previewTransformRequest,
|
|
3964
2349
|
...resolveInitialMapOptions(previewMapOptions)
|
|
3965
2350
|
});
|
|
3966
2351
|
globalThis.__tileflowPreviewMap = map;
|
|
2352
|
+
devtoolsObserver?.attach(map);
|
|
3967
2353
|
installMapBenchmark(map);
|
|
3968
|
-
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
|
2354
|
+
if (window.parent === window) map.addControl(new maplibregl.NavigationControl(), "top-right");
|
|
3969
2355
|
let threeDimensionalControl;
|
|
3970
2356
|
let treeControl;
|
|
3971
2357
|
if (isSemanticPreview) {
|
|
@@ -3973,10 +2359,17 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3973
2359
|
treeControl = new TreeControl();
|
|
3974
2360
|
map.addControl(threeDimensionalControl, "top-right");
|
|
3975
2361
|
map.addControl(treeControl, "top-right");
|
|
2362
|
+
// Embedded Map DevTools owns the common Controls sidebar. Keep the controls mounted
|
|
2363
|
+
// for their renderer lifecycle and message bridge, but remove duplicate buttons.
|
|
2364
|
+
if (window.parent !== window) {
|
|
2365
|
+
threeDimensionalControl.container.hidden = true;
|
|
2366
|
+
treeControl.container.hidden = true;
|
|
2367
|
+
}
|
|
3976
2368
|
}
|
|
3977
|
-
|
|
2369
|
+
// Map DevTools has a shared theme selector in its Controls panel; keep the map-level
|
|
2370
|
+
// cycling button for standalone previews only.
|
|
2371
|
+
if (previewThemes.length > 1 && window.parent === window) map.addControl(new ThemeControl(), "top-right");
|
|
3978
2372
|
let ensuringThreeDimensionalLayers;
|
|
3979
|
-
const treeRuntimeMinimumZoom = 16;
|
|
3980
2373
|
const ensureThreeDimensionalLayers = () => {
|
|
3981
2374
|
if (ensuringThreeDimensionalLayers) return ensuringThreeDimensionalLayers;
|
|
3982
2375
|
const currentStyleLayers = map.getStyle()?.layers || [];
|
|
@@ -3995,11 +2388,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
3995
2388
|
landmarkManifestUrl &&
|
|
3996
2389
|
threeDimensionalEnabled &&
|
|
3997
2390
|
map.getZoom() >= landmarkPreloadZoom;
|
|
3998
|
-
const needsTrees =
|
|
3999
|
-
treesEnabled &&
|
|
4000
|
-
treeRendererMode !== "circle" &&
|
|
4001
|
-
map.getZoom() >= treeRuntimeMinimumZoom &&
|
|
4002
|
-
!map.getLayer("tileflow-vegetation-trees-3d");
|
|
4003
2391
|
const needsLandmarks =
|
|
4004
2392
|
landmarkConfigLayer &&
|
|
4005
2393
|
threeDimensionalEnabled &&
|
|
@@ -4011,7 +2399,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
4011
2399
|
styleLayerIsVisibleAtZoom(buildingWireframeConfigLayer, map.getZoom()) &&
|
|
4012
2400
|
!map.getLayer("tileflow-buildings-wireframe-3d");
|
|
4013
2401
|
if (
|
|
4014
|
-
!needsTrees &&
|
|
4015
2402
|
!needsLandmarks &&
|
|
4016
2403
|
!needsBuildingWireframe &&
|
|
4017
2404
|
!shouldPreloadLandmarks
|
|
@@ -4038,11 +2425,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
4038
2425
|
await loadBuildingWireframeRuntime();
|
|
4039
2426
|
addBuildingWireframeLayerIfConfigured(map, styleLayers);
|
|
4040
2427
|
}
|
|
4041
|
-
if (
|
|
4042
|
-
treesEnabled &&
|
|
4043
|
-
treeRendererMode !== "circle" &&
|
|
4044
|
-
map.getZoom() >= treeRuntimeMinimumZoom
|
|
4045
|
-
) addTreeLayerIfConfigured(map, styleLayers);
|
|
4046
2428
|
const landmarkConfigLayer = styleLayers.find(
|
|
4047
2429
|
(layer) => layer.metadata?.["tileflow:landmark-manifest-url"]
|
|
4048
2430
|
);
|
|
@@ -4073,12 +2455,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
4073
2455
|
map.on("tileflow:3d-toggle", ensureThreeDimensionalLayers);
|
|
4074
2456
|
map.on("tileflow:trees-toggle", ensureThreeDimensionalLayers);
|
|
4075
2457
|
map.on("load", () => {
|
|
2458
|
+
mountCoreScene(map);
|
|
2459
|
+
treeControl?.update();
|
|
4076
2460
|
writeCameraToUrl(map);
|
|
4077
2461
|
updateGlobeBackdrop(map);
|
|
4078
2462
|
ensureThreeDimensionalLayers();
|
|
4079
2463
|
});
|
|
4080
2464
|
map.on("move", () => updateGlobeBackdrop(map));
|
|
2465
|
+
map.on("style.load", () => updateGlobeBackdrop(map));
|
|
4081
2466
|
map.on("moveend", () => writeCameraToUrl(map));
|
|
2467
|
+
${previewSceneClient}
|
|
4082
2468
|
addEventListener("message", (event) => {
|
|
4083
2469
|
if (
|
|
4084
2470
|
window.parent === window ||
|
|
@@ -4149,6 +2535,15 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
4149
2535
|
}
|
|
4150
2536
|
|
|
4151
2537
|
applyStatus(initialStatus);
|
|
2538
|
+
// Embedded previews share their host's stream. Two streams per tab can exhaust Chrome's
|
|
2539
|
+
// six HTTP/1 connections and leave even local module/font requests waiting indefinitely.
|
|
2540
|
+
let unsubscribePreviewStatus;
|
|
2541
|
+
try {
|
|
2542
|
+
if (window.parent !== window) unsubscribePreviewStatus = window.parent.__tileflowDevtoolsHost?.subscribePreviewStatus?.(window, applyStatus);
|
|
2543
|
+
} catch { /* Other embedders keep the standalone stream. */ }
|
|
2544
|
+
if (unsubscribePreviewStatus) {
|
|
2545
|
+
addEventListener("beforeunload", unsubscribePreviewStatus);
|
|
2546
|
+
} else {
|
|
4152
2547
|
const events = new EventSource(${JSON.stringify(`${basePath}/__events`)});
|
|
4153
2548
|
// Generations are monotonic only within one dev-server process. A source-code watcher can
|
|
4154
2549
|
// restart that process at generation 1, so reconnecting after a live connection was lost
|
|
@@ -4169,6 +2564,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
|
|
|
4169
2564
|
for (const eventName of ["building", "ready", "invalid"]) {
|
|
4170
2565
|
events.addEventListener(eventName, (event) => applyStatus(JSON.parse(event.data)));
|
|
4171
2566
|
}
|
|
2567
|
+
}
|
|
4172
2568
|
</script>
|
|
4173
2569
|
</body>
|
|
4174
2570
|
</html>`;
|