@map-zero/ol 0.3.2 → 0.4.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/LICENSE +21 -0
- package/package.json +2 -2
- package/src/index.js +10 -234
- package/src/labels.js +30 -825
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marcos Pérez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@map-zero/ol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenLayers integration helper for map-zero packages.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@map-zero/
|
|
25
|
+
"@map-zero/core": "0.4.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"ol": "10.10.0",
|
package/src/index.js
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
+
import { LruCache, cachedPromise } from '../../core/src/shared/cache.js';
|
|
1
2
|
import MVT from 'ol/format/MVT.js';
|
|
2
|
-
import TileLayer from 'ol/layer/Tile.js';
|
|
3
3
|
import WebGLVectorTileLayer from 'ol/layer/WebGLVectorTile.js';
|
|
4
4
|
import WebGLVectorTileLayerRenderer from 'ol/renderer/webgl/VectorTileLayer.js';
|
|
5
|
-
import ImageTileSource from 'ol/source/ImageTile.js';
|
|
6
5
|
import VectorTileSource from 'ol/source/VectorTile.js';
|
|
7
6
|
import { createXYZ } from 'ol/tilegrid.js';
|
|
8
7
|
import { PMTiles } from 'pmtiles';
|
|
9
8
|
|
|
10
|
-
import { isAipLayer, layerAlias } from '../../
|
|
11
|
-
import { pmtilesInfo } from '../../raster/src/shared/manifest.js';
|
|
12
|
-
import { clampInteger, clampNumber } from '../../raster/src/shared/math.js';
|
|
9
|
+
import { isAipLayer, layerAlias } from '../../core/src/shared/layers.js';
|
|
13
10
|
import { maxZoomExpression, minZoomExpression, zoomInterpolateExpression } from './zoom.js';
|
|
14
11
|
|
|
15
12
|
import {
|
|
@@ -57,12 +54,7 @@ const ROAD_HIGHWAY_DRAW_ORDER = [
|
|
|
57
54
|
* style?: string | Record<string, unknown>,
|
|
58
55
|
* visibleLayers?: string[] | Set<string>,
|
|
59
56
|
* source?: 'auto' | 'pmtiles' | 'dynamic',
|
|
60
|
-
* renderMode?: 'vector' | 'raster-worker',
|
|
61
57
|
* hitDetection?: boolean,
|
|
62
|
-
* workerUrl?: string | URL,
|
|
63
|
-
* rasterPixelRatio?: number,
|
|
64
|
-
* overzoomLevels?: number,
|
|
65
|
-
* edgeGuardPixels?: number,
|
|
66
58
|
* apiBaseUrl?: string,
|
|
67
59
|
* zIndexBase?: number,
|
|
68
60
|
* onTileLoadStart?: () => void,
|
|
@@ -135,9 +127,6 @@ export async function createMapZeroOpenLayersLayers(options) {
|
|
|
135
127
|
onTileLoadError: options.onTileLoadError
|
|
136
128
|
};
|
|
137
129
|
|
|
138
|
-
if (options.renderMode === 'raster-worker') {
|
|
139
|
-
return createRasterWorkerController(context, options);
|
|
140
|
-
}
|
|
141
130
|
|
|
142
131
|
patchWebGlVectorTileRenderer();
|
|
143
132
|
|
|
@@ -233,193 +222,6 @@ export async function createMapZeroOpenLayersLayers(options) {
|
|
|
233
222
|
* destroy: () => void
|
|
234
223
|
* }}
|
|
235
224
|
*/
|
|
236
|
-
function createRasterWorkerController(context, options) {
|
|
237
|
-
if (!isPmtilesManifest(context.manifest)) {
|
|
238
|
-
throw new Error('raster-worker render mode requires vector PMTiles');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const worker = new MapZeroRasterTileWorker({
|
|
242
|
-
manifest: context.manifest,
|
|
243
|
-
manifestUrl: context.manifestUrl,
|
|
244
|
-
styleDocument: context.styleDocument,
|
|
245
|
-
layers: context.orderedLayers.map((layer) => layer.id),
|
|
246
|
-
workerUrl: options.workerUrl ?? new URL('@map-zero/raster/imagery-worker.js', import.meta.url),
|
|
247
|
-
rasterPixelRatio: options.rasterPixelRatio,
|
|
248
|
-
overzoomLevels: options.overzoomLevels,
|
|
249
|
-
edgeGuardPixels: options.edgeGuardPixels
|
|
250
|
-
});
|
|
251
|
-
worker.setVisibleLayers(context.layerVisibility);
|
|
252
|
-
const range = pmtilesZoomRange(context.manifest);
|
|
253
|
-
const maxZoom = range.maxZoom + clampInteger(options.overzoomLevels ?? 0, 0, 4);
|
|
254
|
-
const rasterPixelRatio = clampNumber(options.rasterPixelRatio ?? 2, 1, 2);
|
|
255
|
-
const source = new ImageTileSource({
|
|
256
|
-
minZoom: range.minZoom,
|
|
257
|
-
maxZoom,
|
|
258
|
-
tileGrid: createXYZ({
|
|
259
|
-
minZoom: range.minZoom,
|
|
260
|
-
maxZoom,
|
|
261
|
-
tileSize: 512
|
|
262
|
-
}),
|
|
263
|
-
tileSize: 512,
|
|
264
|
-
transition: 0,
|
|
265
|
-
interpolate: true,
|
|
266
|
-
zDirection: preferNearestZoomLevel,
|
|
267
|
-
wrapX: false,
|
|
268
|
-
loader: (z, x, y) => worker.render(z, x, y)
|
|
269
|
-
});
|
|
270
|
-
source.getTilePixelRatio = () => rasterPixelRatio;
|
|
271
|
-
const layer = new TileLayer({
|
|
272
|
-
source,
|
|
273
|
-
cacheSize: 4096,
|
|
274
|
-
preload: 0,
|
|
275
|
-
useInterimTilesOnError: false
|
|
276
|
-
});
|
|
277
|
-
tagOpenLayersLayer(layer, context.instanceId, context.orderedLayers.map((item) => item.id), 'raster');
|
|
278
|
-
applyLayerZIndex(layer, context.orderedLayers, context.styleDocument, options.zIndexBase);
|
|
279
|
-
|
|
280
|
-
const refresh = () => {
|
|
281
|
-
worker.setVisibleLayers(context.layerVisibility);
|
|
282
|
-
refreshRasterSource(source);
|
|
283
|
-
layer.changed();
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
id: context.instanceId,
|
|
288
|
-
manifest: context.manifest,
|
|
289
|
-
style: context.styleDocument,
|
|
290
|
-
layers: [layer],
|
|
291
|
-
setVisible(layerId, visible) {
|
|
292
|
-
if (!context.layerVisibility.has(layerId)) {
|
|
293
|
-
throw new Error(`unknown map-zero layer: ${layerId}`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
context.layerVisibility.set(layerId, Boolean(visible));
|
|
297
|
-
refresh();
|
|
298
|
-
},
|
|
299
|
-
setOpacity(layerId, opacity) {
|
|
300
|
-
if (!context.layerOpacity?.has?.(layerId)) {
|
|
301
|
-
throw new Error(`unknown map-zero layer: ${layerId}`);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
layer.setOpacity(clamp(Number(opacity), 0, 1));
|
|
305
|
-
},
|
|
306
|
-
destroy() {
|
|
307
|
-
refreshRasterSource(source);
|
|
308
|
-
worker.destroy();
|
|
309
|
-
layer.dispose();
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function refreshRasterSource(source) {
|
|
315
|
-
source.clear();
|
|
316
|
-
source.refresh?.();
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
class MapZeroRasterTileWorker {
|
|
320
|
-
/**
|
|
321
|
-
* @param {{
|
|
322
|
-
* manifest: Record<string, unknown>,
|
|
323
|
-
* manifestUrl: string,
|
|
324
|
-
* styleDocument: Record<string, unknown>,
|
|
325
|
-
* layers: string[],
|
|
326
|
-
* workerUrl: string | URL,
|
|
327
|
-
* rasterPixelRatio?: number,
|
|
328
|
-
* overzoomLevels?: number,
|
|
329
|
-
* edgeGuardPixels?: number
|
|
330
|
-
* }} options
|
|
331
|
-
*/
|
|
332
|
-
constructor(options) {
|
|
333
|
-
assertRasterWorkerSupport();
|
|
334
|
-
this.#rasterPixelRatio = clampNumber(options.rasterPixelRatio ?? 2, 1, 2);
|
|
335
|
-
this.worker = new Worker(options.workerUrl, { type: 'module' });
|
|
336
|
-
this.worker.addEventListener('message', (event) => this.#handleMessage(event.data));
|
|
337
|
-
this.worker.addEventListener('error', (event) => this.#rejectAll(new Error(event.message || 'map-zero OpenLayers raster worker failed')));
|
|
338
|
-
this.worker.postMessage({
|
|
339
|
-
type: 'init',
|
|
340
|
-
options: {
|
|
341
|
-
manifest: options.manifest,
|
|
342
|
-
manifestUrl: resolveUrl(options.manifestUrl, documentBaseUrl()),
|
|
343
|
-
styleDocument: options.styleDocument,
|
|
344
|
-
layers: options.layers,
|
|
345
|
-
tileSize: 512,
|
|
346
|
-
pixelRatio: this.#rasterPixelRatio,
|
|
347
|
-
sourceMaximumLevel: pmtilesZoomRange(options.manifest).maxZoom,
|
|
348
|
-
overzoomLevels: clampInteger(options.overzoomLevels ?? 0, 0, 4),
|
|
349
|
-
edgeGuardPixels: clampInteger(options.edgeGuardPixels ?? 0, 0, 8),
|
|
350
|
-
source: String(pmtilesInfo(options.manifest).url ?? 'tiles.pmtiles')
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* @param {number} z
|
|
357
|
-
* @param {number} x
|
|
358
|
-
* @param {number} y
|
|
359
|
-
* @returns {Promise<ImageBitmap | HTMLCanvasElement>}
|
|
360
|
-
*/
|
|
361
|
-
render(z, x, y) {
|
|
362
|
-
const id = this.#nextId++;
|
|
363
|
-
return new Promise((resolve, reject) => {
|
|
364
|
-
this.#pending.set(id, { resolve, reject });
|
|
365
|
-
this.worker.postMessage({ type: 'render', id, x, y, z });
|
|
366
|
-
}).catch(() => emptyCanvas(512 * this.#rasterPixelRatio, 512 * this.#rasterPixelRatio));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* @param {Map<string, boolean>} visibility
|
|
371
|
-
*/
|
|
372
|
-
setVisibleLayers(visibility) {
|
|
373
|
-
for (const [layerId, visible] of visibility) {
|
|
374
|
-
this.worker.postMessage({ type: 'visibility', layerId, visible });
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
destroy() {
|
|
379
|
-
this.#rejectAll(new Error('map-zero OpenLayers raster worker destroyed'));
|
|
380
|
-
this.worker.terminate();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* @param {any} message
|
|
385
|
-
*/
|
|
386
|
-
#handleMessage(message) {
|
|
387
|
-
if (message?.type !== 'tile') {
|
|
388
|
-
return;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const pending = this.#pending.get(message.id);
|
|
392
|
-
if (!pending) {
|
|
393
|
-
message.image?.close?.();
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
this.#pending.delete(message.id);
|
|
398
|
-
if (message.error) {
|
|
399
|
-
pending.reject(new Error(message.error));
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
pending.resolve(message.image ?? emptyCanvas(512, 512));
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* @param {Error} error
|
|
407
|
-
*/
|
|
408
|
-
#rejectAll(error) {
|
|
409
|
-
for (const pending of this.#pending.values()) {
|
|
410
|
-
pending.reject(error);
|
|
411
|
-
}
|
|
412
|
-
this.#pending.clear();
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/** @type {Worker} */
|
|
416
|
-
worker;
|
|
417
|
-
#rasterPixelRatio = 2;
|
|
418
|
-
#nextId = 1;
|
|
419
|
-
/** @type {Map<number, { resolve: (image: ImageBitmap | HTMLCanvasElement) => void, reject: (error: Error) => void }>} */
|
|
420
|
-
#pending = new Map();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
225
|
/**
|
|
424
226
|
* Add map-zero layers to an existing OpenLayers map.
|
|
425
227
|
*
|
|
@@ -580,7 +382,7 @@ function applyLayerZIndex(layer, orderedLayers, styleDocument, zIndexBase) {
|
|
|
580
382
|
* @param {unknown} layer
|
|
581
383
|
* @param {string} instanceId
|
|
582
384
|
* @param {string[]} layerIds
|
|
583
|
-
* @param {'geometry' | 'labels'
|
|
385
|
+
* @param {'geometry' | 'labels'} role
|
|
584
386
|
*/
|
|
585
387
|
function tagOpenLayersLayer(layer, instanceId, layerIds, role) {
|
|
586
388
|
const namespacedLayerIds = layerIds.map((layerId) => namespaceLayerId(instanceId, layerId));
|
|
@@ -879,8 +681,14 @@ function createLabelTileDataLoader(context) {
|
|
|
879
681
|
* @returns {PMTiles}
|
|
880
682
|
*/
|
|
881
683
|
function createPmtilesArchive(context) {
|
|
684
|
+
if (context.archive) return context.archive;
|
|
882
685
|
const tiles = /** @type {{ url: string }} */ (context.manifest.tiles);
|
|
883
|
-
|
|
686
|
+
const archive = new PMTiles(resolveUrl(tiles.url, context.manifestBaseUrl));
|
|
687
|
+
const cache = new LruCache(128);
|
|
688
|
+
const getZxy = archive.getZxy.bind(archive);
|
|
689
|
+
archive.getZxy = (z, x, y) => cachedPromise(cache, `${z}/${x}/${y}`, () => getZxy(z, x, y));
|
|
690
|
+
context.archive = archive;
|
|
691
|
+
return archive;
|
|
884
692
|
}
|
|
885
693
|
|
|
886
694
|
/**
|
|
@@ -2099,38 +1907,6 @@ function isValidTileCoord(z, x, y) {
|
|
|
2099
1907
|
return Number.isInteger(x) && Number.isInteger(y) && x >= 0 && y >= 0 && x < maxIndex && y < maxIndex;
|
|
2100
1908
|
}
|
|
2101
1909
|
|
|
2102
|
-
/**
|
|
2103
|
-
* @param {number} width
|
|
2104
|
-
* @param {number} height
|
|
2105
|
-
* @returns {HTMLCanvasElement}
|
|
2106
|
-
*/
|
|
2107
|
-
function emptyCanvas(width, height) {
|
|
2108
|
-
const canvas = document.createElement('canvas');
|
|
2109
|
-
canvas.width = width;
|
|
2110
|
-
canvas.height = height;
|
|
2111
|
-
return canvas;
|
|
2112
|
-
}
|
|
2113
|
-
|
|
2114
|
-
function assertRasterWorkerSupport() {
|
|
2115
|
-
if (typeof Worker !== 'function' || typeof OffscreenCanvas !== 'function' || typeof createImageBitmap !== 'function') {
|
|
2116
|
-
throw new Error('map-zero OpenLayers raster-worker mode requires Worker, OffscreenCanvas, and createImageBitmap');
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
/**
|
|
2121
|
-
* Choose the raster tile zoom at the midpoint in zoom space instead of always
|
|
2122
|
-
* forcing the parent or child tile. This keeps fractional zooms sharp without
|
|
2123
|
-
* holding low-resolution tiles for too long.
|
|
2124
|
-
*
|
|
2125
|
-
* @param {number} value
|
|
2126
|
-
* @param {number} high
|
|
2127
|
-
* @param {number} low
|
|
2128
|
-
* @returns {number}
|
|
2129
|
-
*/
|
|
2130
|
-
function preferNearestZoomLevel(value, high, low) {
|
|
2131
|
-
return value - low * Math.sqrt(high / low);
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
1910
|
/**
|
|
2135
1911
|
* @param {number} z
|
|
2136
1912
|
* @param {number} x
|
package/src/labels.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LruCache } from '../../core/src/shared/cache.js';
|
|
1
2
|
import Feature from 'ol/Feature.js';
|
|
2
3
|
import MVT from 'ol/format/MVT.js';
|
|
3
4
|
import VectorTileLayer from 'ol/layer/VectorTile.js';
|
|
@@ -7,153 +8,34 @@ import Stroke from 'ol/style/Stroke.js';
|
|
|
7
8
|
import Style from 'ol/style/Style.js';
|
|
8
9
|
import Text from 'ol/style/Text.js';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
activeLabelLayerIdsForZoom,
|
|
13
|
+
hasEnabledLabels,
|
|
14
|
+
isPoiLikeFeature,
|
|
15
|
+
isSelectedPoiCandidate,
|
|
16
|
+
roadLabel,
|
|
17
|
+
aviationLabel,
|
|
18
|
+
isSelectedPoi,
|
|
19
|
+
poiLabelText,
|
|
20
|
+
priorityClassFromNumber,
|
|
21
|
+
poiPriorityClass,
|
|
22
|
+
poiZIndex,
|
|
23
|
+
labelFont,
|
|
24
|
+
priorityClassRule,
|
|
25
|
+
labelOpacityForZoom,
|
|
26
|
+
cleanText,
|
|
27
|
+
isMeaningfulLabel,
|
|
28
|
+
isExplicitAviationLabel,
|
|
29
|
+
labelRuleForSource,
|
|
30
|
+
zoomInRule,
|
|
31
|
+
resolutionToZoom,
|
|
32
|
+
rgba,
|
|
33
|
+
LABEL_SOURCE_LAYERS,
|
|
34
|
+
ROAD_SOURCE_LAYER,
|
|
35
|
+
POI_SOURCE_LAYER
|
|
36
|
+
} from '../../core/src/labels.js';
|
|
37
|
+
export { activeLabelLayerIdsForZoom, hasEnabledLabels } from '../../core/src/labels.js';
|
|
14
38
|
|
|
15
|
-
const MAJOR_ROADS = new Set(['motorway', 'motorway_link', 'trunk', 'trunk_link', 'primary', 'primary_link']);
|
|
16
|
-
const SECONDARY_ROADS = new Set(['secondary', 'secondary_link']);
|
|
17
|
-
const LOCAL_ROADS = new Set(['residential', 'living_street', 'unclassified']);
|
|
18
|
-
const DISABLED_ROADS = new Set(['service', 'track', 'path', 'footway', 'cycleway', 'steps', 'corridor', 'platform']);
|
|
19
|
-
const LABEL_TEXT_FIELDS = ['name', 'ref', 'iata', 'icao', 'operator', 'official_name', 'short_name'];
|
|
20
|
-
const GENERIC_LABEL_VALUES = new Set([
|
|
21
|
-
'yes',
|
|
22
|
-
'no',
|
|
23
|
-
'true',
|
|
24
|
-
'false',
|
|
25
|
-
'unknown',
|
|
26
|
-
'none',
|
|
27
|
-
'generator',
|
|
28
|
-
'tower',
|
|
29
|
-
'line',
|
|
30
|
-
'plant',
|
|
31
|
-
'substation',
|
|
32
|
-
'transformer',
|
|
33
|
-
'mast',
|
|
34
|
-
'antenna',
|
|
35
|
-
'station',
|
|
36
|
-
'airport',
|
|
37
|
-
'aerodrome',
|
|
38
|
-
'runway',
|
|
39
|
-
'taxiway',
|
|
40
|
-
'terminal',
|
|
41
|
-
'apron',
|
|
42
|
-
'pharmacy',
|
|
43
|
-
'fuel',
|
|
44
|
-
'charging_station',
|
|
45
|
-
'hospital',
|
|
46
|
-
'clinic',
|
|
47
|
-
'police',
|
|
48
|
-
'fire_station',
|
|
49
|
-
'fire_hydrant',
|
|
50
|
-
'defibrillator',
|
|
51
|
-
'fire_extinguisher',
|
|
52
|
-
'shelter',
|
|
53
|
-
'railway_station',
|
|
54
|
-
'train_station',
|
|
55
|
-
'subway_station',
|
|
56
|
-
'bus_station',
|
|
57
|
-
'ferry_terminal',
|
|
58
|
-
'townhall',
|
|
59
|
-
'courthouse',
|
|
60
|
-
'prison',
|
|
61
|
-
'bunker',
|
|
62
|
-
'checkpoint',
|
|
63
|
-
'communications_tower',
|
|
64
|
-
'protected_area',
|
|
65
|
-
'nature_reserve',
|
|
66
|
-
'restaurant',
|
|
67
|
-
'cafe',
|
|
68
|
-
'bar',
|
|
69
|
-
'fast_food',
|
|
70
|
-
'pub',
|
|
71
|
-
'shop',
|
|
72
|
-
'retail',
|
|
73
|
-
'commercial',
|
|
74
|
-
'tourism',
|
|
75
|
-
'attraction',
|
|
76
|
-
'hotel',
|
|
77
|
-
'transport',
|
|
78
|
-
'emergency',
|
|
79
|
-
'government',
|
|
80
|
-
'energy',
|
|
81
|
-
'communications',
|
|
82
|
-
'protected',
|
|
83
|
-
'industrial',
|
|
84
|
-
'military',
|
|
85
|
-
'operational',
|
|
86
|
-
'consumer'
|
|
87
|
-
]);
|
|
88
|
-
const DEFAULT_POI_CLASSES = {
|
|
89
|
-
amenity: [
|
|
90
|
-
'hospital',
|
|
91
|
-
'police',
|
|
92
|
-
'fire_station',
|
|
93
|
-
'bus_station',
|
|
94
|
-
'ferry_terminal',
|
|
95
|
-
'shelter',
|
|
96
|
-
'townhall',
|
|
97
|
-
'courthouse',
|
|
98
|
-
'prison',
|
|
99
|
-
'ranger_station',
|
|
100
|
-
'research_institute',
|
|
101
|
-
'airport',
|
|
102
|
-
'railway_station',
|
|
103
|
-
'train_station',
|
|
104
|
-
'subway_station',
|
|
105
|
-
'communications_tower',
|
|
106
|
-
'bunker',
|
|
107
|
-
'checkpoint',
|
|
108
|
-
'depot',
|
|
109
|
-
'warehouse'
|
|
110
|
-
],
|
|
111
|
-
tourism: ['information'],
|
|
112
|
-
shop: [],
|
|
113
|
-
leisure: ['nature_reserve'],
|
|
114
|
-
railway: ['station'],
|
|
115
|
-
public_transport: ['station'],
|
|
116
|
-
station: ['subway'],
|
|
117
|
-
aeroway: ['aerodrome', 'airport'],
|
|
118
|
-
power: ['plant', 'substation', 'generator', 'tower', 'transformer', 'line'],
|
|
119
|
-
man_made: ['communications_tower', 'mast', 'antenna'],
|
|
120
|
-
'tower:type': ['communication'],
|
|
121
|
-
military: ['barracks', 'bunker', 'checkpoint', 'airbase', 'naval_base', 'range', 'training_area', 'base', 'airfield', 'danger_area', 'ammunition_dump'],
|
|
122
|
-
emergency: ['ambulance_station', 'siren', 'assembly_point', 'disaster_response'],
|
|
123
|
-
office: ['government'],
|
|
124
|
-
government: ['yes', 'public_safety', 'border_control', 'customs', 'ministry', 'aerospace'],
|
|
125
|
-
boundary: ['protected_area'],
|
|
126
|
-
industrial: ['refinery', 'depot', 'storage', 'logistics'],
|
|
127
|
-
landuse: ['industrial']
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Tiny full-label bitmap atlas kept for future point-label experiments.
|
|
132
|
-
*/
|
|
133
|
-
export class LabelAtlas {
|
|
134
|
-
constructor() {
|
|
135
|
-
this.map = new Map();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Create a standard OpenLayers vector tile label layer.
|
|
141
|
-
*
|
|
142
|
-
* Geometry rendering stays on the existing WebGLVectorTileLayer. This layer is
|
|
143
|
-
* only for labels, using OpenLayers text placement and decluttering.
|
|
144
|
-
*
|
|
145
|
-
* @param {{
|
|
146
|
-
* instanceId?: string,
|
|
147
|
-
* tileUrlFunction: (tileCoord: number[] | null) => string | undefined,
|
|
148
|
-
* loadTileData: (tileCoord: number[], url: string | undefined) => Promise<ArrayBuffer | Uint8Array | null>,
|
|
149
|
-
* sourceOptions?: Record<string, unknown>,
|
|
150
|
-
* styleDocument: Record<string, unknown>,
|
|
151
|
-
* onTileLoadStart?: () => void,
|
|
152
|
-
* onTileLoadEnd?: () => void,
|
|
153
|
-
* onTileLoadError?: () => void
|
|
154
|
-
* }} options
|
|
155
|
-
* @returns {{ layer: VectorTileLayer, source: VectorTileSource, attachMap: () => void, detachMap: () => void, refresh: () => void, destroy: () => void }}
|
|
156
|
-
*/
|
|
157
39
|
export function createMapZeroLabelLayer(options) {
|
|
158
40
|
// The geometry layer already decodes every map layer. Labels only need these
|
|
159
41
|
// source layers, so do not create Canvas features for buildings, water or
|
|
@@ -199,7 +81,7 @@ export function createMapZeroLabelLayer(options) {
|
|
|
199
81
|
if (options.onTileLoadEnd) source.on('tileloadend', options.onTileLoadEnd);
|
|
200
82
|
if (options.onTileLoadError) source.on('tileloaderror', options.onTileLoadError);
|
|
201
83
|
|
|
202
|
-
const styleCache = new
|
|
84
|
+
const styleCache = new LruCache(2048);
|
|
203
85
|
const layer = new VectorTileLayer({
|
|
204
86
|
source,
|
|
205
87
|
declutter: true,
|
|
@@ -225,60 +107,13 @@ export function createMapZeroLabelLayer(options) {
|
|
|
225
107
|
layer.changed();
|
|
226
108
|
},
|
|
227
109
|
destroy() {
|
|
110
|
+
styleCache.clear();
|
|
228
111
|
source.clear();
|
|
229
112
|
layer.dispose();
|
|
230
113
|
}
|
|
231
114
|
};
|
|
232
115
|
}
|
|
233
116
|
|
|
234
|
-
/**
|
|
235
|
-
* Return source MVT layer ids needed by the label renderer.
|
|
236
|
-
*
|
|
237
|
-
* @param {Array<{ id: string, type?: string, style?: string }>} orderedLayers
|
|
238
|
-
* @param {Record<string, unknown>} styleDocument
|
|
239
|
-
* @param {Map<string, boolean>} layerVisibility
|
|
240
|
-
* @param {number} zoom
|
|
241
|
-
* @returns {string[]}
|
|
242
|
-
*/
|
|
243
|
-
export function activeLabelLayerIdsForZoom(orderedLayers, styleDocument, layerVisibility, zoom) {
|
|
244
|
-
const labels = labelConfigRoot(styleDocument);
|
|
245
|
-
if (!labels || labels.enabled === false) {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return LABEL_SOURCE_LAYERS.filter((layerId) => {
|
|
250
|
-
const rule = labelRuleForSource(styleDocument, layerId);
|
|
251
|
-
if (!rule || rule.enabled === false) {
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (!orderedLayers.some((layer) => layer.id === layerId) || !layerVisibility.get(layerId)) {
|
|
256
|
-
return false;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return zoomInRule(zoom, rule);
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* @param {Record<string, unknown>} styleDocument
|
|
265
|
-
* @returns {boolean}
|
|
266
|
-
*/
|
|
267
|
-
export function hasEnabledLabels(styleDocument) {
|
|
268
|
-
const labels = labelConfigRoot(styleDocument);
|
|
269
|
-
return Boolean(labels && labels.enabled !== false && LABEL_SOURCE_LAYERS.some((layerId) => {
|
|
270
|
-
const rule = labelRuleForSource(styleDocument, layerId);
|
|
271
|
-
return rule && rule.enabled !== false;
|
|
272
|
-
}));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* @param {Feature} feature
|
|
277
|
-
* @param {number} resolution
|
|
278
|
-
* @param {Record<string, unknown>} styleDocument
|
|
279
|
-
* @param {Map<string, Style>} styleCache
|
|
280
|
-
* @returns {Style | null}
|
|
281
|
-
*/
|
|
282
117
|
function labelStyle(feature, resolution, styleDocument, styleCache) {
|
|
283
118
|
const zoom = resolutionToZoom(resolution);
|
|
284
119
|
const sourceLayer = cleanText(feature.get('sourceLayer'));
|
|
@@ -301,42 +136,6 @@ function labelStyle(feature, resolution, styleDocument, styleCache) {
|
|
|
301
136
|
return null;
|
|
302
137
|
}
|
|
303
138
|
|
|
304
|
-
/**
|
|
305
|
-
* @param {Feature} feature
|
|
306
|
-
* @returns {boolean}
|
|
307
|
-
*/
|
|
308
|
-
function isPoiLikeFeature(feature) {
|
|
309
|
-
return [
|
|
310
|
-
'poi_category',
|
|
311
|
-
'amenity',
|
|
312
|
-
'tourism',
|
|
313
|
-
'shop',
|
|
314
|
-
'leisure',
|
|
315
|
-
'railway',
|
|
316
|
-
'public_transport',
|
|
317
|
-
'station',
|
|
318
|
-
'power',
|
|
319
|
-
'man_made',
|
|
320
|
-
'tower:type',
|
|
321
|
-
'military',
|
|
322
|
-
'emergency',
|
|
323
|
-
'office',
|
|
324
|
-
'government',
|
|
325
|
-
'boundary',
|
|
326
|
-
'protect_class',
|
|
327
|
-
'landuse',
|
|
328
|
-
'industrial'
|
|
329
|
-
].some((property) => cleanText(feature.get(property)));
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* @param {Feature} feature
|
|
334
|
-
* @param {string} sourceLayer
|
|
335
|
-
* @param {number} zoom
|
|
336
|
-
* @param {Record<string, unknown>} styleDocument
|
|
337
|
-
* @param {Map<string, Style>} styleCache
|
|
338
|
-
* @returns {Style | null}
|
|
339
|
-
*/
|
|
340
139
|
function candidateLabelStyle(feature, sourceLayer, zoom, styleDocument, styleCache) {
|
|
341
140
|
const rule = labelRuleForSource(styleDocument, sourceLayer);
|
|
342
141
|
const minZoom = Number(feature.get('minZoom') ?? rule?.minZoom ?? 0);
|
|
@@ -365,28 +164,6 @@ function candidateLabelStyle(feature, sourceLayer, zoom, styleDocument, styleCac
|
|
|
365
164
|
});
|
|
366
165
|
}
|
|
367
166
|
|
|
368
|
-
/**
|
|
369
|
-
* @param {Feature} feature
|
|
370
|
-
* @param {Record<string, unknown>} rule
|
|
371
|
-
* @returns {boolean}
|
|
372
|
-
*/
|
|
373
|
-
function isSelectedPoiCandidate(feature, rule) {
|
|
374
|
-
const className = cleanText(feature.get('class'));
|
|
375
|
-
if (!className || ['clinic', 'pharmacy', 'fuel', 'charging_station', 'fire_hydrant', 'defibrillator', 'fire_extinguisher', 'restaurant', 'cafe', 'bar', 'fast_food', 'pub'].includes(className)) {
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const selected = objectRule(rule.classes) ?? DEFAULT_POI_CLASSES;
|
|
380
|
-
return Object.values(selected).some((values) => Array.isArray(values) && values.map(String).includes(className));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* @param {Feature} feature
|
|
385
|
-
* @param {number} zoom
|
|
386
|
-
* @param {Record<string, unknown>} styleDocument
|
|
387
|
-
* @param {Map<string, Style>} styleCache
|
|
388
|
-
* @returns {Style | null}
|
|
389
|
-
*/
|
|
390
167
|
function roadLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
391
168
|
const rule = labelRuleForSource(styleDocument, ROAD_SOURCE_LAYER);
|
|
392
169
|
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule)) {
|
|
@@ -411,13 +188,6 @@ function roadLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
|
411
188
|
});
|
|
412
189
|
}
|
|
413
190
|
|
|
414
|
-
/**
|
|
415
|
-
* @param {Feature} feature
|
|
416
|
-
* @param {number} zoom
|
|
417
|
-
* @param {Record<string, unknown>} styleDocument
|
|
418
|
-
* @param {Map<string, Style>} styleCache
|
|
419
|
-
* @returns {Style | null}
|
|
420
|
-
*/
|
|
421
191
|
function aviationLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
422
192
|
const rule = labelRuleForSource(styleDocument, 'aip');
|
|
423
193
|
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule)) {
|
|
@@ -442,13 +212,6 @@ function aviationLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
|
442
212
|
});
|
|
443
213
|
}
|
|
444
214
|
|
|
445
|
-
/**
|
|
446
|
-
* @param {Feature} feature
|
|
447
|
-
* @param {number} zoom
|
|
448
|
-
* @param {Record<string, unknown>} styleDocument
|
|
449
|
-
* @param {Map<string, Style>} styleCache
|
|
450
|
-
* @returns {Style | null}
|
|
451
|
-
*/
|
|
452
215
|
function poiLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
453
216
|
const rule = labelRuleForSource(styleDocument, POI_SOURCE_LAYER);
|
|
454
217
|
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule) || !isSelectedPoi(feature, rule)) {
|
|
@@ -472,107 +235,6 @@ function poiLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
|
472
235
|
});
|
|
473
236
|
}
|
|
474
237
|
|
|
475
|
-
/**
|
|
476
|
-
* @param {Feature} feature
|
|
477
|
-
* @param {string} highway
|
|
478
|
-
* @param {number} zoom
|
|
479
|
-
* @param {Record<string, unknown>} rule
|
|
480
|
-
* @returns {{ text: string, priorityClass: string, zIndex: number } | null}
|
|
481
|
-
*/
|
|
482
|
-
function roadLabel(feature, highway, zoom, rule) {
|
|
483
|
-
if (!highway || DISABLED_ROADS.has(highway)) {
|
|
484
|
-
return null;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const local = objectRule(rule.local);
|
|
488
|
-
if (LOCAL_ROADS.has(highway) && local?.enabled !== true) {
|
|
489
|
-
return null;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const ref = cleanText(feature.get('ref'));
|
|
493
|
-
const name = cleanText(feature.get('name'));
|
|
494
|
-
|
|
495
|
-
if (MAJOR_ROADS.has(highway)) {
|
|
496
|
-
const minZoom = Number(rule.majorMinZoom ?? 12);
|
|
497
|
-
if (zoom < minZoom || !isMeaningfulLabel(ref)) return null;
|
|
498
|
-
return { text: ref, priorityClass: 'important', zIndex: 820 };
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (SECONDARY_ROADS.has(highway)) {
|
|
502
|
-
const minZoom = Number(rule.secondaryMinZoom ?? 16);
|
|
503
|
-
if (zoom < minZoom || !isMeaningfulLabel(ref)) return null;
|
|
504
|
-
return { text: ref, priorityClass: 'normal', zIndex: 700 };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
if (LOCAL_ROADS.has(highway)) {
|
|
508
|
-
const minZoom = Number(local?.minZoom ?? 17);
|
|
509
|
-
if (zoom < minZoom || !isMeaningfulLabel(name)) return null;
|
|
510
|
-
return { text: name, priorityClass: 'low', zIndex: 400 };
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* @param {Feature} feature
|
|
518
|
-
* @param {string} aeroway
|
|
519
|
-
* @param {number} zoom
|
|
520
|
-
* @returns {{ text: string, placement: 'line' | 'point', priorityClass: string, zIndex: number } | null}
|
|
521
|
-
*/
|
|
522
|
-
function aviationLabel(feature, aeroway, zoom) {
|
|
523
|
-
if (!aeroway) {
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const ref = cleanText(feature.get('ref'));
|
|
528
|
-
const name = cleanText(feature.get('name'));
|
|
529
|
-
|
|
530
|
-
if (aeroway === 'aerodrome' || aeroway === 'heliport') {
|
|
531
|
-
const text = name || aviationRefText(ref) || aviationFallbackText(aeroway);
|
|
532
|
-
return isRenderableAviationText(text, aeroway) && zoom >= 9
|
|
533
|
-
? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 }
|
|
534
|
-
: null;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (aeroway === 'runway') {
|
|
538
|
-
const text = aviationRefText(ref) || name || 'RWY';
|
|
539
|
-
return isRenderableAviationText(text, aeroway) && zoom >= 11 ? { text, placement: 'line', priorityClass: 'important', zIndex: 900 } : null;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (aeroway === 'terminal' || aeroway === 'apron') {
|
|
543
|
-
const text = name || ref || aviationFallbackText(aeroway);
|
|
544
|
-
return isMeaningfulLabel(text) && zoom >= 14 ? { text, placement: 'point', priorityClass: 'normal', zIndex: 720 } : null;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
if (aeroway === 'helipad') {
|
|
548
|
-
const text = aviationRefText(ref) || name || 'H';
|
|
549
|
-
return isRenderableAviationText(text, aeroway) && zoom >= 12 ? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 } : null;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function aviationFallbackText(aeroway) {
|
|
556
|
-
if (aeroway === 'aerodrome') return 'Aerodrome';
|
|
557
|
-
if (aeroway === 'heliport') return 'Heliport';
|
|
558
|
-
if (aeroway === 'terminal') return 'Terminal';
|
|
559
|
-
if (aeroway === 'apron') return 'Apron';
|
|
560
|
-
return cleanText(aeroway).replace(/_/g, ' ');
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* @param {{
|
|
565
|
-
* cache: Map<string, Style>,
|
|
566
|
-
* styleDocument: Record<string, unknown>,
|
|
567
|
-
* rule: Record<string, unknown>,
|
|
568
|
-
* text: string,
|
|
569
|
-
* placement: 'line' | 'point',
|
|
570
|
-
* priorityClass: string,
|
|
571
|
-
* zIndex: number,
|
|
572
|
-
* zoom: number
|
|
573
|
-
* }} options
|
|
574
|
-
* @returns {Style}
|
|
575
|
-
*/
|
|
576
238
|
function textStyle(options) {
|
|
577
239
|
const priorityRule = priorityClassRule(options.styleDocument, options.priorityClass);
|
|
578
240
|
const opacity = labelOpacityForZoom(options.zoom, Number(priorityRule.opacity ?? options.rule.opacity ?? 0.82));
|
|
@@ -609,460 +271,3 @@ function textStyle(options) {
|
|
|
609
271
|
options.cache.set(key, style);
|
|
610
272
|
return style;
|
|
611
273
|
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* @param {Feature} feature
|
|
615
|
-
* @param {Record<string, unknown>} rule
|
|
616
|
-
* @returns {boolean}
|
|
617
|
-
*/
|
|
618
|
-
function isSelectedPoi(feature, rule) {
|
|
619
|
-
const categories = objectRule(rule.categories);
|
|
620
|
-
const category = poiCategoryForFeature(feature);
|
|
621
|
-
if (category && categories?.[category] === false) {
|
|
622
|
-
return false;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const matchesClass = matchesSelectedPoiClass(feature, rule);
|
|
626
|
-
return matchesClass;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/**
|
|
630
|
-
* @param {Feature} feature
|
|
631
|
-
* @param {Record<string, unknown>} rule
|
|
632
|
-
* @returns {boolean}
|
|
633
|
-
*/
|
|
634
|
-
function matchesSelectedPoiClass(feature, rule) {
|
|
635
|
-
const selected = objectRule(rule.classes) ?? DEFAULT_POI_CLASSES;
|
|
636
|
-
for (const property of [
|
|
637
|
-
'amenity',
|
|
638
|
-
'tourism',
|
|
639
|
-
'shop',
|
|
640
|
-
'leisure',
|
|
641
|
-
'railway',
|
|
642
|
-
'public_transport',
|
|
643
|
-
'station',
|
|
644
|
-
'aeroway',
|
|
645
|
-
'power',
|
|
646
|
-
'man_made',
|
|
647
|
-
'tower:type',
|
|
648
|
-
'military',
|
|
649
|
-
'emergency',
|
|
650
|
-
'office',
|
|
651
|
-
'government',
|
|
652
|
-
'boundary',
|
|
653
|
-
'protect_class',
|
|
654
|
-
'landuse',
|
|
655
|
-
'industrial'
|
|
656
|
-
]) {
|
|
657
|
-
const value = String(feature.get(property) ?? '');
|
|
658
|
-
const allowed = Array.isArray(selected[property]) ? /** @type {unknown[]} */ (selected[property]).map(String) : [];
|
|
659
|
-
if (value && allowed.includes(value)) {
|
|
660
|
-
return true;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
return false;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
/**
|
|
667
|
-
* @param {Feature} feature
|
|
668
|
-
* @returns {string}
|
|
669
|
-
*/
|
|
670
|
-
function poiLabelText(feature) {
|
|
671
|
-
for (const field of LABEL_TEXT_FIELDS) {
|
|
672
|
-
const text = cleanText(feature.get(field));
|
|
673
|
-
if (isMeaningfulLabel(text)) {
|
|
674
|
-
return text;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
return poiFallbackLabel(feature);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
function poiFallbackLabel(feature) {
|
|
682
|
-
const candidates = [
|
|
683
|
-
['military', {
|
|
684
|
-
airbase: 'Airbase',
|
|
685
|
-
airfield: 'Airfield',
|
|
686
|
-
barracks: 'Barracks',
|
|
687
|
-
bunker: 'Bunker',
|
|
688
|
-
checkpoint: 'Checkpoint',
|
|
689
|
-
naval_base: 'Naval base',
|
|
690
|
-
range: 'Range',
|
|
691
|
-
training_area: 'Training area',
|
|
692
|
-
danger_area: 'Danger area',
|
|
693
|
-
ammunition_dump: 'Ammunition dump',
|
|
694
|
-
base: 'Base'
|
|
695
|
-
}],
|
|
696
|
-
['emergency', {
|
|
697
|
-
ambulance_station: 'Ambulance station',
|
|
698
|
-
siren: 'Siren',
|
|
699
|
-
assembly_point: 'Assembly point',
|
|
700
|
-
disaster_response: 'Disaster response'
|
|
701
|
-
}],
|
|
702
|
-
['amenity', {
|
|
703
|
-
hospital: 'Hospital',
|
|
704
|
-
police: 'Police',
|
|
705
|
-
fire_station: 'Fire station',
|
|
706
|
-
shelter: 'Shelter',
|
|
707
|
-
bunker: 'Bunker',
|
|
708
|
-
checkpoint: 'Checkpoint',
|
|
709
|
-
depot: 'Depot',
|
|
710
|
-
warehouse: 'Warehouse',
|
|
711
|
-
prison: 'Prison',
|
|
712
|
-
courthouse: 'Courthouse',
|
|
713
|
-
townhall: 'Town hall',
|
|
714
|
-
bus_station: 'Bus station',
|
|
715
|
-
ferry_terminal: 'Ferry terminal'
|
|
716
|
-
}],
|
|
717
|
-
['power', {
|
|
718
|
-
plant: 'Power plant',
|
|
719
|
-
substation: 'Substation',
|
|
720
|
-
generator: 'Generator',
|
|
721
|
-
tower: 'Power tower',
|
|
722
|
-
transformer: 'Transformer',
|
|
723
|
-
line: 'Power line'
|
|
724
|
-
}],
|
|
725
|
-
['man_made', {
|
|
726
|
-
communications_tower: 'Comms tower',
|
|
727
|
-
mast: 'Mast',
|
|
728
|
-
antenna: 'Antenna'
|
|
729
|
-
}],
|
|
730
|
-
['boundary', {
|
|
731
|
-
protected_area: 'Protected area'
|
|
732
|
-
}],
|
|
733
|
-
['landuse', {
|
|
734
|
-
industrial: 'Industrial'
|
|
735
|
-
}],
|
|
736
|
-
['industrial', {
|
|
737
|
-
refinery: 'Refinery',
|
|
738
|
-
depot: 'Depot',
|
|
739
|
-
storage: 'Storage',
|
|
740
|
-
logistics: 'Logistics'
|
|
741
|
-
}]
|
|
742
|
-
];
|
|
743
|
-
|
|
744
|
-
for (const [property, labels] of candidates) {
|
|
745
|
-
const value = cleanText(feature.get(property));
|
|
746
|
-
if (value && labels[value]) {
|
|
747
|
-
return labels[value];
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const category = poiCategoryForFeature(feature);
|
|
752
|
-
return category === 'consumer' ? '' : titleCase(category);
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
/**
|
|
756
|
-
* @param {Feature} feature
|
|
757
|
-
* @returns {string}
|
|
758
|
-
*/
|
|
759
|
-
function poiCategoryForFeature(feature) {
|
|
760
|
-
const existing = cleanText(feature.get('poi_category'));
|
|
761
|
-
if (existing) {
|
|
762
|
-
return existing;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
const amenity = cleanText(feature.get('amenity'));
|
|
766
|
-
const tourism = cleanText(feature.get('tourism'));
|
|
767
|
-
const shop = cleanText(feature.get('shop'));
|
|
768
|
-
const leisure = cleanText(feature.get('leisure'));
|
|
769
|
-
const railway = cleanText(feature.get('railway'));
|
|
770
|
-
const publicTransport = cleanText(feature.get('public_transport'));
|
|
771
|
-
const station = cleanText(feature.get('station'));
|
|
772
|
-
const aeroway = cleanText(feature.get('aeroway'));
|
|
773
|
-
const power = cleanText(feature.get('power'));
|
|
774
|
-
const manMade = cleanText(feature.get('man_made'));
|
|
775
|
-
const towerType = cleanText(feature.get('tower:type'));
|
|
776
|
-
const military = cleanText(feature.get('military'));
|
|
777
|
-
const emergency = cleanText(feature.get('emergency'));
|
|
778
|
-
const office = cleanText(feature.get('office'));
|
|
779
|
-
const government = cleanText(feature.get('government'));
|
|
780
|
-
const boundary = cleanText(feature.get('boundary'));
|
|
781
|
-
const protectClass = cleanText(feature.get('protect_class'));
|
|
782
|
-
const landuse = cleanText(feature.get('landuse'));
|
|
783
|
-
const industrial = cleanText(feature.get('industrial'));
|
|
784
|
-
|
|
785
|
-
if (['railway_station', 'train_station', 'subway_station', 'bus_station', 'ferry_terminal', 'airport'].includes(amenity) ||
|
|
786
|
-
railway === 'station' || publicTransport === 'station' || station === 'subway' || aeroway === 'aerodrome' || aeroway === 'airport') {
|
|
787
|
-
return 'transport';
|
|
788
|
-
}
|
|
789
|
-
if (['hospital', 'police', 'fire_station', 'shelter'].includes(amenity) || ['ambulance_station', 'siren', 'assembly_point', 'disaster_response'].includes(emergency)) return 'emergency';
|
|
790
|
-
if (['townhall', 'courthouse', 'prison', 'embassy'].includes(amenity) || office === 'government' || government) return 'government';
|
|
791
|
-
if (power) return 'energy';
|
|
792
|
-
if (amenity === 'communications_tower' || manMade === 'communications_tower' || manMade === 'mast' || manMade === 'antenna' || towerType === 'communication') return 'communications';
|
|
793
|
-
if (boundary === 'protected_area' || leisure === 'nature_reserve' || tourism === 'national_park' || protectClass) return 'protected';
|
|
794
|
-
if (landuse === 'industrial' || industrial || ['depot', 'warehouse'].includes(amenity)) return 'industrial';
|
|
795
|
-
if (military || ['bunker', 'checkpoint'].includes(amenity)) return 'military';
|
|
796
|
-
if (shop || tourism || leisure || ['restaurant', 'cafe', 'bar', 'fast_food', 'pub'].includes(amenity)) return 'consumer';
|
|
797
|
-
return 'operational';
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
/**
|
|
801
|
-
* @param {number} priority
|
|
802
|
-
* @returns {string}
|
|
803
|
-
*/
|
|
804
|
-
function priorityClassFromNumber(priority) {
|
|
805
|
-
if (priority >= 900) return 'critical';
|
|
806
|
-
if (priority >= 760) return 'important';
|
|
807
|
-
if (priority >= 500) return 'normal';
|
|
808
|
-
return 'low';
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* @param {Feature} feature
|
|
813
|
-
* @returns {string}
|
|
814
|
-
*/
|
|
815
|
-
function poiPriorityClass(feature) {
|
|
816
|
-
const category = String(feature.get('poi_category') ?? '');
|
|
817
|
-
if (category === 'emergency' || category === 'military') {
|
|
818
|
-
return 'critical';
|
|
819
|
-
}
|
|
820
|
-
if (['transport', 'energy', 'communications', 'government'].includes(category)) {
|
|
821
|
-
return 'important';
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
const amenity = String(feature.get('amenity') ?? '');
|
|
825
|
-
if (amenity === 'hospital' || amenity === 'police' || amenity === 'fire_station') {
|
|
826
|
-
return 'critical';
|
|
827
|
-
}
|
|
828
|
-
return 'normal';
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* @param {Feature} feature
|
|
833
|
-
* @returns {number}
|
|
834
|
-
*/
|
|
835
|
-
function poiZIndex(feature) {
|
|
836
|
-
const priority = poiPriorityClass(feature);
|
|
837
|
-
if (priority === 'critical') return 920;
|
|
838
|
-
if (priority === 'important') return 820;
|
|
839
|
-
return 620;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
/**
|
|
843
|
-
* @param {Record<string, unknown>} rule
|
|
844
|
-
* @param {Record<string, unknown>} priorityRule
|
|
845
|
-
* @param {number} zoom
|
|
846
|
-
* @returns {string}
|
|
847
|
-
*/
|
|
848
|
-
function labelFont(rule, priorityRule, zoom) {
|
|
849
|
-
const configured = String(priorityRule.font ?? rule.font ?? '').trim();
|
|
850
|
-
if (configured) {
|
|
851
|
-
return configured;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
const weight = String(priorityRule.weight ?? rule.weight ?? 600);
|
|
855
|
-
const size = zoom >= 17 ? 12 : zoom >= 15 ? 11 : 10;
|
|
856
|
-
return `${weight} ${size}px sans-serif`;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
/**
|
|
860
|
-
* @param {Record<string, unknown>} styleDocument
|
|
861
|
-
* @param {string} className
|
|
862
|
-
* @returns {Record<string, unknown>}
|
|
863
|
-
*/
|
|
864
|
-
function priorityClassRule(styleDocument, className) {
|
|
865
|
-
const labels = labelConfigRoot(styleDocument);
|
|
866
|
-
const classes = labels?.priorityClasses && typeof labels.priorityClasses === 'object'
|
|
867
|
-
? /** @type {Record<string, unknown>} */ (labels.priorityClasses)
|
|
868
|
-
: {};
|
|
869
|
-
const rule = classes[className];
|
|
870
|
-
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : {};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* @param {number} zoom
|
|
875
|
-
* @param {number} base
|
|
876
|
-
* @returns {number}
|
|
877
|
-
*/
|
|
878
|
-
function labelOpacityForZoom(zoom, base) {
|
|
879
|
-
if (zoom < 12.5) return base * 0.6;
|
|
880
|
-
if (zoom < 14) return base * 0.76;
|
|
881
|
-
return base;
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
/**
|
|
885
|
-
* @param {unknown} value
|
|
886
|
-
* @returns {string}
|
|
887
|
-
*/
|
|
888
|
-
function cleanText(value) {
|
|
889
|
-
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
function titleCase(value) {
|
|
893
|
-
return cleanText(value)
|
|
894
|
-
.replace(/_/g, ' ')
|
|
895
|
-
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
/**
|
|
899
|
-
* @param {string} text
|
|
900
|
-
* @returns {boolean}
|
|
901
|
-
*/
|
|
902
|
-
function isMeaningfulLabel(text) {
|
|
903
|
-
const normalized = cleanText(text);
|
|
904
|
-
if (normalized.length < 2) {
|
|
905
|
-
return false;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const key = normalized.toLowerCase().replace(/\s+/g, '_');
|
|
909
|
-
if (GENERIC_LABEL_VALUES.has(key)) {
|
|
910
|
-
return false;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
if (/^(yes|no|true|false|0|1)$/i.test(normalized)) {
|
|
914
|
-
return false;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
return true;
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* @param {Feature} feature
|
|
922
|
-
* @param {string} sourceLayer
|
|
923
|
-
* @param {string} text
|
|
924
|
-
* @returns {boolean}
|
|
925
|
-
*/
|
|
926
|
-
function isExplicitAviationLabel(feature, sourceLayer, text) {
|
|
927
|
-
if (!AIP_SOURCE_LAYERS.has(sourceLayer)) {
|
|
928
|
-
return false;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const aeroway = cleanText(feature.get('aeroway') || feature.get('class'));
|
|
932
|
-
if (text === 'H') {
|
|
933
|
-
return aeroway === 'helipad' || aeroway === 'heliport';
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
if (text === 'RWY') {
|
|
937
|
-
return aeroway === 'runway';
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
return /^[A-Z]$/.test(text) && ['helipad', 'heliport', 'runway'].includes(aeroway);
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
/**
|
|
944
|
-
* @param {string} ref
|
|
945
|
-
* @returns {string}
|
|
946
|
-
*/
|
|
947
|
-
function aviationRefText(ref) {
|
|
948
|
-
return ref && !/^(yes|no|true|false|0|1)$/i.test(ref) ? ref : '';
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* @param {string} text
|
|
953
|
-
* @param {string} aeroway
|
|
954
|
-
* @returns {boolean}
|
|
955
|
-
*/
|
|
956
|
-
function isRenderableAviationText(text, aeroway) {
|
|
957
|
-
if (isMeaningfulLabel(text)) {
|
|
958
|
-
return true;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
if (text === 'H') {
|
|
962
|
-
return aeroway === 'helipad' || aeroway === 'heliport';
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
if (text === 'RWY') {
|
|
966
|
-
return aeroway === 'runway';
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
return /^[A-Z]$/.test(text) && ['helipad', 'heliport', 'runway'].includes(aeroway);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* @param {Record<string, unknown>} styleDocument
|
|
974
|
-
* @returns {Record<string, unknown> | null}
|
|
975
|
-
*/
|
|
976
|
-
function labelConfigRoot(styleDocument) {
|
|
977
|
-
return styleDocument.labels && typeof styleDocument.labels === 'object'
|
|
978
|
-
? /** @type {Record<string, unknown>} */ (styleDocument.labels)
|
|
979
|
-
: null;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
/**
|
|
983
|
-
* @param {Record<string, unknown>} styleDocument
|
|
984
|
-
* @param {string} sourceLayer
|
|
985
|
-
* @returns {Record<string, unknown> | null}
|
|
986
|
-
*/
|
|
987
|
-
function labelRuleForSource(styleDocument, sourceLayer) {
|
|
988
|
-
const labels = labelConfigRoot(styleDocument);
|
|
989
|
-
const rule = labels?.[sourceLayer] ?? labels?.[labelSourceAlias(sourceLayer)];
|
|
990
|
-
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : null;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
/**
|
|
994
|
-
* @param {string} sourceLayer
|
|
995
|
-
* @returns {string}
|
|
996
|
-
*/
|
|
997
|
-
function labelSourceAlias(sourceLayer) {
|
|
998
|
-
if (sourceLayer === 'aip') return 'aviation';
|
|
999
|
-
if (sourceLayer === 'aviation') return 'aip';
|
|
1000
|
-
return sourceLayer;
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
/**
|
|
1004
|
-
* @param {unknown} rule
|
|
1005
|
-
* @returns {Record<string, unknown> | null}
|
|
1006
|
-
*/
|
|
1007
|
-
function objectRule(rule) {
|
|
1008
|
-
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : null;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
/**
|
|
1012
|
-
* @param {number} zoom
|
|
1013
|
-
* @param {Record<string, unknown>} rule
|
|
1014
|
-
* @returns {boolean}
|
|
1015
|
-
*/
|
|
1016
|
-
function zoomInRule(zoom, rule) {
|
|
1017
|
-
const minZoom = Number(rule.minZoom ?? 0);
|
|
1018
|
-
const maxZoom = Number(rule.maxZoom ?? 22);
|
|
1019
|
-
return (!Number.isFinite(minZoom) || zoom >= minZoom) && (!Number.isFinite(maxZoom) || zoom <= maxZoom);
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
/**
|
|
1023
|
-
* @param {number} resolution
|
|
1024
|
-
* @returns {number}
|
|
1025
|
-
*/
|
|
1026
|
-
function resolutionToZoom(resolution) {
|
|
1027
|
-
const initialResolution = 156543.03392804097;
|
|
1028
|
-
return Math.log2(initialResolution / Number(resolution));
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
/**
|
|
1032
|
-
* @param {string} color
|
|
1033
|
-
* @param {number} opacity
|
|
1034
|
-
* @returns {string}
|
|
1035
|
-
*/
|
|
1036
|
-
function rgba(color, opacity) {
|
|
1037
|
-
const rgb = hexToRgb(color);
|
|
1038
|
-
if (!rgb) {
|
|
1039
|
-
return color;
|
|
1040
|
-
}
|
|
1041
|
-
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp(opacity, 0, 1)})`;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
/**
|
|
1045
|
-
* @param {string} color
|
|
1046
|
-
* @returns {[number, number, number] | null}
|
|
1047
|
-
*/
|
|
1048
|
-
function hexToRgb(color) {
|
|
1049
|
-
const match = /^#?([0-9a-f]{6})$/i.exec(color);
|
|
1050
|
-
if (!match) {
|
|
1051
|
-
return null;
|
|
1052
|
-
}
|
|
1053
|
-
const value = Number.parseInt(match[1], 16);
|
|
1054
|
-
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
/**
|
|
1058
|
-
* @param {number} value
|
|
1059
|
-
* @param {number} min
|
|
1060
|
-
* @param {number} max
|
|
1061
|
-
* @returns {number}
|
|
1062
|
-
*/
|
|
1063
|
-
function clamp(value, min, max) {
|
|
1064
|
-
if (!Number.isFinite(value)) {
|
|
1065
|
-
return min;
|
|
1066
|
-
}
|
|
1067
|
-
return Math.max(min, Math.min(max, value));
|
|
1068
|
-
}
|