map-zero 0.1.0 → 0.2.1

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.
@@ -746,10 +746,11 @@ function createExportRecommendation(options) {
746
746
  */
747
747
  function tileRangeForBbox(bbox, z) {
748
748
  const [minLon, minLat, maxLon, maxLat] = bbox;
749
- const minX = lonToTileX(minLon, z);
750
- const maxX = lonToTileX(maxLon, z);
751
- const minY = latToTileY(maxLat, z);
752
- const maxY = latToTileY(minLat, z);
749
+ const maxTile = 2 ** z - 1;
750
+ const minX = clamp(lonToTileX(minLon, z) - 1, 0, maxTile);
751
+ const maxX = clamp(lonToTileX(maxLon, z) + 1, 0, maxTile);
752
+ const minY = clamp(latToTileY(maxLat, z) - 1, 0, maxTile);
753
+ const maxY = clamp(latToTileY(minLat, z) + 1, 0, maxTile);
753
754
  return {
754
755
  minX,
755
756
  maxX,
@@ -807,7 +808,7 @@ function formatInteger(value) {
807
808
  * @returns {string[]}
808
809
  */
809
810
  function activeLayerIdsForZoom(manifest, style, zoom) {
810
- const layers = /** @type {Array<Record<string, unknown>>} */ (manifest.layers ?? []);
811
+ const layers = /** @type {string[]} */ (manifest.layers ?? []);
811
812
  const visualLayers = layers
812
813
  .filter((layer) => {
813
814
  const rule = layerStyleRule(style, layer);
@@ -825,19 +826,19 @@ function activeLayerIdsForZoom(manifest, style, zoom) {
825
826
 
826
827
  return true;
827
828
  })
828
- .map((layer) => String(layer.id));
829
+ .map((layer) => String(layer));
829
830
 
830
831
  return visualLayers;
831
832
  }
832
833
 
833
834
  /**
834
835
  * @param {Record<string, unknown> | null} style
835
- * @param {Record<string, unknown>} layer
836
+ * @param {string} layerId
836
837
  * @returns {Record<string, unknown>}
837
838
  */
838
- function layerStyleRule(style, layer) {
839
+ function layerStyleRule(style, layerId) {
839
840
  const styleLayers = /** @type {Record<string, unknown> | undefined} */ (style?.layers);
840
- const rule = /** @type {Record<string, unknown>} */ (styleLayers?.[String(layer.style ?? layer.id)] ?? {});
841
+ const rule = /** @type {Record<string, unknown>} */ (styleLayers?.[layerId] ?? {});
841
842
  const visibility = rule.visibility && typeof rule.visibility === 'object'
842
843
  ? /** @type {Record<string, unknown>} */ (rule.visibility)
843
844
  : null;
@@ -902,7 +903,7 @@ function closeWriteStream(stream) {
902
903
  * @returns {Record<string, unknown>}
903
904
  */
904
905
  function createPmtilesMetadata(manifest, style, minZoom, maxZoom, bbox) {
905
- const layers = /** @type {Array<Record<string, unknown>>} */ (manifest.layers ?? []);
906
+ const layers = /** @type {string[]} */ (manifest.layers ?? []);
906
907
  return {
907
908
  tilejson: '3.0.0',
908
909
  name: manifest.name ?? 'map-zero',
@@ -914,25 +915,25 @@ function createPmtilesMetadata(manifest, style, minZoom, maxZoom, bbox) {
914
915
  center: [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2, Math.min(Math.max(12, minZoom), maxZoom)],
915
916
  minzoom: minZoom,
916
917
  maxzoom: maxZoom,
917
- vector_layers: layers.map((layer) => ({
918
- id: String(layer.id),
919
- fields: fieldsForLayer(style, layer)
918
+ vector_layers: layers.map((layerId) => ({
919
+ id: layerId,
920
+ fields: fieldsForLayer(style, layerId)
920
921
  }))
921
922
  };
922
923
  }
923
924
 
924
925
  /**
925
926
  * @param {Record<string, unknown> | null} style
926
- * @param {Record<string, unknown>} layer
927
+ * @param {string} layerId
927
928
  * @returns {Record<string, string>}
928
929
  */
929
- function fieldsForLayer(style, layer) {
930
+ function fieldsForLayer(style, layerId) {
930
931
  const fields = {
931
932
  id: 'String',
932
933
  name: 'String',
933
934
  layer: 'String'
934
935
  };
935
- const byProperty = /** @type {Record<string, unknown> | undefined} */ (layerStyleRule(style, layer).byProperty);
936
+ const byProperty = /** @type {Record<string, unknown> | undefined} */ (layerStyleRule(style, layerId).byProperty);
936
937
  if (byProperty) {
937
938
  for (const key of Object.keys(byProperty)) {
938
939
  fields[key] = 'String';
@@ -0,0 +1,335 @@
1
+ import { createWriteStream, promises as fs } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { basename, join, resolve } from 'node:path';
4
+ import { once } from 'node:events';
5
+
6
+ import { buildPackage } from './build.js';
7
+ import { export3dTiles } from './3dtiles/export.js';
8
+ import { exportPmtiles } from './export-pmtiles.js';
9
+ import { packageMapZero } from './package.js';
10
+
11
+ const GEOFABRIK_INDEX_URL = 'https://download.geofabrik.de/index-v1.json';
12
+
13
+ /**
14
+ * Download the smallest matching OSM extract and build a complete map-zero
15
+ * package for the requested bbox.
16
+ *
17
+ * @param {{
18
+ * bbox: [number, number, number, number],
19
+ * out: string,
20
+ * layers: string[],
21
+ * minZoom?: number,
22
+ * maxZoom?: number,
23
+ * workers?: number,
24
+ * forcePmtiles?: boolean,
25
+ * pmtiles?: boolean,
26
+ * tiles3d?: boolean,
27
+ * zip?: boolean,
28
+ * includeGpkg?: boolean,
29
+ * batchSize?: number,
30
+ * keepTemp?: boolean,
31
+ * debugBuild?: boolean,
32
+ * cacheDir?: string,
33
+ * providerIndexUrl?: string,
34
+ * forceDownload?: boolean,
35
+ * onStage?: (message: string) => void,
36
+ * onBuildProgress?: Parameters<typeof buildPackage>[0]['onProgress'],
37
+ * onPmtilesProgress?: Parameters<typeof exportPmtiles>[0]['onProgress'],
38
+ * on3dTilesProgress?: Parameters<typeof export3dTiles>[0]['onProgress']
39
+ * }} options
40
+ * @returns {Promise<{
41
+ * outDir: string,
42
+ * source: { name: string, id: string, url: string, path: string },
43
+ * counts: Record<string, number>,
44
+ * pmtiles?: Awaited<ReturnType<typeof exportPmtiles>>,
45
+ * tiles3d?: Awaited<ReturnType<typeof export3dTiles>>,
46
+ * zip?: Awaited<ReturnType<typeof packageMapZero>>
47
+ * }>}
48
+ */
49
+ export async function createPackageFromBbox(options) {
50
+ const bbox = options.bbox;
51
+ const outDir = resolve(options.out);
52
+ const cacheDir = resolve(options.cacheDir ?? join(homedir(), '.cache', 'map-zero', 'osm'));
53
+ const provider = await findGeofabrikExtract(bbox, {
54
+ cacheDir,
55
+ indexUrl: options.providerIndexUrl
56
+ });
57
+
58
+ options.onStage?.(`Using OSM extract: ${provider.name} (${provider.id})`);
59
+ const sourcePath = await downloadExtract(provider, {
60
+ cacheDir,
61
+ forceDownload: Boolean(options.forceDownload),
62
+ onStage: options.onStage
63
+ });
64
+
65
+ options.onStage?.('Building map-zero package');
66
+ const build = await buildPackage({
67
+ source: sourcePath,
68
+ bbox,
69
+ layers: options.layers,
70
+ out: outDir,
71
+ keepTemp: options.keepTemp,
72
+ batchSize: options.batchSize,
73
+ debugBuild: options.debugBuild,
74
+ onProgress: options.onBuildProgress
75
+ });
76
+
77
+ /** @type {Awaited<ReturnType<typeof exportPmtiles>> | undefined} */
78
+ let pmtiles;
79
+ if (options.pmtiles !== false) {
80
+ options.onStage?.('Exporting PMTiles');
81
+ pmtiles = await exportPmtiles({
82
+ packageDir: outDir,
83
+ minZoom: options.minZoom ?? 8,
84
+ maxZoom: options.maxZoom ?? 16,
85
+ workers: options.workers ?? 1,
86
+ force: Boolean(options.forcePmtiles),
87
+ onProgress: options.onPmtilesProgress
88
+ });
89
+ }
90
+
91
+ /** @type {Awaited<ReturnType<typeof export3dTiles>> | undefined} */
92
+ let tiles3d;
93
+ if (options.tiles3d !== false) {
94
+ options.onStage?.('Exporting 3D Tiles');
95
+ tiles3d = await export3dTiles({
96
+ packageDir: outDir,
97
+ onProgress: options.on3dTilesProgress
98
+ });
99
+ }
100
+
101
+ /** @type {Awaited<ReturnType<typeof packageMapZero>> | undefined} */
102
+ let zip;
103
+ if (options.zip !== false) {
104
+ options.onStage?.('Creating portable zip');
105
+ zip = await packageMapZero({
106
+ packageDir: outDir,
107
+ includeGpkg: Boolean(options.includeGpkg)
108
+ });
109
+ }
110
+
111
+ return {
112
+ outDir,
113
+ source: {
114
+ name: provider.name,
115
+ id: provider.id,
116
+ url: provider.url,
117
+ path: sourcePath
118
+ },
119
+ counts: build.counts,
120
+ pmtiles,
121
+ tiles3d,
122
+ zip
123
+ };
124
+ }
125
+
126
+ /**
127
+ * @param {[number, number, number, number]} bbox
128
+ * @param {{ cacheDir: string, indexUrl?: string }} options
129
+ */
130
+ async function findGeofabrikExtract(bbox, options) {
131
+ const index = await loadGeofabrikIndex(options.cacheDir, options.indexUrl ?? GEOFABRIK_INDEX_URL);
132
+ const features = Array.isArray(index.features) ? index.features : [];
133
+ const candidates = features
134
+ .map(normalizeGeofabrikFeature)
135
+ .filter(Boolean)
136
+ .filter((feature) => bboxInsideFeature(bbox, feature))
137
+ .sort((a, b) => featureArea(a) - featureArea(b));
138
+
139
+ const selected = candidates[0];
140
+ if (!selected) {
141
+ throw new Error(`no Geofabrik extract fully contains bbox ${formatBbox(bbox)}`);
142
+ }
143
+ return selected;
144
+ }
145
+
146
+ async function loadGeofabrikIndex(cacheDir, indexUrl) {
147
+ await fs.mkdir(cacheDir, { recursive: true });
148
+ const indexPath = join(cacheDir, 'geofabrik-index-v1.json');
149
+ const cached = await fs.readFile(indexPath, 'utf8').catch(() => null);
150
+ if (cached) {
151
+ return JSON.parse(cached);
152
+ }
153
+
154
+ const response = await fetch(indexUrl);
155
+ if (!response.ok) {
156
+ throw new Error(`failed to download Geofabrik index: HTTP ${response.status}`);
157
+ }
158
+ const text = await response.text();
159
+ await fs.writeFile(indexPath, text);
160
+ return JSON.parse(text);
161
+ }
162
+
163
+ function normalizeGeofabrikFeature(feature) {
164
+ const properties = feature?.properties && typeof feature.properties === 'object' ? feature.properties : {};
165
+ const urls = properties.urls && typeof properties.urls === 'object' ? properties.urls : {};
166
+ const url = typeof urls.pbf === 'string' ? urls.pbf : null;
167
+ const id = typeof properties.id === 'string' ? properties.id : null;
168
+ const name = typeof properties.name === 'string' ? properties.name : id;
169
+ if (!url || !id || !name || !feature?.geometry) {
170
+ return null;
171
+ }
172
+ return {
173
+ id,
174
+ name,
175
+ url,
176
+ geometry: feature.geometry,
177
+ bbox: geometryBbox(feature.geometry)
178
+ };
179
+ }
180
+
181
+ async function downloadExtract(provider, options) {
182
+ await fs.mkdir(options.cacheDir, { recursive: true });
183
+ const fileName = safeFileName(basename(new URL(provider.url).pathname) || `${provider.id}.osm.pbf`);
184
+ const filePath = join(options.cacheDir, fileName);
185
+ const stat = await fs.stat(filePath).catch(() => null);
186
+ if (stat?.isFile() && stat.size > 0 && !options.forceDownload) {
187
+ options.onStage?.(`Using cached extract: ${filePath}`);
188
+ return filePath;
189
+ }
190
+
191
+ options.onStage?.(`Downloading ${provider.url}`);
192
+ const response = await fetch(provider.url);
193
+ if (!response.ok || !response.body) {
194
+ throw new Error(`failed to download OSM extract: HTTP ${response.status}`);
195
+ }
196
+
197
+ const tempPath = `${filePath}.part`;
198
+ const stream = createWriteStream(tempPath);
199
+ for await (const chunk of response.body) {
200
+ if (!stream.write(Buffer.from(chunk))) {
201
+ await once(stream, 'drain');
202
+ }
203
+ }
204
+ stream.end();
205
+ await once(stream, 'finish');
206
+ await fs.rename(tempPath, filePath);
207
+ return filePath;
208
+ }
209
+
210
+ function bboxInsideFeature(bbox, feature) {
211
+ if (feature.bbox && !bboxInsideBbox(bbox, feature.bbox)) {
212
+ return false;
213
+ }
214
+ return bboxCorners(bbox).every((point) => pointInGeometry(point, feature.geometry));
215
+ }
216
+
217
+ function bboxInsideBbox(inner, outer) {
218
+ return inner[0] >= outer[0] &&
219
+ inner[1] >= outer[1] &&
220
+ inner[2] <= outer[2] &&
221
+ inner[3] <= outer[3];
222
+ }
223
+
224
+ function bboxCorners(bbox) {
225
+ return [
226
+ [bbox[0], bbox[1]],
227
+ [bbox[0], bbox[3]],
228
+ [bbox[2], bbox[1]],
229
+ [bbox[2], bbox[3]]
230
+ ];
231
+ }
232
+
233
+ function pointInGeometry(point, geometry) {
234
+ if (geometry?.type === 'Polygon') {
235
+ return pointInPolygon(point, geometry.coordinates);
236
+ }
237
+ if (geometry?.type === 'MultiPolygon') {
238
+ return geometry.coordinates.some((polygon) => pointInPolygon(point, polygon));
239
+ }
240
+ return false;
241
+ }
242
+
243
+ function pointInPolygon(point, rings) {
244
+ if (!Array.isArray(rings) || rings.length === 0) {
245
+ return false;
246
+ }
247
+ if (!pointInRing(point, rings[0])) {
248
+ return false;
249
+ }
250
+ return rings.slice(1).every((hole) => !pointInRing(point, hole));
251
+ }
252
+
253
+ function pointInRing(point, ring) {
254
+ let inside = false;
255
+ const x = point[0];
256
+ const y = point[1];
257
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
258
+ const xi = Number(ring[i]?.[0]);
259
+ const yi = Number(ring[i]?.[1]);
260
+ const xj = Number(ring[j]?.[0]);
261
+ const yj = Number(ring[j]?.[1]);
262
+ if (pointOnSegment(x, y, xi, yi, xj, yj)) {
263
+ return true;
264
+ }
265
+ const intersects = ((yi > y) !== (yj > y)) &&
266
+ x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
267
+ if (intersects) inside = !inside;
268
+ }
269
+ return inside;
270
+ }
271
+
272
+ function pointOnSegment(x, y, x1, y1, x2, y2) {
273
+ const cross = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1);
274
+ if (Math.abs(cross) > 1e-10) return false;
275
+ const dot = (x - x1) * (x2 - x1) + (y - y1) * (y2 - y1);
276
+ if (dot < 0) return false;
277
+ const lengthSquared = (x2 - x1) ** 2 + (y2 - y1) ** 2;
278
+ return dot <= lengthSquared;
279
+ }
280
+
281
+ function geometryBbox(geometry) {
282
+ const points = [];
283
+ collectGeometryPoints(geometry, points);
284
+ if (points.length === 0) return null;
285
+ let minLon = Infinity;
286
+ let minLat = Infinity;
287
+ let maxLon = -Infinity;
288
+ let maxLat = -Infinity;
289
+ for (const point of points) {
290
+ minLon = Math.min(minLon, point[0]);
291
+ minLat = Math.min(minLat, point[1]);
292
+ maxLon = Math.max(maxLon, point[0]);
293
+ maxLat = Math.max(maxLat, point[1]);
294
+ }
295
+ return [minLon, minLat, maxLon, maxLat];
296
+ }
297
+
298
+ function collectGeometryPoints(geometry, points) {
299
+ if (geometry?.type === 'Polygon') {
300
+ for (const ring of geometry.coordinates ?? []) {
301
+ collectRingPoints(ring, points);
302
+ }
303
+ }
304
+ if (geometry?.type === 'MultiPolygon') {
305
+ for (const polygon of geometry.coordinates ?? []) {
306
+ for (const ring of polygon) {
307
+ collectRingPoints(ring, points);
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ function collectRingPoints(ring, points) {
314
+ for (const point of ring ?? []) {
315
+ const lon = Number(point?.[0]);
316
+ const lat = Number(point?.[1]);
317
+ if (Number.isFinite(lon) && Number.isFinite(lat)) {
318
+ points.push([lon, lat]);
319
+ }
320
+ }
321
+ }
322
+
323
+ function featureArea(feature) {
324
+ const bbox = feature.bbox;
325
+ if (!bbox) return Infinity;
326
+ return Math.abs((bbox[2] - bbox[0]) * (bbox[3] - bbox[1]));
327
+ }
328
+
329
+ function safeFileName(value) {
330
+ return value.replace(/[^A-Za-z0-9._-]/g, '_');
331
+ }
332
+
333
+ function formatBbox(bbox) {
334
+ return bbox.map((value) => Number(value.toFixed(7))).join(',');
335
+ }
package/src/gpkg-read.js CHANGED
@@ -24,7 +24,7 @@ const LAYER_ALIASES = {
24
24
  * bbox?: [number, number, number, number],
25
25
  * data?: string,
26
26
  * styles?: Record<string, string>,
27
- * layers?: ManifestLayer[]
27
+ * layers?: string[]
28
28
  * }} Manifest
29
29
  *
30
30
  * @typedef {Map<string, Map<string, Set<string>>>} HiddenFilters
@@ -64,7 +64,8 @@ export function openGeoPackageReader(options) {
64
64
  const statementCache = new Map();
65
65
 
66
66
  const layerById = new Map();
67
- for (const layer of options.manifest.layers ?? []) {
67
+ for (const layerId of options.manifest.layers ?? []) {
68
+ const layer = manifestLayer(String(layerId));
68
69
  layerById.set(layer.id, layer);
69
70
  const alias = LAYER_ALIASES[layer.id];
70
71
  if (alias && !layerById.has(alias)) {
@@ -132,6 +133,18 @@ export function openGeoPackageReader(options) {
132
133
  };
133
134
  }
134
135
 
136
+ /**
137
+ * @param {string} layerId
138
+ * @returns {ManifestLayer}
139
+ */
140
+ function manifestLayer(layerId) {
141
+ return {
142
+ id: layerId,
143
+ table: layerId,
144
+ style: layerId
145
+ };
146
+ }
147
+
135
148
  /**
136
149
  * @param {Database.Database} db
137
150
  */
package/src/html.js CHANGED
@@ -138,8 +138,6 @@ export function createViewerHtml(options = {}) {
138
138
  titleEl.textContent = manifest.name || 'map-zero';
139
139
 
140
140
  const bbox = normalizeBbox(manifest.bbox);
141
- const tiles3dBbox = normalizeBbox(manifest.tiles3d?.bbox);
142
- const focusBbox = normalizeBbox(manifest.tiles3d?.focusBbox);
143
141
  const initialView = readInitialView(bbox);
144
142
  const map = new OLMap({
145
143
  target: 'map',
@@ -171,15 +169,15 @@ export function createViewerHtml(options = {}) {
171
169
 
172
170
  document.body.style.background = controller.style.background || '#000000';
173
171
 
174
- for (const layer of controller.manifest.layers || []) {
175
- addLayerToggle(layer, controller);
172
+ for (const layerId of controller.manifest.layers || []) {
173
+ addLayerToggle(String(layerId), controller);
176
174
  }
177
175
 
178
176
  updateStatus();
179
177
  }
180
178
 
181
- function addLayerToggle(layer, controller) {
182
- const rule = controller.style.layers?.[layer.style || layer.id] || {};
179
+ function addLayerToggle(layerId, controller) {
180
+ const rule = controller.style.layers?.[layerId] || {};
183
181
  const label = document.createElement('label');
184
182
  label.className = 'layer-row';
185
183
 
@@ -187,15 +185,15 @@ export function createViewerHtml(options = {}) {
187
185
  input.type = 'checkbox';
188
186
  input.checked = rule.visible !== false;
189
187
  input.addEventListener('change', () => {
190
- controller.setVisible(layer.id, input.checked);
191
- layerStatus.set(layer.id, input.checked ? 'visible' : 'hidden');
188
+ controller.setVisible(layerId, input.checked);
189
+ layerStatus.set(layerId, input.checked ? 'visible' : 'hidden');
192
190
  updateStatus();
193
191
  });
194
192
 
195
193
  const text = document.createElement('span');
196
- text.textContent = layer.id;
194
+ text.textContent = layerId;
197
195
 
198
- layerStatus.set(layer.id, input.checked ? 'visible' : 'hidden');
196
+ layerStatus.set(layerId, input.checked ? 'visible' : 'hidden');
199
197
  label.append(input, text);
200
198
  layersEl.append(label);
201
199
  }
@@ -291,6 +289,15 @@ export function createCesiumViewerHtml(options = {}) {
291
289
  <title>map-zero Cesium</title>
292
290
  <script src="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Cesium.js"></script>
293
291
  <link rel="stylesheet" href="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Widgets/widgets.css">
292
+ <script type="importmap">
293
+ {
294
+ "imports": {
295
+ "ol/": "https://esm.sh/ol@10.9.0/",
296
+ "pmtiles": "/vendor/pmtiles.js",
297
+ "fflate": "/vendor/fflate.js"
298
+ }
299
+ }
300
+ </script>
294
301
  <style>
295
302
  html,
296
303
  body {
@@ -413,6 +420,7 @@ export function createCesiumViewerHtml(options = {}) {
413
420
  const titleEl = document.getElementById('title');
414
421
  const layerControlsEl = document.getElementById('layerControls');
415
422
  const opacityEl = document.getElementById('layerOpacity');
423
+ const params = new URLSearchParams(globalThis.location.search);
416
424
 
417
425
  start().catch((error) => {
418
426
  statusEl.textContent = error.message;
@@ -426,17 +434,16 @@ export function createCesiumViewerHtml(options = {}) {
426
434
  statusEl.textContent = 'Loading manifest';
427
435
  const manifest = await loadMapZeroManifest('/manifest.json');
428
436
  titleEl.textContent = (manifest.name || 'map-zero') + ' 3D';
429
- if (!manifest.tiles3d && !manifest.cesium?.tilesets) {
437
+ if (!manifest.tiles3d) {
430
438
  throw new Error('This package does not define manifest.tiles3d. Run: map-zero 3dtiles <package.mapzero>');
431
439
  }
432
- const sourceLabel = manifest.tiles3d?.url || manifest.cesium?.tilesets?.buildings || '3dtiles';
440
+ const sourceLabel = manifest.tiles3d?.url || '3dtiles';
433
441
 
434
442
  statusEl.textContent = 'Creating Cesium viewer';
435
443
  const Cesium = globalThis.Cesium;
436
444
  Cesium.Ion.defaultAccessToken = '';
437
445
  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);
446
+ const tiles3dBbox = normalizeBbox(manifest.tiles3d?.bbox || manifest.bbox);
440
447
  const viewer = new Cesium.Viewer('cesiumContainer', {
441
448
  animation: false,
442
449
  baseLayer: false,
@@ -451,21 +458,25 @@ export function createCesiumViewerHtml(options = {}) {
451
458
  timeline: false,
452
459
  terrainProvider: new Cesium.EllipsoidTerrainProvider()
453
460
  });
461
+ globalThis.viewer = viewer;
454
462
  viewer.imageryLayers.removeAll();
455
463
 
456
464
  statusEl.textContent = 'Loading 3D Tileset';
457
465
  const controller = await addMapZeroToCesium(viewer, {
458
466
  manifestUrl: '/manifest.json',
459
- style: 'cesium',
467
+ style: 'default',
460
468
  opacity: Number(opacityEl.value),
469
+ contextOverlay: params.get('overlay') !== '0',
470
+ contextOverzoomLevels: params.has('overzoom') ? Number(params.get('overzoom')) : undefined,
461
471
  zoomTo: false,
462
472
  applyDefaultSceneStyle: true
463
473
  });
474
+ globalThis.mapZeroController = controller;
464
475
 
465
476
  statusEl.textContent = 'Positioning camera';
466
477
  const firstTileset = Object.values(controller.tilesets)[0];
467
- if (focusBbox || tiles3dBbox) {
468
- const targetBbox = focusBbox || tiles3dBbox;
478
+ if (tiles3dBbox) {
479
+ const targetBbox = tiles3dBbox;
469
480
  const centerLon = (targetBbox[0] + targetBbox[2]) / 2;
470
481
  const centerLat = (targetBbox[1] + targetBbox[3]) / 2;
471
482
  const spanMeters = bboxDiagonalMeters(targetBbox);
package/src/manifest.js CHANGED
@@ -1,4 +1,3 @@
1
- import { LAYER_DEFINITIONS } from './layers.js';
2
1
  import { packageNameFromPath } from './utils.js';
3
2
 
4
3
  /**
@@ -18,12 +17,6 @@ export function createManifest(options) {
18
17
  default: 'styles/neon-dark.json',
19
18
  'neon-dark': 'styles/neon-dark.json'
20
19
  },
21
- layers: options.layers.map((layer) => ({
22
- id: layer,
23
- type: LAYER_DEFINITIONS[layer].type,
24
- source: 'data.gpkg',
25
- table: layer,
26
- style: layer
27
- }))
20
+ layers: options.layers
28
21
  };
29
22
  }
package/src/mvt.js CHANGED
@@ -2,6 +2,7 @@ import geojsonvt from 'geojson-vt';
2
2
  import vtpbf from 'vt-pbf';
3
3
 
4
4
  const TILE_EXTENT = 4096;
5
+ const TILE_QUERY_BUFFER_UNITS = 128;
5
6
  const MAX_ZOOM = 22;
6
7
  const DEFAULT_MAX_FEATURES = 12000;
7
8
  const TILE_DETAIL_LEVELS = new Set(['overview', 'normal', 'full']);
@@ -204,9 +205,10 @@ export function encodeMvtTile(reader, layerId, zValue, xValue, yValue, options =
204
205
  export function encodeMvtTileWithStats(reader, layerId, zValue, xValue, yValue, options = {}) {
205
206
  const { z, x, y } = parseTileParams(zValue, xValue, yValue);
206
207
  const bbox = tileToBbox(z, x, y);
208
+ const queryBbox = tileQueryBbox(z, x, y);
207
209
  const detail = normalizeDetail(options.detail, z);
208
210
  validateRequestedLayers(reader.getLayers(), new Set([layerId]));
209
- const layerFeatures = readRequestedLayerFeatures(reader, layerId, bbox, z, options.style ?? null, Boolean(options.debugLabels));
211
+ const layerFeatures = readRequestedLayerFeatures(reader, layerId, queryBbox, z, options.style ?? null, Boolean(options.debugLabels));
210
212
  const limited = applyFeatureLimit([layerFeatures], maxFeaturesForZoom(z, options.maxFeatures), z);
211
213
  const layerTile = createLayerTile(limited.layers[0]?.features ?? [], bbox, z, x, y, layerId, detail);
212
214
  const tile = layerTile.tile;
@@ -267,6 +269,7 @@ export function encodeMvtTileSet(reader, zValue, xValue, yValue, layerIds, optio
267
269
  export function encodeMvtTileSetWithStats(reader, zValue, xValue, yValue, layerIds, options = {}) {
268
270
  const { z, x, y } = parseTileParams(zValue, xValue, yValue);
269
271
  const bbox = tileToBbox(z, x, y);
272
+ const queryBbox = tileQueryBbox(z, x, y);
270
273
  const detail = normalizeDetail(options.detail, z);
271
274
  /** @type {Record<string, { features: unknown[] }>} */
272
275
  const layers = {};
@@ -293,7 +296,7 @@ export function encodeMvtTileSetWithStats(reader, zValue, xValue, yValue, layerI
293
296
  }
294
297
  }
295
298
 
296
- const layerFeatures = readRequestedLayerFeatures(reader, layerId, bbox, z, options.style ?? null, Boolean(options.debugLabels));
299
+ const layerFeatures = readRequestedLayerFeatures(reader, layerId, queryBbox, z, options.style ?? null, Boolean(options.debugLabels));
297
300
  originalFeatureCount += layerFeatures.originalFeatureCount;
298
301
  layerFeatureBatches.push(layerFeatures);
299
302
  }
@@ -2539,7 +2542,7 @@ function createTileIndex(features, z) {
2539
2542
  extent: TILE_EXTENT,
2540
2543
  maxZoom: z,
2541
2544
  indexMaxZoom: z,
2542
- buffer: 64,
2545
+ buffer: TILE_QUERY_BUFFER_UNITS,
2543
2546
  // Geometries are already simplified in lon/lat before indexing.
2544
2547
  // Keep geojson-vt from applying a second aggressive simplification pass
2545
2548
  // that can erase small-but-valid low-zoom tile content.
@@ -2548,6 +2551,35 @@ function createTileIndex(features, z) {
2548
2551
  );
2549
2552
  }
2550
2553
 
2554
+ /**
2555
+ * Return the feature-query bbox for a tile, expanded by the same edge buffer
2556
+ * used during MVT clipping. Encoding still uses the exact tile bbox.
2557
+ *
2558
+ * @param {number} z
2559
+ * @param {number} x
2560
+ * @param {number} y
2561
+ * @returns {[number, number, number, number]}
2562
+ */
2563
+ function tileQueryBbox(z, x, y) {
2564
+ return expandBboxByTileUnits(tileToBbox(z, x, y), TILE_QUERY_BUFFER_UNITS);
2565
+ }
2566
+
2567
+ /**
2568
+ * @param {[number, number, number, number]} bbox
2569
+ * @param {number} units
2570
+ * @returns {[number, number, number, number]}
2571
+ */
2572
+ function expandBboxByTileUnits(bbox, units) {
2573
+ const xMargin = ((bbox[2] - bbox[0]) * units) / TILE_EXTENT;
2574
+ const yMargin = ((bbox[3] - bbox[1]) * units) / TILE_EXTENT;
2575
+ return [
2576
+ Math.max(-180, bbox[0] - xMargin),
2577
+ Math.max(-85.05112878, bbox[1] - yMargin),
2578
+ Math.min(180, bbox[2] + xMargin),
2579
+ Math.min(85.05112878, bbox[3] + yMargin)
2580
+ ];
2581
+ }
2582
+
2551
2583
  /**
2552
2584
  * @param {string | undefined} detail
2553
2585
  * @param {number} z