map-zero 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/docs/api.md +66 -0
- package/docs/architecture.md +87 -0
- package/docs/cartography.md +77 -0
- package/docs/cesium.md +107 -0
- package/docs/openlayers.md +98 -0
- package/docs/styles.md +103 -0
- package/package.json +51 -0
- package/packages/cesium/package.json +13 -0
- package/packages/cesium/src/index.js +405 -0
- package/packages/ol/package.json +14 -0
- package/packages/ol/src/index.js +1705 -0
- package/packages/ol/src/labels.js +977 -0
- package/src/3dtiles/b3dm.js +38 -0
- package/src/3dtiles/clipper-surfaces.js +317 -0
- package/src/3dtiles/export.js +768 -0
- package/src/3dtiles/extrude.js +301 -0
- package/src/3dtiles/flat.js +531 -0
- package/src/3dtiles/glb.js +178 -0
- package/src/3dtiles/gpkg-buildings.js +240 -0
- package/src/3dtiles/gpkg-features.js +157 -0
- package/src/3dtiles/tileset.js +75 -0
- package/src/build.js +134 -0
- package/src/cli.js +656 -0
- package/src/export-pmtiles.js +962 -0
- package/src/geometry-read.js +50 -0
- package/src/gpkg-read.js +460 -0
- package/src/gpkg.js +567 -0
- package/src/html.js +593 -0
- package/src/layers.js +357 -0
- package/src/manifest.js +29 -0
- package/src/mvt.js +2593 -0
- package/src/ol.js +5 -0
- package/src/osm.js +2110 -0
- package/src/pmtiles-worker.js +70 -0
- package/src/pmtiles.js +260 -0
- package/src/server.js +720 -0
- package/src/style-command.js +78 -0
- package/src/style-filters.js +76 -0
- package/src/style-presets.js +93 -0
- package/src/style-themes.js +235 -0
- package/src/style.js +13 -0
- package/src/tile-cache.js +59 -0
- package/src/utils.js +222 -0
- package/styles/presets/light.json +4655 -0
- package/styles/presets/monochrome.json +4655 -0
- package/styles/presets/neon-dark-3d.json +90 -0
- package/styles/presets/neon-dark.json +4690 -0
- package/styles/presets/tactical.json +4690 -0
- package/styles/themes/neon-dark.theme.json +20 -0
package/src/html.js
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Return the OpenLayers WebGL vector tile viewer HTML.
|
|
3
|
+
*
|
|
4
|
+
* @param {{ assetVersion?: string }} [options]
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
export function createViewerHtml(options = {}) {
|
|
8
|
+
const assetVersion = encodeURIComponent(options.assetVersion ?? String(Date.now()));
|
|
9
|
+
return `<!doctype html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14
|
+
<title>map-zero</title>
|
|
15
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@10.9.0/ol.css">
|
|
16
|
+
<style>
|
|
17
|
+
html,
|
|
18
|
+
body {
|
|
19
|
+
margin: 0;
|
|
20
|
+
width: 100%;
|
|
21
|
+
height: 100%;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
background: #000;
|
|
24
|
+
color: #d7fbff;
|
|
25
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#app {
|
|
29
|
+
display: grid;
|
|
30
|
+
grid-template-columns: 280px minmax(0, 1fr);
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#panel {
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
border-right: 1px solid rgba(0, 255, 255, 0.22);
|
|
38
|
+
background: #050505;
|
|
39
|
+
padding: 16px;
|
|
40
|
+
overflow: auto;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#map {
|
|
44
|
+
min-width: 0;
|
|
45
|
+
min-height: 0;
|
|
46
|
+
background: #000;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
h1 {
|
|
50
|
+
margin: 0 0 18px;
|
|
51
|
+
color: #fff;
|
|
52
|
+
font-size: 18px;
|
|
53
|
+
font-weight: 650;
|
|
54
|
+
letter-spacing: 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.layer-list {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: 10px;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.layer-row {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 10px;
|
|
66
|
+
color: #d7fbff;
|
|
67
|
+
font-size: 14px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.layer-row input {
|
|
71
|
+
width: 16px;
|
|
72
|
+
height: 16px;
|
|
73
|
+
accent-color: #00ffff;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#status {
|
|
77
|
+
margin-top: 18px;
|
|
78
|
+
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
|
79
|
+
padding-top: 12px;
|
|
80
|
+
color: #9bb7bd;
|
|
81
|
+
font-size: 12px;
|
|
82
|
+
line-height: 1.5;
|
|
83
|
+
white-space: pre-line;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@media (max-width: 720px) {
|
|
87
|
+
#app {
|
|
88
|
+
grid-template-columns: 1fr;
|
|
89
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#panel {
|
|
93
|
+
border-right: 0;
|
|
94
|
+
border-bottom: 1px solid rgba(0, 255, 255, 0.22);
|
|
95
|
+
max-height: 220px;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
</style>
|
|
99
|
+
<script type="importmap">
|
|
100
|
+
{
|
|
101
|
+
"imports": {
|
|
102
|
+
"ol/": "https://esm.sh/ol@10.9.0/",
|
|
103
|
+
"pmtiles": "/vendor/pmtiles.js",
|
|
104
|
+
"fflate": "/vendor/fflate.js"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
108
|
+
</head>
|
|
109
|
+
<body>
|
|
110
|
+
<div id="app">
|
|
111
|
+
<aside id="panel">
|
|
112
|
+
<h1 id="title">map-zero</h1>
|
|
113
|
+
<div id="layers" class="layer-list"></div>
|
|
114
|
+
<div id="status">Loading MVT tiles</div>
|
|
115
|
+
</aside>
|
|
116
|
+
<main id="map"></main>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<script type="module">
|
|
120
|
+
import OLMap from 'ol/Map.js';
|
|
121
|
+
import View from 'ol/View.js';
|
|
122
|
+
import {fromLonLat} from 'ol/proj.js';
|
|
123
|
+
import {addMapZeroToOpenLayers, loadMapZeroManifest} from '/map-zero-ol.js?v=${assetVersion}';
|
|
124
|
+
|
|
125
|
+
const statusEl = document.getElementById('status');
|
|
126
|
+
const layersEl = document.getElementById('layers');
|
|
127
|
+
const titleEl = document.getElementById('title');
|
|
128
|
+
const layerStatus = new globalThis.Map();
|
|
129
|
+
let loadingTiles = 0;
|
|
130
|
+
let tileErrors = 0;
|
|
131
|
+
|
|
132
|
+
start().catch((error) => {
|
|
133
|
+
statusEl.textContent = error.message;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
async function start() {
|
|
137
|
+
const manifest = await loadMapZeroManifest('/manifest.json');
|
|
138
|
+
titleEl.textContent = manifest.name || 'map-zero';
|
|
139
|
+
|
|
140
|
+
const bbox = normalizeBbox(manifest.bbox);
|
|
141
|
+
const tiles3dBbox = normalizeBbox(manifest.tiles3d?.bbox);
|
|
142
|
+
const focusBbox = normalizeBbox(manifest.tiles3d?.focusBbox);
|
|
143
|
+
const initialView = readInitialView(bbox);
|
|
144
|
+
const map = new OLMap({
|
|
145
|
+
target: 'map',
|
|
146
|
+
layers: [],
|
|
147
|
+
view: new View({
|
|
148
|
+
center: fromLonLat(initialView.center),
|
|
149
|
+
zoom: initialView.zoom
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const controller = await addMapZeroToOpenLayers(map, {
|
|
154
|
+
manifestUrl: '/manifest.json',
|
|
155
|
+
manifest,
|
|
156
|
+
style: 'default',
|
|
157
|
+
onTileLoadStart() {
|
|
158
|
+
loadingTiles += 1;
|
|
159
|
+
updateStatus();
|
|
160
|
+
},
|
|
161
|
+
onTileLoadEnd() {
|
|
162
|
+
loadingTiles = Math.max(0, loadingTiles - 1);
|
|
163
|
+
updateStatus();
|
|
164
|
+
},
|
|
165
|
+
onTileLoadError() {
|
|
166
|
+
loadingTiles = Math.max(0, loadingTiles - 1);
|
|
167
|
+
tileErrors += 1;
|
|
168
|
+
updateStatus();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
document.body.style.background = controller.style.background || '#000000';
|
|
173
|
+
|
|
174
|
+
for (const layer of controller.manifest.layers || []) {
|
|
175
|
+
addLayerToggle(layer, controller);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
updateStatus();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function addLayerToggle(layer, controller) {
|
|
182
|
+
const rule = controller.style.layers?.[layer.style || layer.id] || {};
|
|
183
|
+
const label = document.createElement('label');
|
|
184
|
+
label.className = 'layer-row';
|
|
185
|
+
|
|
186
|
+
const input = document.createElement('input');
|
|
187
|
+
input.type = 'checkbox';
|
|
188
|
+
input.checked = rule.visible !== false;
|
|
189
|
+
input.addEventListener('change', () => {
|
|
190
|
+
controller.setVisible(layer.id, input.checked);
|
|
191
|
+
layerStatus.set(layer.id, input.checked ? 'visible' : 'hidden');
|
|
192
|
+
updateStatus();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const text = document.createElement('span');
|
|
196
|
+
text.textContent = layer.id;
|
|
197
|
+
|
|
198
|
+
layerStatus.set(layer.id, input.checked ? 'visible' : 'hidden');
|
|
199
|
+
label.append(input, text);
|
|
200
|
+
layersEl.append(label);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function updateStatus() {
|
|
204
|
+
const tileStatus = loadingTiles > 0
|
|
205
|
+
? 'tiles: loading ' + loadingTiles
|
|
206
|
+
: tileErrors > 0
|
|
207
|
+
? 'tiles: ' + tileErrors + ' errors'
|
|
208
|
+
: 'tiles: ready';
|
|
209
|
+
const layerLines = [...layerStatus.entries()].map(([layer, status]) => layer + ': ' + status);
|
|
210
|
+
statusEl.textContent = [tileStatus, ...layerLines].join('\\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function centerOfBbox(bbox) {
|
|
214
|
+
if (!bbox) {
|
|
215
|
+
return [0, 0];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readInitialView(bbox) {
|
|
222
|
+
const params = new URLSearchParams(window.location.search);
|
|
223
|
+
const hasExplicitView = params.has('lon') && params.has('lat') && params.has('zoom');
|
|
224
|
+
const lon = Number(params.get('lon'));
|
|
225
|
+
const lat = Number(params.get('lat'));
|
|
226
|
+
const zoom = Number(params.get('zoom'));
|
|
227
|
+
|
|
228
|
+
if (hasExplicitView && Number.isFinite(lon) && Number.isFinite(lat) && Number.isFinite(zoom)) {
|
|
229
|
+
return {
|
|
230
|
+
center: [lon, lat],
|
|
231
|
+
zoom,
|
|
232
|
+
fromUrl: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
center: centerOfBbox(bbox),
|
|
238
|
+
zoom: initialZoomForBbox(bbox),
|
|
239
|
+
fromUrl: false
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function initialZoomForBbox(bbox) {
|
|
244
|
+
if (!bbox) {
|
|
245
|
+
return 12;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const lonSpan = Math.abs(bbox[2] - bbox[0]);
|
|
249
|
+
const latSpan = Math.abs(bbox[3] - bbox[1]);
|
|
250
|
+
const span = Math.max(lonSpan, latSpan);
|
|
251
|
+
if (span > 6) return 8;
|
|
252
|
+
if (span > 1) return 10;
|
|
253
|
+
if (span > 0.25) return 12;
|
|
254
|
+
return 14;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeBbox(bbox) {
|
|
258
|
+
if (!Array.isArray(bbox) || bbox.length !== 4) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const values = bbox.map(Number);
|
|
263
|
+
if (
|
|
264
|
+
values.some((value) => !Number.isFinite(value)) ||
|
|
265
|
+
values[0] >= values[2] ||
|
|
266
|
+
values[1] >= values[3]
|
|
267
|
+
) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return values;
|
|
272
|
+
}
|
|
273
|
+
</script>
|
|
274
|
+
</body>
|
|
275
|
+
</html>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Return a minimal Cesium 3D Tiles viewer HTML.
|
|
280
|
+
*
|
|
281
|
+
* @param {{ assetVersion?: string }} [options]
|
|
282
|
+
* @returns {string}
|
|
283
|
+
*/
|
|
284
|
+
export function createCesiumViewerHtml(options = {}) {
|
|
285
|
+
const assetVersion = encodeURIComponent(options.assetVersion ?? String(Date.now()));
|
|
286
|
+
return `<!doctype html>
|
|
287
|
+
<html lang="en">
|
|
288
|
+
<head>
|
|
289
|
+
<meta charset="utf-8">
|
|
290
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
291
|
+
<title>map-zero Cesium</title>
|
|
292
|
+
<script src="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Cesium.js"></script>
|
|
293
|
+
<link rel="stylesheet" href="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Widgets/widgets.css">
|
|
294
|
+
<style>
|
|
295
|
+
html,
|
|
296
|
+
body {
|
|
297
|
+
margin: 0;
|
|
298
|
+
width: 100%;
|
|
299
|
+
height: 100%;
|
|
300
|
+
overflow: hidden;
|
|
301
|
+
background: #000;
|
|
302
|
+
color: #d7fbff;
|
|
303
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#app {
|
|
307
|
+
display: grid;
|
|
308
|
+
grid-template-columns: 280px minmax(0, 1fr);
|
|
309
|
+
width: 100%;
|
|
310
|
+
height: 100%;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#panel {
|
|
314
|
+
box-sizing: border-box;
|
|
315
|
+
border-right: 1px solid rgba(0, 255, 255, 0.22);
|
|
316
|
+
background: #050505;
|
|
317
|
+
padding: 16px;
|
|
318
|
+
overflow: auto;
|
|
319
|
+
z-index: 2;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#cesiumContainer {
|
|
323
|
+
min-width: 0;
|
|
324
|
+
min-height: 0;
|
|
325
|
+
background: #000;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
h1 {
|
|
329
|
+
margin: 0 0 18px;
|
|
330
|
+
color: #fff;
|
|
331
|
+
font-size: 18px;
|
|
332
|
+
font-weight: 650;
|
|
333
|
+
letter-spacing: 0;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.control-list {
|
|
337
|
+
display: grid;
|
|
338
|
+
gap: 12px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.control-row {
|
|
342
|
+
display: grid;
|
|
343
|
+
gap: 6px;
|
|
344
|
+
color: #d7fbff;
|
|
345
|
+
font-size: 14px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.inline-row {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 10px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
input[type="checkbox"] {
|
|
355
|
+
width: 16px;
|
|
356
|
+
height: 16px;
|
|
357
|
+
accent-color: #00ffff;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
input[type="range"] {
|
|
361
|
+
width: 100%;
|
|
362
|
+
accent-color: #00ffff;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#status {
|
|
366
|
+
margin-top: 18px;
|
|
367
|
+
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
|
368
|
+
padding-top: 12px;
|
|
369
|
+
color: #9bb7bd;
|
|
370
|
+
font-size: 12px;
|
|
371
|
+
line-height: 1.5;
|
|
372
|
+
white-space: pre-line;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.cesium-widget-credits {
|
|
376
|
+
display: none !important;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@media (max-width: 720px) {
|
|
380
|
+
#app {
|
|
381
|
+
grid-template-columns: 1fr;
|
|
382
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#panel {
|
|
386
|
+
border-right: 0;
|
|
387
|
+
border-bottom: 1px solid rgba(0, 255, 255, 0.22);
|
|
388
|
+
max-height: 220px;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
</style>
|
|
392
|
+
</head>
|
|
393
|
+
<body>
|
|
394
|
+
<div id="app">
|
|
395
|
+
<aside id="panel">
|
|
396
|
+
<h1 id="title">map-zero 3D</h1>
|
|
397
|
+
<div class="control-list">
|
|
398
|
+
<div id="layerControls"></div>
|
|
399
|
+
<label class="control-row">
|
|
400
|
+
<span>opacity</span>
|
|
401
|
+
<input id="layerOpacity" type="range" min="0.15" max="1" step="0.05" value="1">
|
|
402
|
+
</label>
|
|
403
|
+
</div>
|
|
404
|
+
<div id="status">Loading 3D Tiles</div>
|
|
405
|
+
</aside>
|
|
406
|
+
<main id="cesiumContainer"></main>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<script type="module">
|
|
410
|
+
import {addMapZeroToCesium, loadMapZeroManifest} from '/map-zero-cesium.js?v=${assetVersion}';
|
|
411
|
+
|
|
412
|
+
const statusEl = document.getElementById('status');
|
|
413
|
+
const titleEl = document.getElementById('title');
|
|
414
|
+
const layerControlsEl = document.getElementById('layerControls');
|
|
415
|
+
const opacityEl = document.getElementById('layerOpacity');
|
|
416
|
+
|
|
417
|
+
start().catch((error) => {
|
|
418
|
+
statusEl.textContent = error.message;
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
async function start() {
|
|
422
|
+
if (!globalThis.Cesium) {
|
|
423
|
+
throw new Error('Cesium failed to load');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
statusEl.textContent = 'Loading manifest';
|
|
427
|
+
const manifest = await loadMapZeroManifest('/manifest.json');
|
|
428
|
+
titleEl.textContent = (manifest.name || 'map-zero') + ' 3D';
|
|
429
|
+
if (!manifest.tiles3d && !manifest.cesium?.tilesets) {
|
|
430
|
+
throw new Error('This package does not define manifest.tiles3d. Run: map-zero 3dtiles <package.mapzero>');
|
|
431
|
+
}
|
|
432
|
+
const sourceLabel = manifest.tiles3d?.url || manifest.cesium?.tilesets?.buildings || '3dtiles';
|
|
433
|
+
|
|
434
|
+
statusEl.textContent = 'Creating Cesium viewer';
|
|
435
|
+
const Cesium = globalThis.Cesium;
|
|
436
|
+
Cesium.Ion.defaultAccessToken = '';
|
|
437
|
+
const bbox = normalizeBbox(manifest.bbox);
|
|
438
|
+
const tiles3dBbox = normalizeBbox(manifest.tiles3d?.bbox || manifest.cesium?.bbox || manifest.bbox);
|
|
439
|
+
const focusBbox = normalizeBbox(manifest.tiles3d?.focusBbox || manifest.cesium?.focusBbox);
|
|
440
|
+
const viewer = new Cesium.Viewer('cesiumContainer', {
|
|
441
|
+
animation: false,
|
|
442
|
+
baseLayer: false,
|
|
443
|
+
baseLayerPicker: false,
|
|
444
|
+
fullscreenButton: false,
|
|
445
|
+
geocoder: false,
|
|
446
|
+
homeButton: true,
|
|
447
|
+
infoBox: true,
|
|
448
|
+
navigationHelpButton: false,
|
|
449
|
+
sceneModePicker: true,
|
|
450
|
+
selectionIndicator: true,
|
|
451
|
+
timeline: false,
|
|
452
|
+
terrainProvider: new Cesium.EllipsoidTerrainProvider()
|
|
453
|
+
});
|
|
454
|
+
viewer.imageryLayers.removeAll();
|
|
455
|
+
|
|
456
|
+
statusEl.textContent = 'Loading 3D Tileset';
|
|
457
|
+
const controller = await addMapZeroToCesium(viewer, {
|
|
458
|
+
manifestUrl: '/manifest.json',
|
|
459
|
+
style: 'cesium',
|
|
460
|
+
opacity: Number(opacityEl.value),
|
|
461
|
+
zoomTo: false,
|
|
462
|
+
applyDefaultSceneStyle: true
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
statusEl.textContent = 'Positioning camera';
|
|
466
|
+
const firstTileset = Object.values(controller.tilesets)[0];
|
|
467
|
+
if (focusBbox || tiles3dBbox) {
|
|
468
|
+
const targetBbox = focusBbox || tiles3dBbox;
|
|
469
|
+
const centerLon = (targetBbox[0] + targetBbox[2]) / 2;
|
|
470
|
+
const centerLat = (targetBbox[1] + targetBbox[3]) / 2;
|
|
471
|
+
const spanMeters = bboxDiagonalMeters(targetBbox);
|
|
472
|
+
const altitude = Math.max(1000, Math.min(5000, spanMeters * 0.35));
|
|
473
|
+
viewer.camera.setView({
|
|
474
|
+
destination: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, altitude),
|
|
475
|
+
orientation: {
|
|
476
|
+
heading: 0,
|
|
477
|
+
pitch: Cesium.Math.toRadians(-62),
|
|
478
|
+
roll: 0
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
} else if (firstTileset?.boundingSphere) {
|
|
482
|
+
const radius = Math.max(2000, firstTileset.boundingSphere.radius * 1.8);
|
|
483
|
+
viewer.camera.viewBoundingSphere(
|
|
484
|
+
firstTileset.boundingSphere,
|
|
485
|
+
new Cesium.HeadingPitchRange(0, -0.75, radius)
|
|
486
|
+
);
|
|
487
|
+
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
|
|
488
|
+
} else if (bbox) {
|
|
489
|
+
viewer.camera.setView({
|
|
490
|
+
destination: Cesium.Rectangle.fromDegrees(bbox[0], bbox[1], bbox[2], bbox[3])
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const uniqueTilesets = [...new Set(Object.values(controller.tilesets))];
|
|
495
|
+
let readyTilesets = 0;
|
|
496
|
+
for (const tileset of uniqueTilesets) {
|
|
497
|
+
addCesiumEventListener(tileset.readyEvent, () => {
|
|
498
|
+
readyTilesets += 1;
|
|
499
|
+
statusEl.textContent = [
|
|
500
|
+
'3D Tiles: tileset ready',
|
|
501
|
+
readyTilesets + '/' + uniqueTilesets.length,
|
|
502
|
+
'source: ' + sourceLabel,
|
|
503
|
+
'layers: ' + Object.keys(controller.tilesets).join(', ')
|
|
504
|
+
].join('\\n');
|
|
505
|
+
});
|
|
506
|
+
addCesiumEventListener(tileset.tileLoad, () => {
|
|
507
|
+
statusEl.textContent = [
|
|
508
|
+
'3D Tiles: tile loaded',
|
|
509
|
+
'source: ' + sourceLabel,
|
|
510
|
+
'layers: ' + Object.keys(controller.tilesets).join(', ')
|
|
511
|
+
].join('\\n');
|
|
512
|
+
});
|
|
513
|
+
addCesiumEventListener(tileset.tileFailed, (error) => {
|
|
514
|
+
statusEl.textContent = [
|
|
515
|
+
'3D Tiles: tile error',
|
|
516
|
+
String(error?.message || error || 'unknown error'),
|
|
517
|
+
String(error?.url || '')
|
|
518
|
+
].filter(Boolean).join('\\n');
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
statusEl.textContent = [
|
|
523
|
+
'3D Tiles: ready',
|
|
524
|
+
'source: ' + sourceLabel,
|
|
525
|
+
'layers: ' + Object.keys(controller.tilesets).join(', ')
|
|
526
|
+
].join('\\n');
|
|
527
|
+
|
|
528
|
+
createLayerControls(layerControlsEl, controller);
|
|
529
|
+
opacityEl.addEventListener('input', () => {
|
|
530
|
+
for (const layerId of Object.keys(controller.tilesets)) {
|
|
531
|
+
controller.setOpacity(layerId, Number(opacityEl.value));
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function createLayerControls(container, controller) {
|
|
537
|
+
container.textContent = '';
|
|
538
|
+
for (const layerId of Object.keys(controller.tilesets)) {
|
|
539
|
+
const label = document.createElement('label');
|
|
540
|
+
label.className = 'control-row';
|
|
541
|
+
|
|
542
|
+
const row = document.createElement('span');
|
|
543
|
+
row.className = 'inline-row';
|
|
544
|
+
|
|
545
|
+
const input = document.createElement('input');
|
|
546
|
+
input.type = 'checkbox';
|
|
547
|
+
input.checked = true;
|
|
548
|
+
input.addEventListener('change', () => {
|
|
549
|
+
controller.setVisible(layerId, input.checked);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const text = document.createElement('span');
|
|
553
|
+
text.textContent = layerId;
|
|
554
|
+
|
|
555
|
+
row.append(input, text);
|
|
556
|
+
label.append(row);
|
|
557
|
+
container.append(label);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function normalizeBbox(bbox) {
|
|
562
|
+
if (!Array.isArray(bbox) || bbox.length !== 4) {
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const values = bbox.map(Number);
|
|
567
|
+
if (
|
|
568
|
+
values.some((value) => !Number.isFinite(value)) ||
|
|
569
|
+
values[0] >= values[2] ||
|
|
570
|
+
values[1] >= values[3]
|
|
571
|
+
) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return values;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function bboxDiagonalMeters(bbox) {
|
|
579
|
+
const meanLat = ((bbox[1] + bbox[3]) / 2) * Math.PI / 180;
|
|
580
|
+
const width = Math.abs(bbox[2] - bbox[0]) * 111320 * Math.cos(meanLat);
|
|
581
|
+
const height = Math.abs(bbox[3] - bbox[1]) * 110540;
|
|
582
|
+
return Math.hypot(width, height);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function addCesiumEventListener(event, listener) {
|
|
586
|
+
if (event && typeof event.addEventListener === 'function') {
|
|
587
|
+
event.addEventListener(listener);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
</script>
|
|
591
|
+
</body>
|
|
592
|
+
</html>`;
|
|
593
|
+
}
|