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
package/src/layers.js ADDED
@@ -0,0 +1,357 @@
1
+ export const SUPPORTED_LAYERS = [
2
+ 'roads',
3
+ 'buildings',
4
+ 'water',
5
+ 'landuse',
6
+ 'railways',
7
+ 'boundaries',
8
+ 'pois',
9
+ 'aip'
10
+ ];
11
+
12
+ export const LAYER_ALIASES = {
13
+ aviation: 'aip'
14
+ };
15
+
16
+ export const LAYER_DEFINITIONS = {
17
+ roads: {
18
+ type: 'line',
19
+ gpkgGeometryType: 'LINESTRING',
20
+ columns: ['id', 'name', 'ref', 'highway', 'layer', 'bridge', 'tunnel', 'oneway', 'junction', 'construction', 'service', 'access']
21
+ },
22
+ buildings: {
23
+ type: 'polygon',
24
+ gpkgGeometryType: 'MULTIPOLYGON',
25
+ columns: ['id', 'name', 'building', 'height', 'min_height', 'building:levels']
26
+ },
27
+ water: {
28
+ type: 'polygon',
29
+ gpkgGeometryType: 'MULTIPOLYGON',
30
+ columns: ['id', 'name', 'natural', 'waterway', 'landuse']
31
+ },
32
+ landuse: {
33
+ type: 'polygon',
34
+ gpkgGeometryType: 'MULTIPOLYGON',
35
+ columns: ['id', 'name', 'landuse', 'leisure', 'natural']
36
+ },
37
+ railways: {
38
+ type: 'line',
39
+ gpkgGeometryType: 'LINESTRING',
40
+ columns: ['id', 'name', 'railway']
41
+ },
42
+ boundaries: {
43
+ type: 'line',
44
+ gpkgGeometryType: 'GEOMETRY',
45
+ columns: ['id', 'name', 'admin_level']
46
+ },
47
+ pois: {
48
+ type: 'point',
49
+ gpkgGeometryType: 'POINT',
50
+ columns: [
51
+ 'id',
52
+ 'name',
53
+ 'poi_category',
54
+ 'amenity',
55
+ 'tourism',
56
+ 'shop',
57
+ 'leisure',
58
+ 'railway',
59
+ 'public_transport',
60
+ 'station',
61
+ 'aeroway',
62
+ 'power',
63
+ 'man_made',
64
+ 'tower:type',
65
+ 'military',
66
+ 'emergency',
67
+ 'office',
68
+ 'government',
69
+ 'boundary',
70
+ 'protect_class',
71
+ 'landuse',
72
+ 'industrial'
73
+ ]
74
+ },
75
+ aip: {
76
+ type: 'mixed',
77
+ gpkgGeometryType: 'GEOMETRY',
78
+ columns: ['id', 'name', 'aeroway', 'ref', 'surface', 'width', 'length']
79
+ }
80
+ };
81
+
82
+ /**
83
+ * Normalize public layer aliases to the canonical map-zero layer id.
84
+ *
85
+ * @param {string} layerId
86
+ * @returns {string}
87
+ */
88
+ export function normalizeLayerId(layerId) {
89
+ return LAYER_ALIASES[layerId] ?? layerId;
90
+ }
91
+
92
+ /**
93
+ * Find logical way layers for an OSM tag set.
94
+ *
95
+ * @param {Record<string, string>} tags
96
+ * @param {Set<string>} selectedLayers
97
+ * @returns {string[]}
98
+ */
99
+ export function layersForWay(tags, selectedLayers) {
100
+ /** @type {string[]} */
101
+ const layers = [];
102
+
103
+ if (selectedLayers.has('roads') && tags.highway) {
104
+ layers.push('roads');
105
+ }
106
+
107
+ if (selectedLayers.has('buildings') && tags.building) {
108
+ layers.push('buildings');
109
+ }
110
+
111
+ if (selectedLayers.has('water') && isWater(tags)) {
112
+ layers.push('water');
113
+ }
114
+
115
+ if (selectedLayers.has('landuse') && isLanduse(tags)) {
116
+ layers.push('landuse');
117
+ }
118
+
119
+ if (selectedLayers.has('railways') && tags.railway) {
120
+ layers.push('railways');
121
+ }
122
+
123
+ if (selectedLayers.has('boundaries') && tags.boundary === 'administrative') {
124
+ layers.push('boundaries');
125
+ }
126
+
127
+ if (selectedLayers.has('aip') && tags.aeroway) {
128
+ layers.push('aip');
129
+ }
130
+
131
+ if (selectedLayers.has('pois') && isPoi(tags)) {
132
+ layers.push('pois');
133
+ }
134
+
135
+ return layers;
136
+ }
137
+
138
+ /**
139
+ * Find logical relation layers for an OSM tag set.
140
+ *
141
+ * @param {Record<string, string>} tags
142
+ * @param {Set<string>} selectedLayers
143
+ * @returns {string[]}
144
+ */
145
+ export function layersForRelation(tags, selectedLayers) {
146
+ /** @type {string[]} */
147
+ const layers = [];
148
+
149
+ if (selectedLayers.has('buildings') && tags.building) {
150
+ layers.push('buildings');
151
+ }
152
+
153
+ if (selectedLayers.has('water') && isWater(tags)) {
154
+ layers.push('water');
155
+ }
156
+
157
+ if (selectedLayers.has('landuse') && isLanduse(tags)) {
158
+ layers.push('landuse');
159
+ }
160
+
161
+ if (selectedLayers.has('boundaries') && tags.boundary === 'administrative') {
162
+ layers.push('boundaries');
163
+ }
164
+
165
+ if (selectedLayers.has('aip') && tags.aeroway) {
166
+ layers.push('aip');
167
+ }
168
+
169
+ if (selectedLayers.has('pois') && isPoi(tags)) {
170
+ layers.push('pois');
171
+ }
172
+
173
+ return layers;
174
+ }
175
+
176
+ /**
177
+ * Check whether OSM entity tags should become a POI.
178
+ *
179
+ * @param {Record<string, string>} tags
180
+ * @returns {boolean}
181
+ */
182
+ export function isPoi(tags) {
183
+ return Boolean(
184
+ tags.amenity ||
185
+ tags.tourism ||
186
+ tags.shop ||
187
+ tags.leisure ||
188
+ isRailwayPoi(tags.railway) ||
189
+ isPublicTransportPoi(tags.public_transport) ||
190
+ isAerowayPoi(tags.aeroway) ||
191
+ tags.power ||
192
+ tags.man_made ||
193
+ tags.military ||
194
+ tags.emergency ||
195
+ tags.office ||
196
+ tags.government ||
197
+ tags.boundary === 'protected_area' ||
198
+ tags.protect_class ||
199
+ tags.landuse === 'industrial' ||
200
+ tags.industrial
201
+ );
202
+ }
203
+
204
+ /**
205
+ * @param {string | undefined} value
206
+ * @returns {boolean}
207
+ */
208
+ function isRailwayPoi(value) {
209
+ return ['station', 'halt', 'tram_stop', 'subway_entrance', 'stop'].includes(String(value));
210
+ }
211
+
212
+ /**
213
+ * @param {string | undefined} value
214
+ * @returns {boolean}
215
+ */
216
+ function isPublicTransportPoi(value) {
217
+ return ['station', 'stop_area'].includes(String(value));
218
+ }
219
+
220
+ /**
221
+ * @param {string | undefined} value
222
+ * @returns {boolean}
223
+ */
224
+ function isAerowayPoi(value) {
225
+ return ['aerodrome', 'airport', 'heliport', 'helipad', 'terminal'].includes(String(value));
226
+ }
227
+
228
+ /**
229
+ * Build the table properties for a logical layer.
230
+ *
231
+ * @param {string} layer
232
+ * @param {{ id: string | number, type: string, tags: Record<string, string> }} entity
233
+ * @returns {Record<string, string | null>}
234
+ */
235
+ export function propertiesForLayer(layer, entity) {
236
+ const definition = LAYER_DEFINITIONS[layer];
237
+ const tags = entity.tags;
238
+ /** @type {Record<string, string | null>} */
239
+ const properties = {};
240
+
241
+ for (const column of definition.columns) {
242
+ if (column === 'id') {
243
+ properties[column] = `${entity.type}/${entity.id}`;
244
+ } else if (column === 'poi_category') {
245
+ properties[column] = layer === 'pois' ? poiCategory(tags) : null;
246
+ } else {
247
+ properties[column] = tags[column] ?? null;
248
+ }
249
+ }
250
+
251
+ return properties;
252
+ }
253
+
254
+ /**
255
+ * Classify operational POIs into broad infrastructure categories.
256
+ *
257
+ * @param {Record<string, string>} tags
258
+ * @returns {string}
259
+ */
260
+ function poiCategory(tags) {
261
+ const amenity = tags.amenity;
262
+ const tourism = tags.tourism;
263
+ const shop = tags.shop;
264
+ const leisure = tags.leisure;
265
+ const railway = tags.railway;
266
+ const publicTransport = tags.public_transport;
267
+ const station = tags.station;
268
+ const aeroway = tags.aeroway;
269
+ const power = tags.power;
270
+ const manMade = tags.man_made;
271
+ const towerType = tags['tower:type'];
272
+ const military = tags.military;
273
+ const emergency = tags.emergency;
274
+ const office = tags.office;
275
+ const government = tags.government;
276
+ const boundary = tags.boundary;
277
+ const protectClass = tags.protect_class;
278
+ const landuse = tags.landuse;
279
+ const industrial = tags.industrial;
280
+
281
+ if (
282
+ ['railway_station', 'train_station', 'subway_station', 'bus_station', 'ferry_terminal'].includes(String(amenity)) ||
283
+ railway === 'station' ||
284
+ publicTransport === 'station' ||
285
+ station === 'subway' ||
286
+ aeroway === 'aerodrome' ||
287
+ aeroway === 'airport' ||
288
+ amenity === 'airport'
289
+ ) {
290
+ return 'transport';
291
+ }
292
+
293
+ if (['hospital', 'police', 'fire_station', 'shelter'].includes(String(amenity)) || isOperationalEmergency(emergency)) {
294
+ return 'emergency';
295
+ }
296
+
297
+ if (['townhall', 'courthouse', 'prison', 'embassy'].includes(String(amenity)) || office === 'government' || government) {
298
+ return 'government';
299
+ }
300
+
301
+ if (power) {
302
+ return 'energy';
303
+ }
304
+
305
+ if (
306
+ amenity === 'communications_tower' ||
307
+ manMade === 'communications_tower' ||
308
+ manMade === 'mast' ||
309
+ manMade === 'antenna' ||
310
+ towerType === 'communication' ||
311
+ tags.radar
312
+ ) {
313
+ return 'communications';
314
+ }
315
+
316
+ if (boundary === 'protected_area' || leisure === 'nature_reserve' || tourism === 'national_park' || protectClass) {
317
+ return 'protected';
318
+ }
319
+
320
+ if (landuse === 'industrial' || industrial || ['depot', 'warehouse'].includes(String(amenity))) {
321
+ return 'industrial';
322
+ }
323
+
324
+ if (military || ['bunker', 'checkpoint'].includes(String(amenity))) {
325
+ return 'military';
326
+ }
327
+
328
+ if (shop || tourism || leisure || ['restaurant', 'cafe', 'bar', 'fast_food', 'pub'].includes(String(amenity))) {
329
+ return 'consumer';
330
+ }
331
+
332
+ return 'operational';
333
+ }
334
+
335
+ /**
336
+ * @param {string | undefined} value
337
+ * @returns {boolean}
338
+ */
339
+ function isOperationalEmergency(value) {
340
+ return ['ambulance_station', 'siren', 'assembly_point', 'disaster_response'].includes(String(value));
341
+ }
342
+
343
+ /**
344
+ * @param {Record<string, string>} tags
345
+ * @returns {boolean}
346
+ */
347
+ function isWater(tags) {
348
+ return tags.natural === 'water' || tags.waterway === 'riverbank' || tags.landuse === 'reservoir';
349
+ }
350
+
351
+ /**
352
+ * @param {Record<string, string>} tags
353
+ * @returns {boolean}
354
+ */
355
+ function isLanduse(tags) {
356
+ return Boolean(tags.landuse || tags.leisure === 'park' || tags.natural === 'wood');
357
+ }
@@ -0,0 +1,29 @@
1
+ import { LAYER_DEFINITIONS } from './layers.js';
2
+ import { packageNameFromPath } from './utils.js';
3
+
4
+ /**
5
+ * Create a map-zero package manifest.
6
+ *
7
+ * @param {{ outDir: string, bbox: [number, number, number, number], layers: string[] }} options
8
+ * @returns {Record<string, unknown>}
9
+ */
10
+ export function createManifest(options) {
11
+ return {
12
+ format: 'mapzero',
13
+ version: 1,
14
+ name: packageNameFromPath(options.outDir),
15
+ bbox: options.bbox,
16
+ data: 'data.gpkg',
17
+ styles: {
18
+ default: 'styles/neon-dark.json',
19
+ 'neon-dark': 'styles/neon-dark.json'
20
+ },
21
+ layers: options.layers.map((layer) => ({
22
+ id: layer,
23
+ type: LAYER_DEFINITIONS[layer].type,
24
+ source: 'data.gpkg',
25
+ table: layer,
26
+ style: layer
27
+ }))
28
+ };
29
+ }