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/gpkg.js
ADDED
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import Database from 'better-sqlite3';
|
|
4
|
+
|
|
5
|
+
import { LAYER_DEFINITIONS } from './layers.js';
|
|
6
|
+
import { geometryBbox, quoteIdentifier } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const SRS_ID = 4326;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {{ type: string, coordinates: unknown }} Geometry
|
|
12
|
+
* @typedef {{ geometry: Geometry, properties: Record<string, string | null>, bbox?: [number, number, number, number] }} Feature
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Write selected features into a GeoPackage.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} filePath
|
|
19
|
+
* @param {Record<string, Feature[]>} featuresByLayer
|
|
20
|
+
* @param {string[]} layers
|
|
21
|
+
* @param {[number, number, number, number]} packageBbox
|
|
22
|
+
*/
|
|
23
|
+
export function writeGeoPackage(filePath, featuresByLayer, layers, packageBbox) {
|
|
24
|
+
const writer = openGeoPackageWriter(filePath, layers, packageBbox);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
writer.transaction(() => {
|
|
28
|
+
for (const layer of layers) {
|
|
29
|
+
for (const feature of featuresByLayer[layer] ?? []) {
|
|
30
|
+
writer.insertFeature(layer, feature);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
} finally {
|
|
35
|
+
writer.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Open a GeoPackage writer that accepts features incrementally.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} filePath
|
|
43
|
+
* @param {string[]} layers
|
|
44
|
+
* @param {[number, number, number, number]} packageBbox
|
|
45
|
+
* @returns {{
|
|
46
|
+
* insertFeature: (layer: string, feature: Feature) => void,
|
|
47
|
+
* transaction: (fn: () => void) => void,
|
|
48
|
+
* counts: () => Record<string, number>,
|
|
49
|
+
* close: () => void
|
|
50
|
+
* }}
|
|
51
|
+
*/
|
|
52
|
+
export function openGeoPackageWriter(filePath, layers, packageBbox) {
|
|
53
|
+
if (existsSync(filePath)) {
|
|
54
|
+
unlinkSync(filePath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const db = new Database(filePath);
|
|
58
|
+
/** @type {Record<string, { insert: Database.Statement, spatialIndex: { insert: Database.Statement } | null, propertyColumns: string[] }>} */
|
|
59
|
+
const layerWriters = {};
|
|
60
|
+
/** @type {Record<string, number>} */
|
|
61
|
+
const featureCounts = Object.fromEntries(layers.map((layer) => [layer, 0]));
|
|
62
|
+
let closed = false;
|
|
63
|
+
|
|
64
|
+
db.pragma('journal_mode = WAL');
|
|
65
|
+
db.pragma('foreign_keys = ON');
|
|
66
|
+
db.pragma('application_id = 1196444487');
|
|
67
|
+
db.pragma('user_version = 10400');
|
|
68
|
+
|
|
69
|
+
db.transaction(() => {
|
|
70
|
+
createCoreTables(db);
|
|
71
|
+
|
|
72
|
+
for (const layer of layers) {
|
|
73
|
+
createFeatureTable(db, layer);
|
|
74
|
+
registerFeatureTable(db, layer, [], packageBbox);
|
|
75
|
+
layerWriters[layer] = createLayerWriter(db, layer);
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
insertFeature(layer, feature) {
|
|
81
|
+
const writer = layerWriters[layer];
|
|
82
|
+
if (!writer) {
|
|
83
|
+
throw new Error(`unknown GeoPackage layer: ${layer}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const values = [
|
|
87
|
+
encodeGeoPackageGeometry(feature.geometry),
|
|
88
|
+
...writer.propertyColumns.map((column) => feature.properties[column] ?? null)
|
|
89
|
+
];
|
|
90
|
+
const info = writer.insert.run(values);
|
|
91
|
+
featureCounts[layer] += 1;
|
|
92
|
+
|
|
93
|
+
if (writer.spatialIndex) {
|
|
94
|
+
const bbox = feature.bbox ?? geometryBbox(feature.geometry);
|
|
95
|
+
if (bbox) {
|
|
96
|
+
writer.spatialIndex.insert.run(info.lastInsertRowid, bbox[0], bbox[2], bbox[1], bbox[3]);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
transaction(fn) {
|
|
101
|
+
db.transaction(fn)();
|
|
102
|
+
},
|
|
103
|
+
counts() {
|
|
104
|
+
return { ...featureCounts };
|
|
105
|
+
},
|
|
106
|
+
close() {
|
|
107
|
+
if (closed) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const layer of layers) {
|
|
112
|
+
if (layerWriters[layer]?.spatialIndex) {
|
|
113
|
+
createSpatialIndexMetadata(db, layer);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
closed = true;
|
|
118
|
+
db.close();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @param {Database.Database} db
|
|
125
|
+
*/
|
|
126
|
+
function createCoreTables(db) {
|
|
127
|
+
db.exec(`
|
|
128
|
+
CREATE TABLE gpkg_spatial_ref_sys (
|
|
129
|
+
srs_name TEXT NOT NULL,
|
|
130
|
+
srs_id INTEGER NOT NULL PRIMARY KEY,
|
|
131
|
+
organization TEXT NOT NULL,
|
|
132
|
+
organization_coordsys_id INTEGER NOT NULL,
|
|
133
|
+
definition TEXT NOT NULL,
|
|
134
|
+
description TEXT
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
INSERT INTO gpkg_spatial_ref_sys (
|
|
138
|
+
srs_name,
|
|
139
|
+
srs_id,
|
|
140
|
+
organization,
|
|
141
|
+
organization_coordsys_id,
|
|
142
|
+
definition,
|
|
143
|
+
description
|
|
144
|
+
) VALUES
|
|
145
|
+
('Undefined Cartesian', -1, 'NONE', -1, 'undefined', 'undefined Cartesian coordinate reference system'),
|
|
146
|
+
('Undefined Geographic', 0, 'NONE', 0, 'undefined', 'undefined geographic coordinate reference system'),
|
|
147
|
+
(
|
|
148
|
+
'WGS 84 geodetic',
|
|
149
|
+
4326,
|
|
150
|
+
'EPSG',
|
|
151
|
+
4326,
|
|
152
|
+
'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]',
|
|
153
|
+
'longitude/latitude coordinates in decimal degrees on the WGS 84 datum'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
CREATE TABLE gpkg_contents (
|
|
157
|
+
table_name TEXT NOT NULL PRIMARY KEY,
|
|
158
|
+
data_type TEXT NOT NULL,
|
|
159
|
+
identifier TEXT UNIQUE,
|
|
160
|
+
description TEXT DEFAULT '',
|
|
161
|
+
last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
162
|
+
min_x DOUBLE,
|
|
163
|
+
min_y DOUBLE,
|
|
164
|
+
max_x DOUBLE,
|
|
165
|
+
max_y DOUBLE,
|
|
166
|
+
srs_id INTEGER,
|
|
167
|
+
CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
CREATE TABLE gpkg_geometry_columns (
|
|
171
|
+
table_name TEXT NOT NULL,
|
|
172
|
+
column_name TEXT NOT NULL,
|
|
173
|
+
geometry_type_name TEXT NOT NULL,
|
|
174
|
+
srs_id INTEGER NOT NULL,
|
|
175
|
+
z TINYINT NOT NULL,
|
|
176
|
+
m TINYINT NOT NULL,
|
|
177
|
+
PRIMARY KEY (table_name, column_name),
|
|
178
|
+
CONSTRAINT fk_ggc_tn FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name),
|
|
179
|
+
CONSTRAINT fk_ggc_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)
|
|
180
|
+
);
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {Database.Database} db
|
|
186
|
+
* @param {string} layer
|
|
187
|
+
*/
|
|
188
|
+
function createFeatureTable(db, layer) {
|
|
189
|
+
const definition = LAYER_DEFINITIONS[layer];
|
|
190
|
+
const columns = definition.columns
|
|
191
|
+
.map((column) => `${quoteIdentifier(column)} TEXT`)
|
|
192
|
+
.join(',\n ');
|
|
193
|
+
|
|
194
|
+
db.exec(`
|
|
195
|
+
CREATE TABLE ${quoteIdentifier(layer)} (
|
|
196
|
+
${quoteIdentifier('fid')} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
197
|
+
${quoteIdentifier('geom')} BLOB NOT NULL,
|
|
198
|
+
${columns}
|
|
199
|
+
);
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @param {Database.Database} db
|
|
205
|
+
* @param {string} layer
|
|
206
|
+
* @param {Feature[]} features
|
|
207
|
+
* @param {[number, number, number, number]} packageBbox
|
|
208
|
+
*/
|
|
209
|
+
function registerFeatureTable(db, layer, features, packageBbox) {
|
|
210
|
+
const definition = LAYER_DEFINITIONS[layer];
|
|
211
|
+
const bbox = features.length > 0 ? mergeFeatureBboxes(features) : packageBbox;
|
|
212
|
+
|
|
213
|
+
db.prepare(`
|
|
214
|
+
INSERT INTO gpkg_contents (
|
|
215
|
+
table_name,
|
|
216
|
+
data_type,
|
|
217
|
+
identifier,
|
|
218
|
+
description,
|
|
219
|
+
min_x,
|
|
220
|
+
min_y,
|
|
221
|
+
max_x,
|
|
222
|
+
max_y,
|
|
223
|
+
srs_id
|
|
224
|
+
) VALUES (?, 'features', ?, ?, ?, ?, ?, ?, ?)
|
|
225
|
+
`).run(
|
|
226
|
+
layer,
|
|
227
|
+
layer,
|
|
228
|
+
`map-zero ${layer}`,
|
|
229
|
+
bbox[0],
|
|
230
|
+
bbox[1],
|
|
231
|
+
bbox[2],
|
|
232
|
+
bbox[3],
|
|
233
|
+
SRS_ID
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
db.prepare(`
|
|
237
|
+
INSERT INTO gpkg_geometry_columns (
|
|
238
|
+
table_name,
|
|
239
|
+
column_name,
|
|
240
|
+
geometry_type_name,
|
|
241
|
+
srs_id,
|
|
242
|
+
z,
|
|
243
|
+
m
|
|
244
|
+
) VALUES (?, 'geom', ?, ?, 0, 0)
|
|
245
|
+
`).run(layer, definition.gpkgGeometryType, SRS_ID);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* @param {Database.Database} db
|
|
250
|
+
* @param {string} layer
|
|
251
|
+
* @param {Feature[]} features
|
|
252
|
+
*/
|
|
253
|
+
function insertFeatures(db, layer, features) {
|
|
254
|
+
const writer = createLayerWriter(db, layer);
|
|
255
|
+
|
|
256
|
+
for (const feature of features) {
|
|
257
|
+
const values = [
|
|
258
|
+
encodeGeoPackageGeometry(feature.geometry),
|
|
259
|
+
...writer.propertyColumns.map((column) => feature.properties[column] ?? null)
|
|
260
|
+
];
|
|
261
|
+
const info = writer.insert.run(values);
|
|
262
|
+
|
|
263
|
+
if (writer.spatialIndex) {
|
|
264
|
+
const bbox = feature.bbox ?? geometryBbox(feature.geometry);
|
|
265
|
+
if (bbox) {
|
|
266
|
+
writer.spatialIndex.insert.run(info.lastInsertRowid, bbox[0], bbox[2], bbox[1], bbox[3]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (writer.spatialIndex) {
|
|
272
|
+
createSpatialIndexMetadata(db, layer);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @param {Database.Database} db
|
|
278
|
+
* @param {string} layer
|
|
279
|
+
* @returns {{ insert: Database.Statement, spatialIndex: { insert: Database.Statement } | null, propertyColumns: string[] }}
|
|
280
|
+
*/
|
|
281
|
+
function createLayerWriter(db, layer) {
|
|
282
|
+
const definition = LAYER_DEFINITIONS[layer];
|
|
283
|
+
const table = quoteIdentifier(layer);
|
|
284
|
+
const propertyColumns = definition.columns;
|
|
285
|
+
const columns = ['geom', ...propertyColumns];
|
|
286
|
+
const insertSql = `
|
|
287
|
+
INSERT INTO ${table} (${columns.map(quoteIdentifier).join(', ')})
|
|
288
|
+
VALUES (${columns.map(() => '?').join(', ')})
|
|
289
|
+
`;
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
insert: db.prepare(insertSql),
|
|
293
|
+
spatialIndex: createSpatialIndex(db, layer),
|
|
294
|
+
propertyColumns
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @param {Database.Database} db
|
|
300
|
+
* @param {string} layer
|
|
301
|
+
* @returns {{ insert: Database.Statement } | null}
|
|
302
|
+
*/
|
|
303
|
+
function createSpatialIndex(db, layer) {
|
|
304
|
+
const rtree = quoteIdentifier(`rtree_${layer}_geom`);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
db.exec(`CREATE VIRTUAL TABLE ${rtree} USING rtree(id, minx, maxx, miny, maxy);`);
|
|
308
|
+
return {
|
|
309
|
+
insert: db.prepare(`INSERT INTO ${rtree} (id, minx, maxx, miny, maxy) VALUES (?, ?, ?, ?, ?)`)
|
|
310
|
+
};
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Register the GeoPackage RTree extension after manual index population.
|
|
318
|
+
*
|
|
319
|
+
* @param {Database.Database} db
|
|
320
|
+
* @param {string} layer
|
|
321
|
+
*/
|
|
322
|
+
function createSpatialIndexMetadata(db, layer) {
|
|
323
|
+
const table = quoteIdentifier(layer);
|
|
324
|
+
const rtree = quoteIdentifier(`rtree_${layer}_geom`);
|
|
325
|
+
const fid = quoteIdentifier('fid');
|
|
326
|
+
const geom = quoteIdentifier('geom');
|
|
327
|
+
|
|
328
|
+
db.exec(`
|
|
329
|
+
CREATE TABLE IF NOT EXISTS gpkg_extensions (
|
|
330
|
+
table_name TEXT,
|
|
331
|
+
column_name TEXT,
|
|
332
|
+
extension_name TEXT NOT NULL,
|
|
333
|
+
definition TEXT NOT NULL,
|
|
334
|
+
scope TEXT NOT NULL,
|
|
335
|
+
CONSTRAINT ge_tce UNIQUE (table_name, column_name, extension_name)
|
|
336
|
+
);
|
|
337
|
+
`);
|
|
338
|
+
|
|
339
|
+
db.prepare(`
|
|
340
|
+
INSERT OR REPLACE INTO gpkg_extensions (
|
|
341
|
+
table_name,
|
|
342
|
+
column_name,
|
|
343
|
+
extension_name,
|
|
344
|
+
definition,
|
|
345
|
+
scope
|
|
346
|
+
) VALUES (?, 'geom', 'gpkg_rtree_index', 'http://www.geopackage.org/spec/#extension_rtree', 'write-only')
|
|
347
|
+
`).run(layer);
|
|
348
|
+
|
|
349
|
+
db.exec(`
|
|
350
|
+
CREATE TRIGGER ${quoteIdentifier(`rtree_${layer}_geom_insert`)}
|
|
351
|
+
AFTER INSERT ON ${table}
|
|
352
|
+
WHEN (NEW.${geom} NOT NULL AND NOT ST_IsEmpty(NEW.${geom}))
|
|
353
|
+
BEGIN
|
|
354
|
+
INSERT OR REPLACE INTO ${rtree}
|
|
355
|
+
VALUES (
|
|
356
|
+
NEW.${fid},
|
|
357
|
+
ST_MinX(NEW.${geom}),
|
|
358
|
+
ST_MaxX(NEW.${geom}),
|
|
359
|
+
ST_MinY(NEW.${geom}),
|
|
360
|
+
ST_MaxY(NEW.${geom})
|
|
361
|
+
);
|
|
362
|
+
END;
|
|
363
|
+
|
|
364
|
+
CREATE TRIGGER ${quoteIdentifier(`rtree_${layer}_geom_update1`)}
|
|
365
|
+
AFTER UPDATE OF ${geom} ON ${table}
|
|
366
|
+
WHEN OLD.${fid} = NEW.${fid} AND (NEW.${geom} NOT NULL AND NOT ST_IsEmpty(NEW.${geom}))
|
|
367
|
+
BEGIN
|
|
368
|
+
INSERT OR REPLACE INTO ${rtree}
|
|
369
|
+
VALUES (
|
|
370
|
+
NEW.${fid},
|
|
371
|
+
ST_MinX(NEW.${geom}),
|
|
372
|
+
ST_MaxX(NEW.${geom}),
|
|
373
|
+
ST_MinY(NEW.${geom}),
|
|
374
|
+
ST_MaxY(NEW.${geom})
|
|
375
|
+
);
|
|
376
|
+
END;
|
|
377
|
+
|
|
378
|
+
CREATE TRIGGER ${quoteIdentifier(`rtree_${layer}_geom_update2`)}
|
|
379
|
+
AFTER UPDATE OF ${geom} ON ${table}
|
|
380
|
+
WHEN OLD.${fid} = NEW.${fid} AND (NEW.${geom} ISNULL OR ST_IsEmpty(NEW.${geom}))
|
|
381
|
+
BEGIN
|
|
382
|
+
DELETE FROM ${rtree} WHERE id = OLD.${fid};
|
|
383
|
+
END;
|
|
384
|
+
|
|
385
|
+
CREATE TRIGGER ${quoteIdentifier(`rtree_${layer}_geom_update3`)}
|
|
386
|
+
AFTER UPDATE ON ${table}
|
|
387
|
+
WHEN OLD.${fid} != NEW.${fid} AND (NEW.${geom} NOT NULL AND NOT ST_IsEmpty(NEW.${geom}))
|
|
388
|
+
BEGIN
|
|
389
|
+
DELETE FROM ${rtree} WHERE id = OLD.${fid};
|
|
390
|
+
INSERT OR REPLACE INTO ${rtree}
|
|
391
|
+
VALUES (
|
|
392
|
+
NEW.${fid},
|
|
393
|
+
ST_MinX(NEW.${geom}),
|
|
394
|
+
ST_MaxX(NEW.${geom}),
|
|
395
|
+
ST_MinY(NEW.${geom}),
|
|
396
|
+
ST_MaxY(NEW.${geom})
|
|
397
|
+
);
|
|
398
|
+
END;
|
|
399
|
+
|
|
400
|
+
CREATE TRIGGER ${quoteIdentifier(`rtree_${layer}_geom_update4`)}
|
|
401
|
+
AFTER UPDATE ON ${table}
|
|
402
|
+
WHEN OLD.${fid} != NEW.${fid} AND (NEW.${geom} ISNULL OR ST_IsEmpty(NEW.${geom}))
|
|
403
|
+
BEGIN
|
|
404
|
+
DELETE FROM ${rtree} WHERE id IN (OLD.${fid}, NEW.${fid});
|
|
405
|
+
END;
|
|
406
|
+
|
|
407
|
+
CREATE TRIGGER ${quoteIdentifier(`rtree_${layer}_geom_delete`)}
|
|
408
|
+
AFTER DELETE ON ${table}
|
|
409
|
+
WHEN OLD.${geom} NOT NULL
|
|
410
|
+
BEGIN
|
|
411
|
+
DELETE FROM ${rtree} WHERE id = OLD.${fid};
|
|
412
|
+
END;
|
|
413
|
+
`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* @param {Feature[]} features
|
|
418
|
+
* @returns {[number, number, number, number]}
|
|
419
|
+
*/
|
|
420
|
+
function mergeFeatureBboxes(features) {
|
|
421
|
+
/** @type {[number, number, number, number] | null} */
|
|
422
|
+
let bbox = null;
|
|
423
|
+
|
|
424
|
+
for (const feature of features) {
|
|
425
|
+
const featureBbox = feature.bbox ?? geometryBbox(feature.geometry);
|
|
426
|
+
if (!featureBbox) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!bbox) {
|
|
431
|
+
bbox = [...featureBbox];
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
bbox[0] = Math.min(bbox[0], featureBbox[0]);
|
|
436
|
+
bbox[1] = Math.min(bbox[1], featureBbox[1]);
|
|
437
|
+
bbox[2] = Math.max(bbox[2], featureBbox[2]);
|
|
438
|
+
bbox[3] = Math.max(bbox[3], featureBbox[3]);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return bbox ?? [0, 0, 0, 0];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Encode a GeoJSON-like geometry as GeoPackage binary geometry.
|
|
446
|
+
*
|
|
447
|
+
* @param {Geometry} geometry
|
|
448
|
+
* @returns {Buffer}
|
|
449
|
+
*/
|
|
450
|
+
function encodeGeoPackageGeometry(geometry) {
|
|
451
|
+
const header = Buffer.alloc(8);
|
|
452
|
+
header.writeUInt8(0x47, 0);
|
|
453
|
+
header.writeUInt8(0x50, 1);
|
|
454
|
+
header.writeUInt8(0, 2);
|
|
455
|
+
header.writeUInt8(1, 3);
|
|
456
|
+
header.writeInt32LE(SRS_ID, 4);
|
|
457
|
+
|
|
458
|
+
return Buffer.concat([header, encodeWkbGeometry(geometry)]);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* @param {Geometry} geometry
|
|
463
|
+
* @returns {Buffer}
|
|
464
|
+
*/
|
|
465
|
+
function encodeWkbGeometry(geometry) {
|
|
466
|
+
switch (geometry.type) {
|
|
467
|
+
case 'Point':
|
|
468
|
+
return concatWkb([
|
|
469
|
+
uint8(1),
|
|
470
|
+
uint32(1),
|
|
471
|
+
double(/** @type {[number, number]} */ (geometry.coordinates)[0]),
|
|
472
|
+
double(/** @type {[number, number]} */ (geometry.coordinates)[1])
|
|
473
|
+
]);
|
|
474
|
+
|
|
475
|
+
case 'LineString':
|
|
476
|
+
return encodeWkbLineString(/** @type {Array<[number, number]>} */ (geometry.coordinates));
|
|
477
|
+
|
|
478
|
+
case 'MultiLineString':
|
|
479
|
+
return concatWkb([
|
|
480
|
+
uint8(1),
|
|
481
|
+
uint32(5),
|
|
482
|
+
uint32(/** @type {Array<Array<[number, number]>>} */ (geometry.coordinates).length),
|
|
483
|
+
.../** @type {Array<Array<[number, number]>>} */ (geometry.coordinates).map(encodeWkbLineString)
|
|
484
|
+
]);
|
|
485
|
+
|
|
486
|
+
case 'Polygon':
|
|
487
|
+
return encodeWkbPolygon(/** @type {Array<Array<[number, number]>>} */ (geometry.coordinates));
|
|
488
|
+
|
|
489
|
+
case 'MultiPolygon':
|
|
490
|
+
return concatWkb([
|
|
491
|
+
uint8(1),
|
|
492
|
+
uint32(6),
|
|
493
|
+
uint32(/** @type {Array<Array<Array<[number, number]>>>} */ (geometry.coordinates).length),
|
|
494
|
+
.../** @type {Array<Array<Array<[number, number]>>>} */ (geometry.coordinates).map(encodeWkbPolygon)
|
|
495
|
+
]);
|
|
496
|
+
|
|
497
|
+
default:
|
|
498
|
+
throw new Error(`unsupported geometry type: ${geometry.type}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* @param {Array<[number, number]>} coordinates
|
|
504
|
+
* @returns {Buffer}
|
|
505
|
+
*/
|
|
506
|
+
function encodeWkbLineString(coordinates) {
|
|
507
|
+
return concatWkb([
|
|
508
|
+
uint8(1),
|
|
509
|
+
uint32(2),
|
|
510
|
+
uint32(coordinates.length),
|
|
511
|
+
...coordinates.flatMap(([x, y]) => [double(x), double(y)])
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @param {Array<Array<[number, number]>>} rings
|
|
517
|
+
* @returns {Buffer}
|
|
518
|
+
*/
|
|
519
|
+
function encodeWkbPolygon(rings) {
|
|
520
|
+
return concatWkb([
|
|
521
|
+
uint8(1),
|
|
522
|
+
uint32(3),
|
|
523
|
+
uint32(rings.length),
|
|
524
|
+
...rings.flatMap((ring) => [
|
|
525
|
+
uint32(ring.length),
|
|
526
|
+
...ring.flatMap(([x, y]) => [double(x), double(y)])
|
|
527
|
+
])
|
|
528
|
+
]);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* @param {Buffer[]} parts
|
|
533
|
+
* @returns {Buffer}
|
|
534
|
+
*/
|
|
535
|
+
function concatWkb(parts) {
|
|
536
|
+
return Buffer.concat(parts);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* @param {number} value
|
|
541
|
+
* @returns {Buffer}
|
|
542
|
+
*/
|
|
543
|
+
function uint8(value) {
|
|
544
|
+
const buffer = Buffer.alloc(1);
|
|
545
|
+
buffer.writeUInt8(value);
|
|
546
|
+
return buffer;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* @param {number} value
|
|
551
|
+
* @returns {Buffer}
|
|
552
|
+
*/
|
|
553
|
+
function uint32(value) {
|
|
554
|
+
const buffer = Buffer.alloc(4);
|
|
555
|
+
buffer.writeUInt32LE(value);
|
|
556
|
+
return buffer;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* @param {number} value
|
|
561
|
+
* @returns {Buffer}
|
|
562
|
+
*/
|
|
563
|
+
function double(value) {
|
|
564
|
+
const buffer = Buffer.alloc(8);
|
|
565
|
+
buffer.writeDoubleLE(value);
|
|
566
|
+
return buffer;
|
|
567
|
+
}
|