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,78 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+
4
+ import {
5
+ createStyleFromPreset,
6
+ createStyleFromTheme,
7
+ listStylePresets,
8
+ listStyleThemes,
9
+ readStyleTheme
10
+ } from './style.js';
11
+
12
+ /**
13
+ * Rewrite a package style file without touching data.gpkg or tiles.pmtiles.
14
+ *
15
+ * @param {{ packageDir: string, preset?: string, theme?: string }} options
16
+ * @returns {Promise<{ stylePath: string, styleUrl: string, name: string, sourceType: 'preset' | 'theme' }>}
17
+ */
18
+ export async function writePackageStyle(options) {
19
+ const packageDir = resolve(options.packageDir);
20
+ if (options.preset && options.theme) {
21
+ throw new Error('use either --preset or --theme, not both');
22
+ }
23
+
24
+ const manifestPath = join(packageDir, 'manifest.json');
25
+ const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
26
+ if (manifest.format !== 'mapzero' || !Array.isArray(manifest.layers)) {
27
+ throw new Error('manifest must be a mapzero package with layers');
28
+ }
29
+
30
+ const selectedLayers = manifest.layers.map((layer) => String(layer.id));
31
+ const sourceType = options.theme ? 'theme' : 'preset';
32
+ const styleDocument = options.theme
33
+ ? createStyleFromTheme(options.theme, selectedLayers)
34
+ : createStyleFromPreset(options.preset ?? 'neon-dark', selectedLayers);
35
+ const name = String(styleDocument.name ?? (options.theme ? readStyleTheme(options.theme).name : options.preset) ?? 'style');
36
+ const styleFile = `${safeStyleFileName(name)}.json`;
37
+ const styleUrl = `styles/${styleFile}`;
38
+ const stylePath = join(packageDir, 'styles', styleFile);
39
+ await fs.mkdir(join(packageDir, 'styles'), { recursive: true });
40
+ await fs.writeFile(stylePath, `${JSON.stringify(styleDocument, null, 2)}\n`);
41
+
42
+ manifest.styles = {
43
+ ...(manifest.styles && typeof manifest.styles === 'object' ? manifest.styles : {}),
44
+ default: styleUrl,
45
+ [name]: styleUrl
46
+ };
47
+ await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
48
+
49
+ return {
50
+ stylePath,
51
+ styleUrl,
52
+ name,
53
+ sourceType
54
+ };
55
+ }
56
+
57
+ /**
58
+ * @returns {string[]}
59
+ */
60
+ export function availableStylePresets() {
61
+ return listStylePresets();
62
+ }
63
+
64
+ /**
65
+ * @returns {string[]}
66
+ */
67
+ export function availableStyleThemes() {
68
+ return listStyleThemes();
69
+ }
70
+
71
+ /**
72
+ * @param {string} name
73
+ * @returns {string}
74
+ */
75
+ function safeStyleFileName(name) {
76
+ const safe = name.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
77
+ return safe || 'style';
78
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Build server/export-side filters for style rules hidden with byProperty.*.visible=false.
3
+ *
4
+ * @param {Record<string, unknown>} manifest
5
+ * @param {Record<string, unknown> | null} style
6
+ * @returns {import('./gpkg-read.js').HiddenFilters}
7
+ */
8
+ export function createHiddenFilters(manifest, style) {
9
+ const filters = new Map();
10
+ const manifestLayers = Array.isArray(manifest.layers) ? manifest.layers : [];
11
+ const styleLayers = /** @type {Record<string, unknown> | undefined} */ (style?.layers);
12
+
13
+ if (!styleLayers || typeof styleLayers !== 'object') {
14
+ return filters;
15
+ }
16
+
17
+ for (const layer of manifestLayers) {
18
+ const layerRecord = /** @type {Record<string, unknown>} */ (layer);
19
+ const layerId = String(layerRecord.id);
20
+ const styleId = String(layerRecord.style ?? layerId);
21
+ const styleRule = /** @type {Record<string, unknown> | undefined} */ (styleLayers[styleId] ?? styleLayers[layerAlias(styleId)]);
22
+ const byProperty = /** @type {Record<string, unknown> | undefined} */ (styleRule?.byProperty);
23
+
24
+ if (!byProperty || typeof byProperty !== 'object') {
25
+ continue;
26
+ }
27
+
28
+ const layerFilters = new Map();
29
+ for (const [propertyName, values] of Object.entries(byProperty)) {
30
+ if (!values || typeof values !== 'object') {
31
+ continue;
32
+ }
33
+
34
+ for (const [value, overrides] of Object.entries(/** @type {Record<string, unknown>} */ (values))) {
35
+ if (styleOverrideVisible(overrides) === false) {
36
+ if (!layerFilters.has(propertyName)) {
37
+ layerFilters.set(propertyName, new Set());
38
+ }
39
+ layerFilters.get(propertyName).add(value);
40
+ }
41
+ }
42
+ }
43
+
44
+ if (layerFilters.size > 0) {
45
+ filters.set(layerId, layerFilters);
46
+ }
47
+ }
48
+
49
+ return filters;
50
+ }
51
+
52
+ /**
53
+ * @param {string} layerId
54
+ * @returns {string}
55
+ */
56
+ function layerAlias(layerId) {
57
+ if (layerId === 'aip') return 'aviation';
58
+ if (layerId === 'aviation') return 'aip';
59
+ return layerId;
60
+ }
61
+
62
+ /**
63
+ * @param {unknown} overrides
64
+ * @returns {boolean | undefined}
65
+ */
66
+ function styleOverrideVisible(overrides) {
67
+ if (!overrides || typeof overrides !== 'object') {
68
+ return undefined;
69
+ }
70
+
71
+ const record = /** @type {Record<string, unknown>} */ (overrides);
72
+ const visibility = record.visibility && typeof record.visibility === 'object'
73
+ ? /** @type {Record<string, unknown>} */ (record.visibility)
74
+ : null;
75
+ return /** @type {boolean | undefined} */ (visibility?.visible ?? record.visible);
76
+ }
@@ -0,0 +1,93 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { SUPPORTED_LAYERS } from './layers.js';
6
+
7
+ export const PRESETS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'styles', 'presets');
8
+ const PRESET_NAME_PATTERN = /^[a-z0-9-]+$/;
9
+
10
+ /**
11
+ * Load a portable JSON style preset and filter it to the selected package layers.
12
+ *
13
+ * @param {string} preset
14
+ * @param {string[]} selectedLayers
15
+ * @returns {Record<string, unknown>}
16
+ */
17
+ export function createStyleFromPreset(preset = 'neon-dark', selectedLayers = SUPPORTED_LAYERS) {
18
+ const style = readStylePreset(preset);
19
+ return filterStyleLayers(style, selectedLayers);
20
+ }
21
+
22
+ /**
23
+ * Backward-compatible default style factory.
24
+ *
25
+ * @param {string[]} selectedLayers
26
+ * @returns {Record<string, unknown>}
27
+ */
28
+ export function createNeonDarkStyle(selectedLayers = SUPPORTED_LAYERS) {
29
+ return createStyleFromPreset('neon-dark', selectedLayers);
30
+ }
31
+
32
+ /**
33
+ * List bundled JSON style presets.
34
+ *
35
+ * @returns {string[]}
36
+ */
37
+ export function listStylePresets() {
38
+ return readdirSync(PRESETS_DIR)
39
+ .filter((file) => file.endsWith('.json'))
40
+ .map((file) => file.slice(0, -'.json'.length))
41
+ .sort();
42
+ }
43
+
44
+ /**
45
+ * Read one bundled style preset from styles/presets.
46
+ *
47
+ * @param {string} preset
48
+ * @returns {Record<string, unknown>}
49
+ */
50
+ export function readStylePreset(preset) {
51
+ if (!PRESET_NAME_PATTERN.test(preset)) {
52
+ throw new Error(`invalid style preset name: ${preset}`);
53
+ }
54
+
55
+ const filePath = join(PRESETS_DIR, `${preset}.json`);
56
+ try {
57
+ return JSON.parse(readFileSync(filePath, 'utf8'));
58
+ } catch (error) {
59
+ const available = listStylePresets().join(', ');
60
+ throw new Error(`unknown style preset: ${preset}${available ? `; available presets: ${available}` : ''}`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Return a style containing only selected layers.
66
+ *
67
+ * @param {Record<string, unknown>} style
68
+ * @param {string[]} selectedLayers
69
+ * @returns {Record<string, unknown>}
70
+ */
71
+ export function filterStyleLayers(style, selectedLayers = SUPPORTED_LAYERS) {
72
+ const selected = new Set(selectedLayers);
73
+ const layers = style.layers && typeof style.layers === 'object'
74
+ ? /** @type {Record<string, unknown>} */ (style.layers)
75
+ : {};
76
+ const drawOrder = Array.isArray(style.drawOrder)
77
+ ? style.drawOrder.map(String).filter((layer) => selected.has(layer))
78
+ : Object.keys(layers).filter((layer) => selected.has(layer));
79
+
80
+ /** @type {Record<string, unknown>} */
81
+ const filteredLayers = {};
82
+ for (const layer of drawOrder) {
83
+ if (layers[layer]) {
84
+ filteredLayers[layer] = layers[layer];
85
+ }
86
+ }
87
+
88
+ return {
89
+ ...structuredClone(style),
90
+ drawOrder,
91
+ layers: filteredLayers
92
+ };
93
+ }
@@ -0,0 +1,235 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { SUPPORTED_LAYERS } from './layers.js';
6
+ import { filterStyleLayers, readStylePreset } from './style-presets.js';
7
+
8
+ export const THEMES_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'styles', 'themes');
9
+ const THEME_NAME_PATTERN = /^[a-z0-9-]+$/;
10
+ const COLOR_PATCHES = {
11
+ background: [{ path: ['background'] }],
12
+ roads: [
13
+ { path: ['layers', 'roads', 'stroke'] },
14
+ { path: ['layers', 'roads', 'body', 'color'] }
15
+ ],
16
+ roadsMajor: [
17
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'motorway', 'stroke'] },
18
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'motorway', 'body', 'color'] },
19
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'trunk', 'stroke'] },
20
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'trunk', 'body', 'color'] },
21
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'primary', 'stroke'] },
22
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'primary', 'body', 'color'] }
23
+ ],
24
+ roadsCasing: [
25
+ { path: ['layers', 'roads', 'casing', 'color'] },
26
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'motorway', 'casing', 'color'] },
27
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'trunk', 'casing', 'color'] },
28
+ { path: ['layers', 'roads', 'byProperty', 'highway', 'primary', 'casing', 'color'] }
29
+ ],
30
+ buildings: [
31
+ { path: ['layers', 'buildings', 'stroke'] },
32
+ { path: ['layers', 'buildings', 'body', 'color'] },
33
+ { path: ['layers', 'buildings', 'glow', 'color'] }
34
+ ],
35
+ water: [
36
+ { path: ['layers', 'water', 'stroke'] },
37
+ { path: ['layers', 'water', 'body', 'color'] },
38
+ { path: ['layers', 'water', 'glow', 'color'] }
39
+ ],
40
+ landuse: [
41
+ { path: ['layers', 'landuse', 'stroke'] },
42
+ { path: ['layers', 'landuse', 'body', 'color'] },
43
+ { path: ['layers', 'landuse', 'glow', 'color'] }
44
+ ],
45
+ labels: [
46
+ { path: ['labels', 'roads', 'fill'] },
47
+ { path: ['labels', 'aip', 'fill'] },
48
+ { path: ['labels', 'pois', 'fill'] },
49
+ { path: ['labels', 'priorityClasses', 'important', 'fill'] },
50
+ { path: ['labels', 'priorityClasses', 'normal', 'fill'] }
51
+ ],
52
+ critical: [
53
+ { path: ['labels', 'priorityClasses', 'critical', 'fill'] }
54
+ ]
55
+ };
56
+ const INTENSITY_PATCHES = {
57
+ roads: [
58
+ ['layers', 'roads', 'strokeOpacity'],
59
+ ['layers', 'roads', 'body', 'opacity']
60
+ ],
61
+ buildings: [
62
+ ['layers', 'buildings', 'strokeOpacity'],
63
+ ['layers', 'buildings', 'body', 'opacity'],
64
+ ['layers', 'buildings', 'glow', 'opacity']
65
+ ],
66
+ labels: [
67
+ ['labels', 'roads', 'opacity'],
68
+ ['labels', 'aip', 'opacity'],
69
+ ['labels', 'pois', 'opacity'],
70
+ ['labels', 'priorityClasses', 'important', 'opacity'],
71
+ ['labels', 'priorityClasses', 'normal', 'opacity']
72
+ ]
73
+ };
74
+
75
+ /**
76
+ * @typedef {{
77
+ * name?: string,
78
+ * base?: string,
79
+ * colors?: Record<string, string>,
80
+ * intensity?: Record<string, number>,
81
+ * overrides?: Record<string, unknown>
82
+ * }} StyleTheme
83
+ */
84
+
85
+ /**
86
+ * List bundled compact style themes.
87
+ *
88
+ * @returns {string[]}
89
+ */
90
+ export function listStyleThemes() {
91
+ return readdirSync(THEMES_DIR)
92
+ .filter((file) => file.endsWith('.theme.json'))
93
+ .map((file) => file.slice(0, -'.theme.json'.length))
94
+ .sort();
95
+ }
96
+
97
+ /**
98
+ * Read a bundled theme name or a local theme JSON file.
99
+ *
100
+ * @param {string} theme
101
+ * @returns {StyleTheme}
102
+ */
103
+ export function readStyleTheme(theme) {
104
+ const filePath = theme.endsWith('.json') || theme.includes('/') || theme.includes('\\')
105
+ ? resolve(theme)
106
+ : bundledThemePath(theme);
107
+ const document = JSON.parse(readFileSync(filePath, 'utf8'));
108
+ validateTheme(document, theme);
109
+ return /** @type {StyleTheme} */ (document);
110
+ }
111
+
112
+ /**
113
+ * Expand a compact theme into a full map-zero style document.
114
+ *
115
+ * @param {StyleTheme | string} theme
116
+ * @param {string[]} selectedLayers
117
+ * @returns {Record<string, unknown>}
118
+ */
119
+ export function createStyleFromTheme(theme, selectedLayers = SUPPORTED_LAYERS) {
120
+ const themeDocument = typeof theme === 'string' ? readStyleTheme(theme) : theme;
121
+ validateTheme(themeDocument, themeDocument.name ?? 'theme');
122
+ const base = readStylePreset(themeDocument.base ?? 'neon-dark');
123
+ const style = structuredClone(base);
124
+ style.name = themeDocument.name ?? base.name ?? 'theme';
125
+
126
+ applyThemeColors(style, themeDocument.colors ?? {});
127
+ applyThemeIntensity(style, themeDocument.intensity ?? {});
128
+ if (themeDocument.overrides && typeof themeDocument.overrides === 'object') {
129
+ mergeObject(style, themeDocument.overrides);
130
+ }
131
+
132
+ return filterStyleLayers(style, selectedLayers);
133
+ }
134
+
135
+ /**
136
+ * @param {string} themeName
137
+ * @returns {string}
138
+ */
139
+ function bundledThemePath(themeName) {
140
+ if (!THEME_NAME_PATTERN.test(themeName)) {
141
+ throw new Error(`invalid style theme name: ${themeName}`);
142
+ }
143
+
144
+ return join(THEMES_DIR, `${themeName}.theme.json`);
145
+ }
146
+
147
+ /**
148
+ * @param {unknown} value
149
+ * @param {string} source
150
+ */
151
+ function validateTheme(value, source) {
152
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
153
+ throw new Error(`style theme must be a JSON object: ${source}`);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {Record<string, unknown>} style
159
+ * @param {Record<string, string>} colors
160
+ */
161
+ function applyThemeColors(style, colors) {
162
+ for (const [key, color] of Object.entries(colors)) {
163
+ if (typeof color !== 'string') {
164
+ continue;
165
+ }
166
+
167
+ for (const patch of COLOR_PATCHES[key] ?? []) {
168
+ setPathIfPresent(style, patch.path, color);
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * @param {Record<string, unknown>} style
175
+ * @param {Record<string, number>} intensity
176
+ */
177
+ function applyThemeIntensity(style, intensity) {
178
+ for (const [key, value] of Object.entries(intensity)) {
179
+ const numeric = Number(value);
180
+ if (!Number.isFinite(numeric)) {
181
+ continue;
182
+ }
183
+
184
+ const opacity = Math.max(0, Math.min(1, numeric));
185
+ for (const path of INTENSITY_PATCHES[key] ?? []) {
186
+ setPathIfPresent(style, path, opacity);
187
+ }
188
+ }
189
+ }
190
+
191
+ /**
192
+ * @param {Record<string, unknown>} target
193
+ * @param {string[]} path
194
+ * @param {unknown} value
195
+ */
196
+ function setPathIfPresent(target, path, value) {
197
+ let current = /** @type {Record<string, unknown> | undefined} */ (target);
198
+ for (const segment of path.slice(0, -1)) {
199
+ const next = current?.[segment];
200
+ if (!next || typeof next !== 'object' || Array.isArray(next)) {
201
+ return;
202
+ }
203
+ current = /** @type {Record<string, unknown>} */ (next);
204
+ }
205
+
206
+ const key = path.at(-1);
207
+ if (current && key && Object.hasOwn(current, key)) {
208
+ current[key] = value;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * @param {Record<string, unknown>} target
214
+ * @param {Record<string, unknown>} source
215
+ */
216
+ function mergeObject(target, source) {
217
+ for (const [key, value] of Object.entries(source)) {
218
+ if (isPlainObject(value) && isPlainObject(target[key])) {
219
+ mergeObject(
220
+ /** @type {Record<string, unknown>} */ (target[key]),
221
+ /** @type {Record<string, unknown>} */ (value)
222
+ );
223
+ } else {
224
+ target[key] = structuredClone(value);
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * @param {unknown} value
231
+ * @returns {value is Record<string, unknown>}
232
+ */
233
+ function isPlainObject(value) {
234
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
235
+ }
package/src/style.js ADDED
@@ -0,0 +1,13 @@
1
+ export {
2
+ createNeonDarkStyle,
3
+ createStyleFromPreset,
4
+ filterStyleLayers,
5
+ listStylePresets,
6
+ readStylePreset
7
+ } from './style-presets.js';
8
+
9
+ export {
10
+ createStyleFromTheme,
11
+ listStyleThemes,
12
+ readStyleTheme
13
+ } from './style-themes.js';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Small in-memory LRU cache for generated MVT tiles.
3
+ */
4
+ export class TileCache {
5
+ /**
6
+ * @param {number} maxEntries
7
+ */
8
+ constructor(maxEntries) {
9
+ this.maxEntries = Math.max(0, Math.floor(maxEntries));
10
+ /** @type {Map<string, unknown>} */
11
+ this.entries = new Map();
12
+ }
13
+
14
+ /**
15
+ * @param {string} key
16
+ * @returns {unknown | undefined}
17
+ */
18
+ get(key) {
19
+ if (this.maxEntries <= 0) {
20
+ return undefined;
21
+ }
22
+
23
+ const value = this.entries.get(key);
24
+ if (value === undefined) {
25
+ return undefined;
26
+ }
27
+
28
+ this.entries.delete(key);
29
+ this.entries.set(key, value);
30
+ return value;
31
+ }
32
+
33
+ /**
34
+ * @param {string} key
35
+ * @param {unknown} value
36
+ */
37
+ set(key, value) {
38
+ if (this.maxEntries <= 0) {
39
+ return;
40
+ }
41
+
42
+ if (this.entries.has(key)) {
43
+ this.entries.delete(key);
44
+ }
45
+
46
+ this.entries.set(key, value);
47
+ while (this.entries.size > this.maxEntries) {
48
+ const oldest = this.entries.keys().next().value;
49
+ if (oldest === undefined) {
50
+ break;
51
+ }
52
+ this.entries.delete(oldest);
53
+ }
54
+ }
55
+
56
+ clear() {
57
+ this.entries.clear();
58
+ }
59
+ }