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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap a GLB buffer in a minimal valid B3DM container.
|
|
3
|
+
*
|
|
4
|
+
* @param {Buffer} glb
|
|
5
|
+
* @returns {Buffer}
|
|
6
|
+
*/
|
|
7
|
+
export function buildB3dm(glb) {
|
|
8
|
+
const featureTableJson = padJsonForSection({ BATCH_LENGTH: 0 }, 28);
|
|
9
|
+
const header = Buffer.alloc(28);
|
|
10
|
+
header.write('b3dm', 0, 4, 'ascii');
|
|
11
|
+
header.writeUInt32LE(1, 4);
|
|
12
|
+
header.writeUInt32LE(28 + featureTableJson.length + glb.length, 8);
|
|
13
|
+
header.writeUInt32LE(featureTableJson.length, 12);
|
|
14
|
+
header.writeUInt32LE(0, 16);
|
|
15
|
+
header.writeUInt32LE(0, 20);
|
|
16
|
+
header.writeUInt32LE(0, 24);
|
|
17
|
+
return Buffer.concat([header, featureTableJson, glb]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {unknown} value
|
|
22
|
+
* @param {number} sectionOffset
|
|
23
|
+
* @returns {Buffer}
|
|
24
|
+
*/
|
|
25
|
+
function padJsonForSection(value, sectionOffset) {
|
|
26
|
+
const buffer = Buffer.from(JSON.stringify(value), 'utf8');
|
|
27
|
+
const targetLength = align(sectionOffset + buffer.length, 8) - sectionOffset;
|
|
28
|
+
return Buffer.concat([buffer, Buffer.alloc(targetLength - buffer.length, 0x20)]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} value
|
|
33
|
+
* @param {number} alignment
|
|
34
|
+
* @returns {number}
|
|
35
|
+
*/
|
|
36
|
+
function align(value, alignment) {
|
|
37
|
+
return Math.ceil(value / alignment) * alignment;
|
|
38
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import earcut from 'earcut';
|
|
2
|
+
import {
|
|
3
|
+
EndType,
|
|
4
|
+
JoinType,
|
|
5
|
+
NativeClipperLibRequestedFormat,
|
|
6
|
+
loadNativeClipperLibInstanceAsync
|
|
7
|
+
} from 'js-angusj-clipper';
|
|
8
|
+
|
|
9
|
+
import { wgs84SurfaceNormal, wgs84ToEcef } from './extrude.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SCALE = 100;
|
|
12
|
+
const DEFAULT_ARC_TOLERANCE_METERS = 0.25;
|
|
13
|
+
const DEFAULT_CLEAN_DISTANCE_METERS = 0.03;
|
|
14
|
+
let clipperPromise;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build a flat 3D mesh from linework using Clipper's open-path offsetter.
|
|
18
|
+
*
|
|
19
|
+
* This is intended to replace custom ribbon joins/caps for exported Cesium
|
|
20
|
+
* cartography. It produces dissolved polygonal road surfaces that can be
|
|
21
|
+
* triangulated and written to 3D Tiles.
|
|
22
|
+
*
|
|
23
|
+
* @param {Array<Array<[number, number]>>} lines
|
|
24
|
+
* @param {{
|
|
25
|
+
* widthMeters: number,
|
|
26
|
+
* height?: number,
|
|
27
|
+
* scale?: number,
|
|
28
|
+
* arcToleranceMeters?: number,
|
|
29
|
+
* cleanDistanceMeters?: number,
|
|
30
|
+
* minSegmentMeters?: number,
|
|
31
|
+
* miterLimit?: number
|
|
32
|
+
* }} options
|
|
33
|
+
* @returns {Promise<{
|
|
34
|
+
* positions: Float32Array,
|
|
35
|
+
* normals: Float32Array,
|
|
36
|
+
* indices: Uint16Array,
|
|
37
|
+
* min: [number, number, number],
|
|
38
|
+
* max: [number, number, number],
|
|
39
|
+
* bbox: [number, number, number, number],
|
|
40
|
+
* maxHeight: number,
|
|
41
|
+
* featureCount: number
|
|
42
|
+
* } | null>}
|
|
43
|
+
*/
|
|
44
|
+
export async function buildClipperLineSurfaceMesh(lines, options) {
|
|
45
|
+
const cleanLines = lines.map(cleanLine).filter((line) => line.length >= 2);
|
|
46
|
+
if (cleanLines.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
const clipper = await getClipper();
|
|
49
|
+
const scale = positiveNumber(options.scale, DEFAULT_SCALE);
|
|
50
|
+
const projection = createLocalProjection(cleanLines.flat());
|
|
51
|
+
const openPaths = [];
|
|
52
|
+
const closedPaths = [];
|
|
53
|
+
const minSegmentMeters = positiveNumber(options.minSegmentMeters, 0.05);
|
|
54
|
+
|
|
55
|
+
for (const line of cleanLines) {
|
|
56
|
+
const local = removeRedundantLocalPoints(line.map((point) => projectPoint(point, projection)), minSegmentMeters);
|
|
57
|
+
if (local.length < 2) continue;
|
|
58
|
+
const closed = local.length > 2 && samePoint(local[0], local[local.length - 1]);
|
|
59
|
+
const path = local.slice(0, closed ? -1 : undefined).map((point) => toIntPoint(point, scale));
|
|
60
|
+
if (path.length < (closed ? 3 : 2)) continue;
|
|
61
|
+
if (closed) closedPaths.push(path);
|
|
62
|
+
else openPaths.push(path);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const offsetInputs = [];
|
|
66
|
+
if (openPaths.length > 0) {
|
|
67
|
+
offsetInputs.push({
|
|
68
|
+
data: openPaths,
|
|
69
|
+
joinType: JoinType.Round,
|
|
70
|
+
endType: EndType.OpenRound
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (closedPaths.length > 0) {
|
|
74
|
+
offsetInputs.push({
|
|
75
|
+
data: closedPaths,
|
|
76
|
+
joinType: JoinType.Round,
|
|
77
|
+
endType: EndType.ClosedLine
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (offsetInputs.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
const offsetTree = clipper.offsetToPolyTree({
|
|
83
|
+
delta: options.widthMeters * scale / 2,
|
|
84
|
+
arcTolerance: positiveNumber(options.arcToleranceMeters, DEFAULT_ARC_TOLERANCE_METERS) * scale,
|
|
85
|
+
miterLimit: positiveNumber(options.miterLimit, 2),
|
|
86
|
+
offsetInputs
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!offsetTree || offsetTree.total === 0) return null;
|
|
90
|
+
|
|
91
|
+
const polygons = collectPolyTreePolygons(offsetTree);
|
|
92
|
+
return buildSurfaceMeshFromClipperPolygons(polygons, projection, scale, options.height ?? 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getClipper() {
|
|
96
|
+
clipperPromise ??= loadNativeClipperLibInstanceAsync(
|
|
97
|
+
NativeClipperLibRequestedFormat.WasmWithAsmJsFallback
|
|
98
|
+
);
|
|
99
|
+
return clipperPromise;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collectPolyTreePolygons(polyTree) {
|
|
103
|
+
const polygons = [];
|
|
104
|
+
for (const child of polyTree.childs) {
|
|
105
|
+
collectNode(child, polygons);
|
|
106
|
+
}
|
|
107
|
+
return polygons;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function collectNode(node, polygons) {
|
|
111
|
+
if (node.isOpen) return;
|
|
112
|
+
if (!node.isHole) {
|
|
113
|
+
const holes = node.childs.filter((child) => child.isHole && !child.isOpen);
|
|
114
|
+
polygons.push([node.contour, ...holes.map((child) => child.contour)]);
|
|
115
|
+
for (const hole of holes) {
|
|
116
|
+
for (const nested of hole.childs) {
|
|
117
|
+
collectNode(nested, polygons);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
for (const child of node.childs) {
|
|
122
|
+
collectNode(child, polygons);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildSurfaceMeshFromClipperPolygons(polygons, projection, scale, height) {
|
|
128
|
+
const positions = [];
|
|
129
|
+
const normals = [];
|
|
130
|
+
const indices = [];
|
|
131
|
+
const bboxes = [];
|
|
132
|
+
let featureCount = 0;
|
|
133
|
+
|
|
134
|
+
for (const polygon of polygons) {
|
|
135
|
+
const vertices = [];
|
|
136
|
+
const holes = [];
|
|
137
|
+
const localPoints = [];
|
|
138
|
+
let cursor = 0;
|
|
139
|
+
|
|
140
|
+
for (let ringIndex = 0; ringIndex < polygon.length; ringIndex++) {
|
|
141
|
+
const ring = cleanPathRing(polygon[ringIndex], scale);
|
|
142
|
+
if (ring.length < 3) continue;
|
|
143
|
+
if (ringIndex > 0) holes.push(cursor);
|
|
144
|
+
for (const point of ring) {
|
|
145
|
+
vertices.push(point[0], point[1]);
|
|
146
|
+
localPoints.push(point);
|
|
147
|
+
cursor++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (vertices.length < 6) continue;
|
|
152
|
+
const triangles = earcut(vertices, holes, 2);
|
|
153
|
+
if (triangles.length === 0) continue;
|
|
154
|
+
|
|
155
|
+
const lonLatPoints = localPoints.map((point) => unprojectPoint(point, projection));
|
|
156
|
+
const centroid = polygonCentroid(lonLatPoints);
|
|
157
|
+
const normal = wgs84SurfaceNormal(centroid[0], centroid[1]);
|
|
158
|
+
const ecef = lonLatPoints.map(([lon, lat]) => wgs84ToEcef(lon, lat, height));
|
|
159
|
+
const vertexOffset = positions.length / 3;
|
|
160
|
+
|
|
161
|
+
for (const point of ecef) {
|
|
162
|
+
positions.push(...point);
|
|
163
|
+
normals.push(...normal);
|
|
164
|
+
}
|
|
165
|
+
for (const index of triangles) {
|
|
166
|
+
indices.push(vertexOffset + index);
|
|
167
|
+
}
|
|
168
|
+
bboxes.push(lineBbox(lonLatPoints));
|
|
169
|
+
featureCount++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (positions.length === 0 || featureCount === 0) return null;
|
|
173
|
+
const positionArray = new Float32Array(positions);
|
|
174
|
+
const normalArray = new Float32Array(normals);
|
|
175
|
+
const indexArray = positionArray.length / 3 > 65535
|
|
176
|
+
? new Uint32Array(indices)
|
|
177
|
+
: new Uint16Array(indices);
|
|
178
|
+
const bounds = minMaxVec3(positionArray);
|
|
179
|
+
return {
|
|
180
|
+
positions: positionArray,
|
|
181
|
+
normals: normalArray,
|
|
182
|
+
indices: indexArray,
|
|
183
|
+
min: bounds.min,
|
|
184
|
+
max: bounds.max,
|
|
185
|
+
bbox: mergeBboxes(bboxes),
|
|
186
|
+
maxHeight: height,
|
|
187
|
+
featureCount
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cleanPathRing(path, scale) {
|
|
192
|
+
const ring = [];
|
|
193
|
+
for (const point of path) {
|
|
194
|
+
const local = [point.x / scale, point.y / scale];
|
|
195
|
+
const last = ring[ring.length - 1];
|
|
196
|
+
if (!last || !samePoint(last, local)) {
|
|
197
|
+
ring.push(local);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (ring.length > 1 && samePoint(ring[0], ring[ring.length - 1])) {
|
|
201
|
+
ring.pop();
|
|
202
|
+
}
|
|
203
|
+
return ring;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function cleanLine(line) {
|
|
207
|
+
return line
|
|
208
|
+
.map((point) => [Number(point?.[0]), Number(point?.[1])])
|
|
209
|
+
.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function removeRedundantLocalPoints(points, minDistance) {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (const point of points) {
|
|
215
|
+
const last = out[out.length - 1];
|
|
216
|
+
if (!last || distance2(last, point) >= minDistance) {
|
|
217
|
+
out.push(point);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function createLocalProjection(points) {
|
|
224
|
+
const origin = polygonCentroid(points);
|
|
225
|
+
const meanLat = origin[1] * Math.PI / 180;
|
|
226
|
+
return {
|
|
227
|
+
origin,
|
|
228
|
+
metersPerLon: Math.max(1, 111320 * Math.cos(meanLat)),
|
|
229
|
+
metersPerLat: 110540
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function projectPoint(point, projection) {
|
|
234
|
+
return [
|
|
235
|
+
(point[0] - projection.origin[0]) * projection.metersPerLon,
|
|
236
|
+
(point[1] - projection.origin[1]) * projection.metersPerLat
|
|
237
|
+
];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function unprojectPoint(point, projection) {
|
|
241
|
+
return [
|
|
242
|
+
projection.origin[0] + point[0] / projection.metersPerLon,
|
|
243
|
+
projection.origin[1] + point[1] / projection.metersPerLat
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toIntPoint(point, scale) {
|
|
248
|
+
return {
|
|
249
|
+
x: Math.round(point[0] * scale),
|
|
250
|
+
y: Math.round(point[1] * scale)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function polygonCentroid(points) {
|
|
255
|
+
let lon = 0;
|
|
256
|
+
let lat = 0;
|
|
257
|
+
for (const point of points) {
|
|
258
|
+
lon += point[0];
|
|
259
|
+
lat += point[1];
|
|
260
|
+
}
|
|
261
|
+
return [lon / points.length, lat / points.length];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function lineBbox(points) {
|
|
265
|
+
let minLon = Infinity;
|
|
266
|
+
let minLat = Infinity;
|
|
267
|
+
let maxLon = -Infinity;
|
|
268
|
+
let maxLat = -Infinity;
|
|
269
|
+
for (const [lon, lat] of points) {
|
|
270
|
+
minLon = Math.min(minLon, lon);
|
|
271
|
+
minLat = Math.min(minLat, lat);
|
|
272
|
+
maxLon = Math.max(maxLon, lon);
|
|
273
|
+
maxLat = Math.max(maxLat, lat);
|
|
274
|
+
}
|
|
275
|
+
return [minLon, minLat, maxLon, maxLat];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function mergeBboxes(bboxes) {
|
|
279
|
+
let minLon = Infinity;
|
|
280
|
+
let minLat = Infinity;
|
|
281
|
+
let maxLon = -Infinity;
|
|
282
|
+
let maxLat = -Infinity;
|
|
283
|
+
for (const bbox of bboxes) {
|
|
284
|
+
minLon = Math.min(minLon, bbox[0]);
|
|
285
|
+
minLat = Math.min(minLat, bbox[1]);
|
|
286
|
+
maxLon = Math.max(maxLon, bbox[2]);
|
|
287
|
+
maxLat = Math.max(maxLat, bbox[3]);
|
|
288
|
+
}
|
|
289
|
+
return [minLon, minLat, maxLon, maxLat];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function minMaxVec3(positions) {
|
|
293
|
+
const min = [Infinity, Infinity, Infinity];
|
|
294
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
295
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
296
|
+
min[0] = Math.min(min[0], positions[i]);
|
|
297
|
+
min[1] = Math.min(min[1], positions[i + 1]);
|
|
298
|
+
min[2] = Math.min(min[2], positions[i + 2]);
|
|
299
|
+
max[0] = Math.max(max[0], positions[i]);
|
|
300
|
+
max[1] = Math.max(max[1], positions[i + 1]);
|
|
301
|
+
max[2] = Math.max(max[2], positions[i + 2]);
|
|
302
|
+
}
|
|
303
|
+
return { min, max };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function samePoint(a, b) {
|
|
307
|
+
return Math.abs(a[0] - b[0]) < 1e-9 && Math.abs(a[1] - b[1]) < 1e-9;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function distance2(a, b) {
|
|
311
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function positiveNumber(value, fallback) {
|
|
315
|
+
const number = Number(value);
|
|
316
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
317
|
+
}
|