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,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
|
+
}
|
package/src/gpkg-read.js
ADDED
|
@@ -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
|
+
}
|