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,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,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
|
+
}
|