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.
- package/CHANGELOG.md +14 -0
- package/README.md +31 -2
- package/docs/cesium.md +24 -9
- package/package.json +4 -3
- package/packages/cesium/package.json +6 -3
- package/packages/cesium/src/imagery-worker.js +604 -0
- package/packages/cesium/src/imagery.js +434 -0
- package/packages/cesium/src/index.js +199 -35
- package/packages/ol/package.json +1 -1
- package/packages/ol/src/index.js +349 -16
- package/src/3dtiles/b3dm.js +18 -2
- package/src/3dtiles/clipper-surfaces.js +121 -20
- package/src/3dtiles/export.js +298 -25
- package/src/3dtiles/extrude.js +78 -23
- package/src/3dtiles/flat.js +8 -20
- package/src/3dtiles/glb.js +78 -27
- package/src/3dtiles/gpkg-features.js +4 -4
- package/src/3dtiles/precision.js +47 -0
- package/src/cli.js +100 -2
- package/src/export-pmtiles.js +17 -16
- package/src/from-bbox.js +335 -0
- package/src/gpkg-read.js +15 -2
- package/src/html.js +28 -17
- package/src/manifest.js +1 -8
- package/src/mvt.js +35 -3
- package/src/package.js +343 -0
- package/src/server.js +38 -10
- package/src/style-command.js +1 -1
- package/src/style-filters.js +2 -3
- package/styles/presets/neon-dark-3d.json +0 -90
package/src/export-pmtiles.js
CHANGED
|
@@ -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
|
|
750
|
-
const
|
|
751
|
-
const
|
|
752
|
-
const
|
|
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 {
|
|
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
|
|
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 {
|
|
836
|
+
* @param {string} layerId
|
|
836
837
|
* @returns {Record<string, unknown>}
|
|
837
838
|
*/
|
|
838
|
-
function layerStyleRule(style,
|
|
839
|
+
function layerStyleRule(style, layerId) {
|
|
839
840
|
const styleLayers = /** @type {Record<string, unknown> | undefined} */ (style?.layers);
|
|
840
|
-
const rule = /** @type {Record<string, unknown>} */ (styleLayers?.[
|
|
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 {
|
|
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((
|
|
918
|
-
id:
|
|
919
|
-
fields: fieldsForLayer(style,
|
|
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 {
|
|
927
|
+
* @param {string} layerId
|
|
927
928
|
* @returns {Record<string, string>}
|
|
928
929
|
*/
|
|
929
|
-
function fieldsForLayer(style,
|
|
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,
|
|
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';
|
package/src/from-bbox.js
ADDED
|
@@ -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?:
|
|
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
|
|
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
|
|
175
|
-
addLayerToggle(
|
|
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(
|
|
182
|
-
const rule = controller.style.layers?.[
|
|
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(
|
|
191
|
-
layerStatus.set(
|
|
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 =
|
|
194
|
+
text.textContent = layerId;
|
|
197
195
|
|
|
198
|
-
layerStatus.set(
|
|
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
|
|
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 ||
|
|
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.
|
|
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: '
|
|
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 (
|
|
468
|
-
const targetBbox =
|
|
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
|
|
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,
|
|
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,
|
|
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:
|
|
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
|