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.
Files changed (52) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +220 -0
  4. package/docs/api.md +66 -0
  5. package/docs/architecture.md +87 -0
  6. package/docs/cartography.md +77 -0
  7. package/docs/cesium.md +107 -0
  8. package/docs/openlayers.md +98 -0
  9. package/docs/styles.md +103 -0
  10. package/package.json +51 -0
  11. package/packages/cesium/package.json +13 -0
  12. package/packages/cesium/src/index.js +405 -0
  13. package/packages/ol/package.json +14 -0
  14. package/packages/ol/src/index.js +1705 -0
  15. package/packages/ol/src/labels.js +977 -0
  16. package/src/3dtiles/b3dm.js +38 -0
  17. package/src/3dtiles/clipper-surfaces.js +317 -0
  18. package/src/3dtiles/export.js +768 -0
  19. package/src/3dtiles/extrude.js +301 -0
  20. package/src/3dtiles/flat.js +531 -0
  21. package/src/3dtiles/glb.js +178 -0
  22. package/src/3dtiles/gpkg-buildings.js +240 -0
  23. package/src/3dtiles/gpkg-features.js +157 -0
  24. package/src/3dtiles/tileset.js +75 -0
  25. package/src/build.js +134 -0
  26. package/src/cli.js +656 -0
  27. package/src/export-pmtiles.js +962 -0
  28. package/src/geometry-read.js +50 -0
  29. package/src/gpkg-read.js +460 -0
  30. package/src/gpkg.js +567 -0
  31. package/src/html.js +593 -0
  32. package/src/layers.js +357 -0
  33. package/src/manifest.js +29 -0
  34. package/src/mvt.js +2593 -0
  35. package/src/ol.js +5 -0
  36. package/src/osm.js +2110 -0
  37. package/src/pmtiles-worker.js +70 -0
  38. package/src/pmtiles.js +260 -0
  39. package/src/server.js +720 -0
  40. package/src/style-command.js +78 -0
  41. package/src/style-filters.js +76 -0
  42. package/src/style-presets.js +93 -0
  43. package/src/style-themes.js +235 -0
  44. package/src/style.js +13 -0
  45. package/src/tile-cache.js +59 -0
  46. package/src/utils.js +222 -0
  47. package/styles/presets/light.json +4655 -0
  48. package/styles/presets/monochrome.json +4655 -0
  49. package/styles/presets/neon-dark-3d.json +90 -0
  50. package/styles/presets/neon-dark.json +4690 -0
  51. package/styles/presets/tactical.json +4690 -0
  52. package/styles/themes/neon-dark.theme.json +20 -0
@@ -0,0 +1,50 @@
1
+ import wkx from 'wkx';
2
+
3
+ const ENVELOPE_LENGTHS = [0, 32, 48, 48, 64];
4
+
5
+ /**
6
+ * Decode a GeoPackage binary geometry into GeoJSON geometry.
7
+ *
8
+ * GeoPackage geometry blobs start with a binary header. The WKB payload begins
9
+ * after the fixed 8-byte header plus the optional envelope.
10
+ *
11
+ * @param {Buffer | Uint8Array | null} value
12
+ * @returns {Record<string, unknown> | null}
13
+ */
14
+ export function decodeGeoPackageGeometry(value) {
15
+ if (!value) {
16
+ return null;
17
+ }
18
+
19
+ const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
20
+
21
+ if (buffer.length < 8) {
22
+ throw new Error('invalid GeoPackage geometry: header is too short');
23
+ }
24
+
25
+ if (buffer.readUInt8(0) !== 0x47 || buffer.readUInt8(1) !== 0x50) {
26
+ throw new Error('invalid GeoPackage geometry: missing GP magic bytes');
27
+ }
28
+
29
+ const flags = buffer.readUInt8(3);
30
+ const isEmpty = Boolean(flags & 0b00010000);
31
+
32
+ if (isEmpty) {
33
+ return null;
34
+ }
35
+
36
+ const envelopeCode = (flags >> 1) & 0b00000111;
37
+ const envelopeLength = ENVELOPE_LENGTHS[envelopeCode];
38
+
39
+ if (envelopeLength === undefined) {
40
+ throw new Error(`invalid GeoPackage geometry: unsupported envelope code ${envelopeCode}`);
41
+ }
42
+
43
+ const wkbOffset = 8 + envelopeLength;
44
+
45
+ if (buffer.length <= wkbOffset) {
46
+ throw new Error('invalid GeoPackage geometry: missing WKB payload');
47
+ }
48
+
49
+ return wkx.Geometry.parse(buffer.subarray(wkbOffset)).toGeoJSON();
50
+ }
@@ -0,0 +1,460 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ import { decodeGeoPackageGeometry } from './geometry-read.js';
4
+ import { quoteIdentifier } from './utils.js';
5
+
6
+ const LAYER_ALIASES = {
7
+ aviation: 'aip',
8
+ aip: 'aviation'
9
+ };
10
+
11
+ /**
12
+ * @typedef {{
13
+ * id: string,
14
+ * type?: string,
15
+ * source?: string,
16
+ * table: string,
17
+ * style?: string
18
+ * }} ManifestLayer
19
+ *
20
+ * @typedef {{
21
+ * format?: string,
22
+ * version?: number,
23
+ * name?: string,
24
+ * bbox?: [number, number, number, number],
25
+ * data?: string,
26
+ * styles?: Record<string, string>,
27
+ * layers?: ManifestLayer[]
28
+ * }} Manifest
29
+ *
30
+ * @typedef {Map<string, Map<string, Set<string>>>} HiddenFilters
31
+ *
32
+ * @typedef {{
33
+ * column: string,
34
+ * include?: string[],
35
+ * exclude?: string[],
36
+ * minNumber?: number,
37
+ * maxNumber?: number
38
+ * }} TilePropertyFilter
39
+ *
40
+ * @typedef {{
41
+ * all?: TilePropertyFilter[],
42
+ * any?: TilePropertyFilter[],
43
+ * minRtreeSpan?: number
44
+ * }} TileQueryFilters
45
+ */
46
+
47
+ /**
48
+ * Open a GeoPackage reader in readonly mode.
49
+ *
50
+ * @param {{ gpkgPath: string, manifest: Manifest, hiddenFilters?: HiddenFilters }} options
51
+ * @returns {{
52
+ * close: () => void,
53
+ * getInfo: (packageDir: string) => Record<string, unknown>,
54
+ * getLayers: () => Array<Record<string, unknown>>,
55
+ * getTileFeatures: (layerId: string, bbox: [number, number, number, number], filters?: TileQueryFilters) => Array<Record<string, unknown>>
56
+ * }}
57
+ */
58
+ export function openGeoPackageReader(options) {
59
+ const db = new Database(options.gpkgPath, {
60
+ readonly: true,
61
+ fileMustExist: true
62
+ });
63
+ configureReadPerformance(db);
64
+ const statementCache = new Map();
65
+
66
+ const layerById = new Map();
67
+ for (const layer of options.manifest.layers ?? []) {
68
+ layerById.set(layer.id, layer);
69
+ const alias = LAYER_ALIASES[layer.id];
70
+ if (alias && !layerById.has(alias)) {
71
+ layerById.set(alias, layer);
72
+ }
73
+ }
74
+
75
+ const tableContents = loadContents(db);
76
+ const geometryColumns = loadGeometryColumns(db);
77
+ const layers = buildLayerMetadata(db, [...layerById.values()], tableContents, geometryColumns);
78
+ const columnsByTable = new Map(layers.map((layer) => [
79
+ String(layer.table),
80
+ layer.exists ? loadTableColumns(db, String(layer.table)) : new Set()
81
+ ]));
82
+
83
+ return {
84
+ close() {
85
+ db.close();
86
+ },
87
+
88
+ getInfo(packageDir) {
89
+ return {
90
+ packagePath: packageDir,
91
+ gpkgPath: options.gpkgPath,
92
+ manifest: {
93
+ format: options.manifest.format,
94
+ version: options.manifest.version,
95
+ name: options.manifest.name,
96
+ bbox: options.manifest.bbox,
97
+ data: options.manifest.data,
98
+ styles: options.manifest.styles,
99
+ layerCount: options.manifest.layers?.length ?? 0
100
+ },
101
+ layers
102
+ };
103
+ },
104
+
105
+ getLayers() {
106
+ return layers;
107
+ },
108
+
109
+ getTileFeatures(layerId, bbox, filters = {}) {
110
+ const { layer, metadata } = getReadableLayer(layerId, layerById, layers);
111
+ const tableColumns = columnsByTable.get(layer.table) ?? new Set();
112
+ const hiddenFilters = hiddenFiltersForLayer(
113
+ options.hiddenFilters?.get(layerId) ?? options.hiddenFilters?.get(String(layer.id)),
114
+ tableColumns
115
+ );
116
+ const tileFilters = normalizeTileQueryFilters(filters, tableColumns);
117
+ const rows = selectFeaturesWithBbox(
118
+ db,
119
+ layer.table,
120
+ String(metadata.geometryColumn),
121
+ String(metadata.rtree),
122
+ bbox,
123
+ hiddenFilters,
124
+ tileFilters,
125
+ statementCache
126
+ );
127
+
128
+ return rows
129
+ .map((row) => rowToFeature(row, String(metadata.geometryColumn)))
130
+ .filter((feature) => feature.geometry !== null);
131
+ }
132
+ };
133
+ }
134
+
135
+ /**
136
+ * @param {Database.Database} db
137
+ */
138
+ function configureReadPerformance(db) {
139
+ for (const pragma of [
140
+ 'query_only = ON',
141
+ 'temp_store = MEMORY',
142
+ 'cache_size = -65536',
143
+ 'mmap_size = 268435456'
144
+ ]) {
145
+ try {
146
+ db.pragma(pragma);
147
+ } catch {
148
+ // Some SQLite builds can reject optional tuning pragmas; readonly access still works without them.
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * @param {Database.Database} db
155
+ * @param {string} table
156
+ * @returns {Set<string>}
157
+ */
158
+ function loadTableColumns(db, table) {
159
+ const rows = db.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all();
160
+ return new Set(rows.map((row) => String(row.name)));
161
+ }
162
+
163
+ /**
164
+ * @param {Database.Database} db
165
+ * @returns {Map<string, Record<string, unknown>>}
166
+ */
167
+ function loadContents(db) {
168
+ const rows = db.prepare(`
169
+ SELECT table_name, data_type, identifier, description, min_x, min_y, max_x, max_y, srs_id
170
+ FROM gpkg_contents
171
+ WHERE data_type = 'features'
172
+ `).all();
173
+
174
+ return new Map(rows.map((row) => [String(row.table_name), row]));
175
+ }
176
+
177
+ /**
178
+ * @param {Database.Database} db
179
+ * @returns {Map<string, Record<string, unknown>>}
180
+ */
181
+ function loadGeometryColumns(db) {
182
+ const rows = db.prepare(`
183
+ SELECT table_name, column_name, geometry_type_name, srs_id, z, m
184
+ FROM gpkg_geometry_columns
185
+ `).all();
186
+
187
+ return new Map(rows.map((row) => [String(row.table_name), row]));
188
+ }
189
+
190
+ /**
191
+ * @param {Database.Database} db
192
+ * @param {ManifestLayer[]} manifestLayers
193
+ * @param {Map<string, Record<string, unknown>>} tableContents
194
+ * @param {Map<string, Record<string, unknown>>} geometryColumns
195
+ * @returns {Array<Record<string, unknown>>}
196
+ */
197
+ function buildLayerMetadata(db, manifestLayers, tableContents, geometryColumns) {
198
+ return manifestLayers.map((layer) => {
199
+ const content = tableContents.get(layer.table);
200
+ const geometry = geometryColumns.get(layer.table);
201
+ const geometryColumn = geometry?.column_name ? String(geometry.column_name) : null;
202
+ const rtree = geometryColumn ? findRtree(db, layer.table, geometryColumn) : null;
203
+
204
+ return {
205
+ ...layer,
206
+ exists: Boolean(content && geometry),
207
+ geometryColumn,
208
+ geometryType: geometry?.geometry_type_name ?? null,
209
+ srsId: geometry?.srs_id ?? content?.srs_id ?? null,
210
+ bbox: content
211
+ ? [content.min_x, content.min_y, content.max_x, content.max_y].map((value) => Number(value))
212
+ : null,
213
+ rtree
214
+ };
215
+ });
216
+ }
217
+
218
+ /**
219
+ * @param {Database.Database} db
220
+ * @param {string} table
221
+ * @param {string} geometryColumn
222
+ * @returns {string | null}
223
+ */
224
+ function findRtree(db, table, geometryColumn) {
225
+ const rtree = `rtree_${table}_${geometryColumn}`;
226
+ const row = db.prepare(`
227
+ SELECT name
228
+ FROM sqlite_master
229
+ WHERE type = 'table' AND name = ?
230
+ `).get(rtree);
231
+
232
+ return row ? rtree : null;
233
+ }
234
+
235
+ /**
236
+ * @param {Database.Database} db
237
+ * @param {string} table
238
+ * @param {string} geometryColumn
239
+ * @param {string} rtree
240
+ * @param {[number, number, number, number]} bbox
241
+ * @param {Array<{ column: string, values: string[] }>} hiddenFilters
242
+ * @param {{ all: TilePropertyFilter[], any: TilePropertyFilter[] }} tileFilters
243
+ * @param {Map<string, Database.Statement>} statementCache
244
+ * @returns {Record<string, unknown>[]}
245
+ */
246
+ function selectFeaturesWithBbox(db, table, geometryColumn, rtree, bbox, hiddenFilters, tileFilters, statementCache) {
247
+ const [minX, minY, maxX, maxY] = bbox;
248
+ const params = [maxX, minX, maxY, minY];
249
+ let filterSql = '';
250
+ const cacheKeyParts = [table, geometryColumn, rtree];
251
+
252
+ for (const filter of hiddenFilters) {
253
+ if (filter.values.length === 0) {
254
+ continue;
255
+ }
256
+
257
+ filterSql += `
258
+ AND (
259
+ feature_table.${quoteIdentifier(filter.column)} IS NULL
260
+ OR feature_table.${quoteIdentifier(filter.column)} NOT IN (${filter.values.map(() => '?').join(', ')})
261
+ )`;
262
+ params.push(...filter.values);
263
+ cacheKeyParts.push(`hidden:${filter.column}:${filter.values.length}`);
264
+ }
265
+
266
+ for (const filter of tileFilters.all) {
267
+ const sql = propertyFilterSql(filter, params, cacheKeyParts);
268
+ if (sql) {
269
+ filterSql += `
270
+ AND ${sql}`;
271
+ }
272
+ }
273
+
274
+ const anySql = tileFilters.any
275
+ .map((filter) => propertyFilterSql(filter, params, cacheKeyParts))
276
+ .filter(Boolean);
277
+ if (anySql.length > 0) {
278
+ cacheKeyParts.push(`any:${anySql.length}`);
279
+ filterSql += `
280
+ AND (${anySql.join(' OR ')})`;
281
+ }
282
+
283
+ if (Number.isFinite(tileFilters.minRtreeSpan) && Number(tileFilters.minRtreeSpan) > 0) {
284
+ filterSql += `
285
+ AND (
286
+ (rtree_table.maxx - rtree_table.minx) >= ?
287
+ OR (rtree_table.maxy - rtree_table.miny) >= ?
288
+ )`;
289
+ params.push(Number(tileFilters.minRtreeSpan), Number(tileFilters.minRtreeSpan));
290
+ cacheKeyParts.push('rtree-span');
291
+ }
292
+
293
+ const cacheKey = cacheKeyParts.join('|');
294
+ let statement = statementCache.get(cacheKey);
295
+ if (!statement) {
296
+ statement = db.prepare(`
297
+ SELECT feature_table.*
298
+ FROM ${quoteIdentifier(table)} AS feature_table
299
+ JOIN ${quoteIdentifier(rtree)} AS rtree_table
300
+ ON feature_table.rowid = rtree_table.id
301
+ WHERE rtree_table.minx <= ?
302
+ AND rtree_table.maxx >= ?
303
+ AND rtree_table.miny <= ?
304
+ AND rtree_table.maxy >= ?
305
+ ${filterSql}
306
+ `);
307
+ statementCache.set(cacheKey, statement);
308
+ }
309
+
310
+ return statement.all(...params);
311
+ }
312
+
313
+ /**
314
+ * @param {TileQueryFilters} filters
315
+ * @param {Set<string>} tableColumns
316
+ * @returns {{ all: TilePropertyFilter[], any: TilePropertyFilter[] }}
317
+ */
318
+ function normalizeTileQueryFilters(filters, tableColumns) {
319
+ return {
320
+ all: normalizePropertyFilters(filters.all, tableColumns),
321
+ any: normalizePropertyFilters(filters.any, tableColumns),
322
+ minRtreeSpan: Number.isFinite(filters.minRtreeSpan) ? Number(filters.minRtreeSpan) : undefined
323
+ };
324
+ }
325
+
326
+ /**
327
+ * @param {TilePropertyFilter[] | undefined} filters
328
+ * @param {Set<string>} tableColumns
329
+ * @returns {TilePropertyFilter[]}
330
+ */
331
+ function normalizePropertyFilters(filters, tableColumns) {
332
+ if (!Array.isArray(filters)) {
333
+ return [];
334
+ }
335
+
336
+ return filters.filter((filter) => filter?.column && tableColumns.has(filter.column));
337
+ }
338
+
339
+ /**
340
+ * @param {TilePropertyFilter} filter
341
+ * @param {unknown[]} params
342
+ * @param {string[]} cacheKeyParts
343
+ * @returns {string}
344
+ */
345
+ function propertyFilterSql(filter, params, cacheKeyParts) {
346
+ const column = `feature_table.${quoteIdentifier(filter.column)}`;
347
+ const parts = [];
348
+
349
+ if (Array.isArray(filter.include)) {
350
+ if (filter.include.length === 0) {
351
+ return '0';
352
+ }
353
+
354
+ parts.push(`${column} IN (${filter.include.map(() => '?').join(', ')})`);
355
+ params.push(...filter.include);
356
+ cacheKeyParts.push(`include:${filter.column}:${filter.include.length}`);
357
+ }
358
+
359
+ if (Array.isArray(filter.exclude) && filter.exclude.length > 0) {
360
+ parts.push(`(${column} IS NULL OR ${column} NOT IN (${filter.exclude.map(() => '?').join(', ')}))`);
361
+ params.push(...filter.exclude);
362
+ cacheKeyParts.push(`exclude:${filter.column}:${filter.exclude.length}`);
363
+ }
364
+
365
+ if (Number.isFinite(filter.minNumber)) {
366
+ parts.push(`CAST(${column} AS REAL) >= ?`);
367
+ params.push(Number(filter.minNumber));
368
+ cacheKeyParts.push(`min:${filter.column}`);
369
+ }
370
+
371
+ if (Number.isFinite(filter.maxNumber)) {
372
+ parts.push(`CAST(${column} AS REAL) <= ?`);
373
+ params.push(Number(filter.maxNumber));
374
+ cacheKeyParts.push(`max:${filter.column}`);
375
+ }
376
+
377
+ return parts.length > 0 ? `(${parts.join(' AND ')})` : '';
378
+ }
379
+
380
+ /**
381
+ * @param {Map<string, Set<string>> | undefined} layerFilters
382
+ * @param {Set<string>} tableColumns
383
+ * @returns {Array<{ column: string, values: string[] }>}
384
+ */
385
+ function hiddenFiltersForLayer(layerFilters, tableColumns) {
386
+ if (!layerFilters) {
387
+ return [];
388
+ }
389
+
390
+ const filters = [];
391
+ for (const [column, values] of layerFilters.entries()) {
392
+ if (tableColumns.has(column) && values.size > 0) {
393
+ filters.push({
394
+ column,
395
+ values: [...values]
396
+ });
397
+ }
398
+ }
399
+
400
+ return filters;
401
+ }
402
+
403
+ /**
404
+ * @param {Record<string, unknown>} row
405
+ * @param {string} geometryColumn
406
+ * @returns {{ type: 'Feature', id: unknown, geometry: Record<string, unknown> | null, properties: Record<string, unknown> }}
407
+ */
408
+ function rowToFeature(row, geometryColumn) {
409
+ const geometry = decodeGeoPackageGeometry(/** @type {Buffer | null} */ (row[geometryColumn]));
410
+ /** @type {Record<string, unknown>} */
411
+ const properties = {};
412
+
413
+ for (const [key, value] of Object.entries(row)) {
414
+ if (key !== geometryColumn) {
415
+ properties[key] = value;
416
+ }
417
+ }
418
+
419
+ return {
420
+ type: 'Feature',
421
+ id: properties.fid ?? properties.id ?? null,
422
+ geometry,
423
+ properties
424
+ };
425
+ }
426
+
427
+ /**
428
+ * @param {string} layerId
429
+ * @param {Map<string, ManifestLayer>} layerById
430
+ * @param {Array<Record<string, unknown>>} layers
431
+ * @returns {{ layer: ManifestLayer, metadata: Record<string, unknown> }}
432
+ */
433
+ function getReadableLayer(layerId, layerById, layers) {
434
+ const layer = layerById.get(layerId);
435
+ if (!layer) {
436
+ throw httpError(404, `unknown layer: ${layerId}`);
437
+ }
438
+
439
+ const metadata = layers.find((item) => item.id === layer.id);
440
+ if (!metadata || !metadata.exists || !metadata.geometryColumn) {
441
+ throw httpError(404, `layer is not available in GeoPackage: ${layerId}`);
442
+ }
443
+
444
+ if (!metadata.rtree) {
445
+ throw httpError(400, `layer does not have an RTree spatial index: ${layerId}`);
446
+ }
447
+
448
+ return { layer, metadata };
449
+ }
450
+
451
+ /**
452
+ * @param {number} statusCode
453
+ * @param {string} message
454
+ * @returns {Error & { statusCode: number }}
455
+ */
456
+ function httpError(statusCode, message) {
457
+ const error = /** @type {Error & { statusCode: number }} */ (new Error(message));
458
+ error.statusCode = statusCode;
459
+ return error;
460
+ }