@map-zero/ol 0.3.1 → 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 +4 -4
- package/src/index.js +21 -242
- package/src/labels.js +34 -826
- package/src/zoom.js +36 -0
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,10 +22,10 @@
|
|
|
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
|
-
"ol": "
|
|
29
|
-
"pmtiles": "
|
|
28
|
+
"ol": "10.10.0",
|
|
29
|
+
"pmtiles": "4.4.1"
|
|
30
30
|
}
|
|
31
31
|
}
|
package/src/index.js
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
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 {
|
|
12
|
-
import { clampInteger, clampNumber } from '../../raster/src/shared/math.js';
|
|
9
|
+
import { isAipLayer, layerAlias } from '../../core/src/shared/layers.js';
|
|
10
|
+
import { maxZoomExpression, minZoomExpression, zoomInterpolateExpression } from './zoom.js';
|
|
13
11
|
|
|
14
12
|
import {
|
|
15
13
|
activeLabelLayerIdsForZoom,
|
|
@@ -56,11 +54,7 @@ const ROAD_HIGHWAY_DRAW_ORDER = [
|
|
|
56
54
|
* style?: string | Record<string, unknown>,
|
|
57
55
|
* visibleLayers?: string[] | Set<string>,
|
|
58
56
|
* source?: 'auto' | 'pmtiles' | 'dynamic',
|
|
59
|
-
*
|
|
60
|
-
* workerUrl?: string | URL,
|
|
61
|
-
* rasterPixelRatio?: number,
|
|
62
|
-
* overzoomLevels?: number,
|
|
63
|
-
* edgeGuardPixels?: number,
|
|
57
|
+
* hitDetection?: boolean,
|
|
64
58
|
* apiBaseUrl?: string,
|
|
65
59
|
* zIndexBase?: number,
|
|
66
60
|
* onTileLoadStart?: () => void,
|
|
@@ -133,9 +127,6 @@ export async function createMapZeroOpenLayersLayers(options) {
|
|
|
133
127
|
onTileLoadError: options.onTileLoadError
|
|
134
128
|
};
|
|
135
129
|
|
|
136
|
-
if (options.renderMode === 'raster-worker') {
|
|
137
|
-
return createRasterWorkerController(context, options);
|
|
138
|
-
}
|
|
139
130
|
|
|
140
131
|
patchWebGlVectorTileRenderer();
|
|
141
132
|
|
|
@@ -144,6 +135,7 @@ export async function createMapZeroOpenLayersLayers(options) {
|
|
|
144
135
|
source,
|
|
145
136
|
preload: 0,
|
|
146
137
|
useInterimTilesOnError: false,
|
|
138
|
+
disableHitDetection: options.hitDetection === false,
|
|
147
139
|
style: createWebGlStyles(context)
|
|
148
140
|
});
|
|
149
141
|
tagOpenLayersLayer(layer, instanceId, orderedLayers.map((item) => item.id), 'geometry');
|
|
@@ -230,193 +222,6 @@ export async function createMapZeroOpenLayersLayers(options) {
|
|
|
230
222
|
* destroy: () => void
|
|
231
223
|
* }}
|
|
232
224
|
*/
|
|
233
|
-
function createRasterWorkerController(context, options) {
|
|
234
|
-
if (!isPmtilesManifest(context.manifest)) {
|
|
235
|
-
throw new Error('raster-worker render mode requires vector PMTiles');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const worker = new MapZeroRasterTileWorker({
|
|
239
|
-
manifest: context.manifest,
|
|
240
|
-
manifestUrl: context.manifestUrl,
|
|
241
|
-
styleDocument: context.styleDocument,
|
|
242
|
-
layers: context.orderedLayers.map((layer) => layer.id),
|
|
243
|
-
workerUrl: options.workerUrl ?? new URL('@map-zero/raster/imagery-worker.js', import.meta.url),
|
|
244
|
-
rasterPixelRatio: options.rasterPixelRatio,
|
|
245
|
-
overzoomLevels: options.overzoomLevels,
|
|
246
|
-
edgeGuardPixels: options.edgeGuardPixels
|
|
247
|
-
});
|
|
248
|
-
worker.setVisibleLayers(context.layerVisibility);
|
|
249
|
-
const range = pmtilesZoomRange(context.manifest);
|
|
250
|
-
const maxZoom = range.maxZoom + clampInteger(options.overzoomLevels ?? 0, 0, 4);
|
|
251
|
-
const rasterPixelRatio = clampNumber(options.rasterPixelRatio ?? 2, 1, 2);
|
|
252
|
-
const source = new ImageTileSource({
|
|
253
|
-
minZoom: range.minZoom,
|
|
254
|
-
maxZoom,
|
|
255
|
-
tileGrid: createXYZ({
|
|
256
|
-
minZoom: range.minZoom,
|
|
257
|
-
maxZoom,
|
|
258
|
-
tileSize: 512
|
|
259
|
-
}),
|
|
260
|
-
tileSize: 512,
|
|
261
|
-
transition: 0,
|
|
262
|
-
interpolate: true,
|
|
263
|
-
zDirection: preferNearestZoomLevel,
|
|
264
|
-
wrapX: false,
|
|
265
|
-
loader: (z, x, y) => worker.render(z, x, y)
|
|
266
|
-
});
|
|
267
|
-
source.getTilePixelRatio = () => rasterPixelRatio;
|
|
268
|
-
const layer = new TileLayer({
|
|
269
|
-
source,
|
|
270
|
-
cacheSize: 4096,
|
|
271
|
-
preload: 0,
|
|
272
|
-
useInterimTilesOnError: false
|
|
273
|
-
});
|
|
274
|
-
tagOpenLayersLayer(layer, context.instanceId, context.orderedLayers.map((item) => item.id), 'raster');
|
|
275
|
-
applyLayerZIndex(layer, context.orderedLayers, context.styleDocument, options.zIndexBase);
|
|
276
|
-
|
|
277
|
-
const refresh = () => {
|
|
278
|
-
worker.setVisibleLayers(context.layerVisibility);
|
|
279
|
-
refreshRasterSource(source);
|
|
280
|
-
layer.changed();
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
id: context.instanceId,
|
|
285
|
-
manifest: context.manifest,
|
|
286
|
-
style: context.styleDocument,
|
|
287
|
-
layers: [layer],
|
|
288
|
-
setVisible(layerId, visible) {
|
|
289
|
-
if (!context.layerVisibility.has(layerId)) {
|
|
290
|
-
throw new Error(`unknown map-zero layer: ${layerId}`);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
context.layerVisibility.set(layerId, Boolean(visible));
|
|
294
|
-
refresh();
|
|
295
|
-
},
|
|
296
|
-
setOpacity(layerId, opacity) {
|
|
297
|
-
if (!context.layerOpacity?.has?.(layerId)) {
|
|
298
|
-
throw new Error(`unknown map-zero layer: ${layerId}`);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
layer.setOpacity(clamp(Number(opacity), 0, 1));
|
|
302
|
-
},
|
|
303
|
-
destroy() {
|
|
304
|
-
refreshRasterSource(source);
|
|
305
|
-
worker.destroy();
|
|
306
|
-
layer.dispose();
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function refreshRasterSource(source) {
|
|
312
|
-
source.clear();
|
|
313
|
-
source.refresh?.();
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
class MapZeroRasterTileWorker {
|
|
317
|
-
/**
|
|
318
|
-
* @param {{
|
|
319
|
-
* manifest: Record<string, unknown>,
|
|
320
|
-
* manifestUrl: string,
|
|
321
|
-
* styleDocument: Record<string, unknown>,
|
|
322
|
-
* layers: string[],
|
|
323
|
-
* workerUrl: string | URL,
|
|
324
|
-
* rasterPixelRatio?: number,
|
|
325
|
-
* overzoomLevels?: number,
|
|
326
|
-
* edgeGuardPixels?: number
|
|
327
|
-
* }} options
|
|
328
|
-
*/
|
|
329
|
-
constructor(options) {
|
|
330
|
-
assertRasterWorkerSupport();
|
|
331
|
-
this.#rasterPixelRatio = clampNumber(options.rasterPixelRatio ?? 2, 1, 2);
|
|
332
|
-
this.worker = new Worker(options.workerUrl, { type: 'module' });
|
|
333
|
-
this.worker.addEventListener('message', (event) => this.#handleMessage(event.data));
|
|
334
|
-
this.worker.addEventListener('error', (event) => this.#rejectAll(new Error(event.message || 'map-zero OpenLayers raster worker failed')));
|
|
335
|
-
this.worker.postMessage({
|
|
336
|
-
type: 'init',
|
|
337
|
-
options: {
|
|
338
|
-
manifest: options.manifest,
|
|
339
|
-
manifestUrl: resolveUrl(options.manifestUrl, documentBaseUrl()),
|
|
340
|
-
styleDocument: options.styleDocument,
|
|
341
|
-
layers: options.layers,
|
|
342
|
-
tileSize: 512,
|
|
343
|
-
pixelRatio: this.#rasterPixelRatio,
|
|
344
|
-
sourceMaximumLevel: pmtilesZoomRange(options.manifest).maxZoom,
|
|
345
|
-
overzoomLevels: clampInteger(options.overzoomLevels ?? 0, 0, 4),
|
|
346
|
-
edgeGuardPixels: clampInteger(options.edgeGuardPixels ?? 0, 0, 8),
|
|
347
|
-
source: String(pmtilesInfo(options.manifest).url ?? 'tiles.pmtiles')
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* @param {number} z
|
|
354
|
-
* @param {number} x
|
|
355
|
-
* @param {number} y
|
|
356
|
-
* @returns {Promise<ImageBitmap | HTMLCanvasElement>}
|
|
357
|
-
*/
|
|
358
|
-
render(z, x, y) {
|
|
359
|
-
const id = this.#nextId++;
|
|
360
|
-
return new Promise((resolve, reject) => {
|
|
361
|
-
this.#pending.set(id, { resolve, reject });
|
|
362
|
-
this.worker.postMessage({ type: 'render', id, x, y, z });
|
|
363
|
-
}).catch(() => emptyCanvas(512 * this.#rasterPixelRatio, 512 * this.#rasterPixelRatio));
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* @param {Map<string, boolean>} visibility
|
|
368
|
-
*/
|
|
369
|
-
setVisibleLayers(visibility) {
|
|
370
|
-
for (const [layerId, visible] of visibility) {
|
|
371
|
-
this.worker.postMessage({ type: 'visibility', layerId, visible });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
destroy() {
|
|
376
|
-
this.#rejectAll(new Error('map-zero OpenLayers raster worker destroyed'));
|
|
377
|
-
this.worker.terminate();
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* @param {any} message
|
|
382
|
-
*/
|
|
383
|
-
#handleMessage(message) {
|
|
384
|
-
if (message?.type !== 'tile') {
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const pending = this.#pending.get(message.id);
|
|
389
|
-
if (!pending) {
|
|
390
|
-
message.image?.close?.();
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
this.#pending.delete(message.id);
|
|
395
|
-
if (message.error) {
|
|
396
|
-
pending.reject(new Error(message.error));
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
pending.resolve(message.image ?? emptyCanvas(512, 512));
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* @param {Error} error
|
|
404
|
-
*/
|
|
405
|
-
#rejectAll(error) {
|
|
406
|
-
for (const pending of this.#pending.values()) {
|
|
407
|
-
pending.reject(error);
|
|
408
|
-
}
|
|
409
|
-
this.#pending.clear();
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/** @type {Worker} */
|
|
413
|
-
worker;
|
|
414
|
-
#rasterPixelRatio = 2;
|
|
415
|
-
#nextId = 1;
|
|
416
|
-
/** @type {Map<number, { resolve: (image: ImageBitmap | HTMLCanvasElement) => void, reject: (error: Error) => void }>} */
|
|
417
|
-
#pending = new Map();
|
|
418
|
-
}
|
|
419
|
-
|
|
420
225
|
/**
|
|
421
226
|
* Add map-zero layers to an existing OpenLayers map.
|
|
422
227
|
*
|
|
@@ -577,7 +382,7 @@ function applyLayerZIndex(layer, orderedLayers, styleDocument, zIndexBase) {
|
|
|
577
382
|
* @param {unknown} layer
|
|
578
383
|
* @param {string} instanceId
|
|
579
384
|
* @param {string[]} layerIds
|
|
580
|
-
* @param {'geometry' | 'labels'
|
|
385
|
+
* @param {'geometry' | 'labels'} role
|
|
581
386
|
*/
|
|
582
387
|
function tagOpenLayersLayer(layer, instanceId, layerIds, role) {
|
|
583
388
|
const namespacedLayerIds = layerIds.map((layerId) => namespaceLayerId(instanceId, layerId));
|
|
@@ -876,8 +681,14 @@ function createLabelTileDataLoader(context) {
|
|
|
876
681
|
* @returns {PMTiles}
|
|
877
682
|
*/
|
|
878
683
|
function createPmtilesArchive(context) {
|
|
684
|
+
if (context.archive) return context.archive;
|
|
879
685
|
const tiles = /** @type {{ url: string }} */ (context.manifest.tiles);
|
|
880
|
-
|
|
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;
|
|
881
692
|
}
|
|
882
693
|
|
|
883
694
|
/**
|
|
@@ -1252,7 +1063,7 @@ function onewayFilter(filter) {
|
|
|
1252
1063
|
* @returns {unknown[]}
|
|
1253
1064
|
*/
|
|
1254
1065
|
function minZoomFilter(filter, zoom) {
|
|
1255
|
-
return Number.isFinite(zoom) ? ['all', filter,
|
|
1066
|
+
return Number.isFinite(zoom) ? ['all', filter, minZoomExpression(zoom)] : filter;
|
|
1256
1067
|
}
|
|
1257
1068
|
|
|
1258
1069
|
/**
|
|
@@ -1423,10 +1234,10 @@ function roadOverrideZoomFilter(filter, override) {
|
|
|
1423
1234
|
const normalized = normalizeStyleRule(/** @type {Record<string, unknown>} */ (override));
|
|
1424
1235
|
let next = filter;
|
|
1425
1236
|
if (Number.isFinite(normalized.minZoom)) {
|
|
1426
|
-
next = ['all', next,
|
|
1237
|
+
next = ['all', next, minZoomExpression(Number(normalized.minZoom))];
|
|
1427
1238
|
}
|
|
1428
1239
|
if (Number.isFinite(normalized.maxZoom)) {
|
|
1429
|
-
next = ['all', next,
|
|
1240
|
+
next = ['all', next, maxZoomExpression(Number(normalized.maxZoom))];
|
|
1430
1241
|
}
|
|
1431
1242
|
return next;
|
|
1432
1243
|
}
|
|
@@ -1647,11 +1458,11 @@ function createLayerFilter(layerId, rule) {
|
|
|
1647
1458
|
];
|
|
1648
1459
|
|
|
1649
1460
|
if (Number.isFinite(rule.minZoom)) {
|
|
1650
|
-
filters.push(
|
|
1461
|
+
filters.push(minZoomExpression(Number(rule.minZoom)));
|
|
1651
1462
|
}
|
|
1652
1463
|
|
|
1653
1464
|
if (Number.isFinite(rule.maxZoom)) {
|
|
1654
|
-
filters.push(
|
|
1465
|
+
filters.push(maxZoomExpression(Number(rule.maxZoom)));
|
|
1655
1466
|
}
|
|
1656
1467
|
|
|
1657
1468
|
if (layerId === 'pois') {
|
|
@@ -1836,7 +1647,7 @@ function zoomWidthScale(rule) {
|
|
|
1836
1647
|
return null;
|
|
1837
1648
|
}
|
|
1838
1649
|
|
|
1839
|
-
const
|
|
1650
|
+
const validStops = [];
|
|
1840
1651
|
for (const stop of stops) {
|
|
1841
1652
|
if (!Array.isArray(stop) || stop.length < 2) {
|
|
1842
1653
|
continue;
|
|
@@ -1845,11 +1656,11 @@ function zoomWidthScale(rule) {
|
|
|
1845
1656
|
const zoom = Number(stop[0]);
|
|
1846
1657
|
const scale = Number(stop[1]);
|
|
1847
1658
|
if (Number.isFinite(zoom) && Number.isFinite(scale) && scale >= 0) {
|
|
1848
|
-
|
|
1659
|
+
validStops.push([zoom, scale]);
|
|
1849
1660
|
}
|
|
1850
1661
|
}
|
|
1851
1662
|
|
|
1852
|
-
return
|
|
1663
|
+
return zoomInterpolateExpression(validStops);
|
|
1853
1664
|
}
|
|
1854
1665
|
|
|
1855
1666
|
/**
|
|
@@ -2096,38 +1907,6 @@ function isValidTileCoord(z, x, y) {
|
|
|
2096
1907
|
return Number.isInteger(x) && Number.isInteger(y) && x >= 0 && y >= 0 && x < maxIndex && y < maxIndex;
|
|
2097
1908
|
}
|
|
2098
1909
|
|
|
2099
|
-
/**
|
|
2100
|
-
* @param {number} width
|
|
2101
|
-
* @param {number} height
|
|
2102
|
-
* @returns {HTMLCanvasElement}
|
|
2103
|
-
*/
|
|
2104
|
-
function emptyCanvas(width, height) {
|
|
2105
|
-
const canvas = document.createElement('canvas');
|
|
2106
|
-
canvas.width = width;
|
|
2107
|
-
canvas.height = height;
|
|
2108
|
-
return canvas;
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
function assertRasterWorkerSupport() {
|
|
2112
|
-
if (typeof Worker !== 'function' || typeof OffscreenCanvas !== 'function' || typeof createImageBitmap !== 'function') {
|
|
2113
|
-
throw new Error('map-zero OpenLayers raster-worker mode requires Worker, OffscreenCanvas, and createImageBitmap');
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
/**
|
|
2118
|
-
* Choose the raster tile zoom at the midpoint in zoom space instead of always
|
|
2119
|
-
* forcing the parent or child tile. This keeps fractional zooms sharp without
|
|
2120
|
-
* holding low-resolution tiles for too long.
|
|
2121
|
-
*
|
|
2122
|
-
* @param {number} value
|
|
2123
|
-
* @param {number} high
|
|
2124
|
-
* @param {number} low
|
|
2125
|
-
* @returns {number}
|
|
2126
|
-
*/
|
|
2127
|
-
function preferNearestZoomLevel(value, high, low) {
|
|
2128
|
-
return value - low * Math.sqrt(high / low);
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
1910
|
/**
|
|
2132
1911
|
* @param {number} z
|
|
2133
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,155 +8,39 @@ 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
|
|
41
|
+
// source layers, so do not create Canvas features for buildings, water or
|
|
42
|
+
// other non-label data a second time.
|
|
43
|
+
const format = new MVT({ featureClass: Feature, layers: LABEL_SOURCE_LAYERS });
|
|
159
44
|
const source = new VectorTileSource({
|
|
160
45
|
format,
|
|
161
46
|
maxZoom: 22,
|
|
@@ -196,7 +81,7 @@ export function createMapZeroLabelLayer(options) {
|
|
|
196
81
|
if (options.onTileLoadEnd) source.on('tileloadend', options.onTileLoadEnd);
|
|
197
82
|
if (options.onTileLoadError) source.on('tileloaderror', options.onTileLoadError);
|
|
198
83
|
|
|
199
|
-
const styleCache = new
|
|
84
|
+
const styleCache = new LruCache(2048);
|
|
200
85
|
const layer = new VectorTileLayer({
|
|
201
86
|
source,
|
|
202
87
|
declutter: true,
|
|
@@ -222,60 +107,13 @@ export function createMapZeroLabelLayer(options) {
|
|
|
222
107
|
layer.changed();
|
|
223
108
|
},
|
|
224
109
|
destroy() {
|
|
110
|
+
styleCache.clear();
|
|
225
111
|
source.clear();
|
|
226
112
|
layer.dispose();
|
|
227
113
|
}
|
|
228
114
|
};
|
|
229
115
|
}
|
|
230
116
|
|
|
231
|
-
/**
|
|
232
|
-
* Return source MVT layer ids needed by the label renderer.
|
|
233
|
-
*
|
|
234
|
-
* @param {Array<{ id: string, type?: string, style?: string }>} orderedLayers
|
|
235
|
-
* @param {Record<string, unknown>} styleDocument
|
|
236
|
-
* @param {Map<string, boolean>} layerVisibility
|
|
237
|
-
* @param {number} zoom
|
|
238
|
-
* @returns {string[]}
|
|
239
|
-
*/
|
|
240
|
-
export function activeLabelLayerIdsForZoom(orderedLayers, styleDocument, layerVisibility, zoom) {
|
|
241
|
-
const labels = labelConfigRoot(styleDocument);
|
|
242
|
-
if (!labels || labels.enabled === false) {
|
|
243
|
-
return [];
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return LABEL_SOURCE_LAYERS.filter((layerId) => {
|
|
247
|
-
const rule = labelRuleForSource(styleDocument, layerId);
|
|
248
|
-
if (!rule || rule.enabled === false) {
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (!orderedLayers.some((layer) => layer.id === layerId) || !layerVisibility.get(layerId)) {
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return zoomInRule(zoom, rule);
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* @param {Record<string, unknown>} styleDocument
|
|
262
|
-
* @returns {boolean}
|
|
263
|
-
*/
|
|
264
|
-
export function hasEnabledLabels(styleDocument) {
|
|
265
|
-
const labels = labelConfigRoot(styleDocument);
|
|
266
|
-
return Boolean(labels && labels.enabled !== false && LABEL_SOURCE_LAYERS.some((layerId) => {
|
|
267
|
-
const rule = labelRuleForSource(styleDocument, layerId);
|
|
268
|
-
return rule && rule.enabled !== false;
|
|
269
|
-
}));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* @param {Feature} feature
|
|
274
|
-
* @param {number} resolution
|
|
275
|
-
* @param {Record<string, unknown>} styleDocument
|
|
276
|
-
* @param {Map<string, Style>} styleCache
|
|
277
|
-
* @returns {Style | null}
|
|
278
|
-
*/
|
|
279
117
|
function labelStyle(feature, resolution, styleDocument, styleCache) {
|
|
280
118
|
const zoom = resolutionToZoom(resolution);
|
|
281
119
|
const sourceLayer = cleanText(feature.get('sourceLayer'));
|
|
@@ -298,42 +136,6 @@ function labelStyle(feature, resolution, styleDocument, styleCache) {
|
|
|
298
136
|
return null;
|
|
299
137
|
}
|
|
300
138
|
|
|
301
|
-
/**
|
|
302
|
-
* @param {Feature} feature
|
|
303
|
-
* @returns {boolean}
|
|
304
|
-
*/
|
|
305
|
-
function isPoiLikeFeature(feature) {
|
|
306
|
-
return [
|
|
307
|
-
'poi_category',
|
|
308
|
-
'amenity',
|
|
309
|
-
'tourism',
|
|
310
|
-
'shop',
|
|
311
|
-
'leisure',
|
|
312
|
-
'railway',
|
|
313
|
-
'public_transport',
|
|
314
|
-
'station',
|
|
315
|
-
'power',
|
|
316
|
-
'man_made',
|
|
317
|
-
'tower:type',
|
|
318
|
-
'military',
|
|
319
|
-
'emergency',
|
|
320
|
-
'office',
|
|
321
|
-
'government',
|
|
322
|
-
'boundary',
|
|
323
|
-
'protect_class',
|
|
324
|
-
'landuse',
|
|
325
|
-
'industrial'
|
|
326
|
-
].some((property) => cleanText(feature.get(property)));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* @param {Feature} feature
|
|
331
|
-
* @param {string} sourceLayer
|
|
332
|
-
* @param {number} zoom
|
|
333
|
-
* @param {Record<string, unknown>} styleDocument
|
|
334
|
-
* @param {Map<string, Style>} styleCache
|
|
335
|
-
* @returns {Style | null}
|
|
336
|
-
*/
|
|
337
139
|
function candidateLabelStyle(feature, sourceLayer, zoom, styleDocument, styleCache) {
|
|
338
140
|
const rule = labelRuleForSource(styleDocument, sourceLayer);
|
|
339
141
|
const minZoom = Number(feature.get('minZoom') ?? rule?.minZoom ?? 0);
|
|
@@ -362,28 +164,6 @@ function candidateLabelStyle(feature, sourceLayer, zoom, styleDocument, styleCac
|
|
|
362
164
|
});
|
|
363
165
|
}
|
|
364
166
|
|
|
365
|
-
/**
|
|
366
|
-
* @param {Feature} feature
|
|
367
|
-
* @param {Record<string, unknown>} rule
|
|
368
|
-
* @returns {boolean}
|
|
369
|
-
*/
|
|
370
|
-
function isSelectedPoiCandidate(feature, rule) {
|
|
371
|
-
const className = cleanText(feature.get('class'));
|
|
372
|
-
if (!className || ['clinic', 'pharmacy', 'fuel', 'charging_station', 'fire_hydrant', 'defibrillator', 'fire_extinguisher', 'restaurant', 'cafe', 'bar', 'fast_food', 'pub'].includes(className)) {
|
|
373
|
-
return false;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const selected = objectRule(rule.classes) ?? DEFAULT_POI_CLASSES;
|
|
377
|
-
return Object.values(selected).some((values) => Array.isArray(values) && values.map(String).includes(className));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* @param {Feature} feature
|
|
382
|
-
* @param {number} zoom
|
|
383
|
-
* @param {Record<string, unknown>} styleDocument
|
|
384
|
-
* @param {Map<string, Style>} styleCache
|
|
385
|
-
* @returns {Style | null}
|
|
386
|
-
*/
|
|
387
167
|
function roadLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
388
168
|
const rule = labelRuleForSource(styleDocument, ROAD_SOURCE_LAYER);
|
|
389
169
|
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule)) {
|
|
@@ -408,13 +188,6 @@ function roadLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
|
408
188
|
});
|
|
409
189
|
}
|
|
410
190
|
|
|
411
|
-
/**
|
|
412
|
-
* @param {Feature} feature
|
|
413
|
-
* @param {number} zoom
|
|
414
|
-
* @param {Record<string, unknown>} styleDocument
|
|
415
|
-
* @param {Map<string, Style>} styleCache
|
|
416
|
-
* @returns {Style | null}
|
|
417
|
-
*/
|
|
418
191
|
function aviationLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
419
192
|
const rule = labelRuleForSource(styleDocument, 'aip');
|
|
420
193
|
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule)) {
|
|
@@ -439,13 +212,6 @@ function aviationLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
|
439
212
|
});
|
|
440
213
|
}
|
|
441
214
|
|
|
442
|
-
/**
|
|
443
|
-
* @param {Feature} feature
|
|
444
|
-
* @param {number} zoom
|
|
445
|
-
* @param {Record<string, unknown>} styleDocument
|
|
446
|
-
* @param {Map<string, Style>} styleCache
|
|
447
|
-
* @returns {Style | null}
|
|
448
|
-
*/
|
|
449
215
|
function poiLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
450
216
|
const rule = labelRuleForSource(styleDocument, POI_SOURCE_LAYER);
|
|
451
217
|
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule) || !isSelectedPoi(feature, rule)) {
|
|
@@ -469,107 +235,6 @@ function poiLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
|
469
235
|
});
|
|
470
236
|
}
|
|
471
237
|
|
|
472
|
-
/**
|
|
473
|
-
* @param {Feature} feature
|
|
474
|
-
* @param {string} highway
|
|
475
|
-
* @param {number} zoom
|
|
476
|
-
* @param {Record<string, unknown>} rule
|
|
477
|
-
* @returns {{ text: string, priorityClass: string, zIndex: number } | null}
|
|
478
|
-
*/
|
|
479
|
-
function roadLabel(feature, highway, zoom, rule) {
|
|
480
|
-
if (!highway || DISABLED_ROADS.has(highway)) {
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const local = objectRule(rule.local);
|
|
485
|
-
if (LOCAL_ROADS.has(highway) && local?.enabled !== true) {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const ref = cleanText(feature.get('ref'));
|
|
490
|
-
const name = cleanText(feature.get('name'));
|
|
491
|
-
|
|
492
|
-
if (MAJOR_ROADS.has(highway)) {
|
|
493
|
-
const minZoom = Number(rule.majorMinZoom ?? 12);
|
|
494
|
-
if (zoom < minZoom || !isMeaningfulLabel(ref)) return null;
|
|
495
|
-
return { text: ref, priorityClass: 'important', zIndex: 820 };
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (SECONDARY_ROADS.has(highway)) {
|
|
499
|
-
const minZoom = Number(rule.secondaryMinZoom ?? 16);
|
|
500
|
-
if (zoom < minZoom || !isMeaningfulLabel(ref)) return null;
|
|
501
|
-
return { text: ref, priorityClass: 'normal', zIndex: 700 };
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (LOCAL_ROADS.has(highway)) {
|
|
505
|
-
const minZoom = Number(local?.minZoom ?? 17);
|
|
506
|
-
if (zoom < minZoom || !isMeaningfulLabel(name)) return null;
|
|
507
|
-
return { text: name, priorityClass: 'low', zIndex: 400 };
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* @param {Feature} feature
|
|
515
|
-
* @param {string} aeroway
|
|
516
|
-
* @param {number} zoom
|
|
517
|
-
* @returns {{ text: string, placement: 'line' | 'point', priorityClass: string, zIndex: number } | null}
|
|
518
|
-
*/
|
|
519
|
-
function aviationLabel(feature, aeroway, zoom) {
|
|
520
|
-
if (!aeroway) {
|
|
521
|
-
return null;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const ref = cleanText(feature.get('ref'));
|
|
525
|
-
const name = cleanText(feature.get('name'));
|
|
526
|
-
|
|
527
|
-
if (aeroway === 'aerodrome' || aeroway === 'heliport') {
|
|
528
|
-
const text = name || aviationRefText(ref) || aviationFallbackText(aeroway);
|
|
529
|
-
return isRenderableAviationText(text, aeroway) && zoom >= 9
|
|
530
|
-
? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 }
|
|
531
|
-
: null;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if (aeroway === 'runway') {
|
|
535
|
-
const text = aviationRefText(ref) || name || 'RWY';
|
|
536
|
-
return isRenderableAviationText(text, aeroway) && zoom >= 11 ? { text, placement: 'line', priorityClass: 'important', zIndex: 900 } : null;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
if (aeroway === 'terminal' || aeroway === 'apron') {
|
|
540
|
-
const text = name || ref || aviationFallbackText(aeroway);
|
|
541
|
-
return isMeaningfulLabel(text) && zoom >= 14 ? { text, placement: 'point', priorityClass: 'normal', zIndex: 720 } : null;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
if (aeroway === 'helipad') {
|
|
545
|
-
const text = aviationRefText(ref) || name || 'H';
|
|
546
|
-
return isRenderableAviationText(text, aeroway) && zoom >= 12 ? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 } : null;
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return null;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
function aviationFallbackText(aeroway) {
|
|
553
|
-
if (aeroway === 'aerodrome') return 'Aerodrome';
|
|
554
|
-
if (aeroway === 'heliport') return 'Heliport';
|
|
555
|
-
if (aeroway === 'terminal') return 'Terminal';
|
|
556
|
-
if (aeroway === 'apron') return 'Apron';
|
|
557
|
-
return cleanText(aeroway).replace(/_/g, ' ');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* @param {{
|
|
562
|
-
* cache: Map<string, Style>,
|
|
563
|
-
* styleDocument: Record<string, unknown>,
|
|
564
|
-
* rule: Record<string, unknown>,
|
|
565
|
-
* text: string,
|
|
566
|
-
* placement: 'line' | 'point',
|
|
567
|
-
* priorityClass: string,
|
|
568
|
-
* zIndex: number,
|
|
569
|
-
* zoom: number
|
|
570
|
-
* }} options
|
|
571
|
-
* @returns {Style}
|
|
572
|
-
*/
|
|
573
238
|
function textStyle(options) {
|
|
574
239
|
const priorityRule = priorityClassRule(options.styleDocument, options.priorityClass);
|
|
575
240
|
const opacity = labelOpacityForZoom(options.zoom, Number(priorityRule.opacity ?? options.rule.opacity ?? 0.82));
|
|
@@ -606,460 +271,3 @@ function textStyle(options) {
|
|
|
606
271
|
options.cache.set(key, style);
|
|
607
272
|
return style;
|
|
608
273
|
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* @param {Feature} feature
|
|
612
|
-
* @param {Record<string, unknown>} rule
|
|
613
|
-
* @returns {boolean}
|
|
614
|
-
*/
|
|
615
|
-
function isSelectedPoi(feature, rule) {
|
|
616
|
-
const categories = objectRule(rule.categories);
|
|
617
|
-
const category = poiCategoryForFeature(feature);
|
|
618
|
-
if (category && categories?.[category] === false) {
|
|
619
|
-
return false;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const matchesClass = matchesSelectedPoiClass(feature, rule);
|
|
623
|
-
return matchesClass;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* @param {Feature} feature
|
|
628
|
-
* @param {Record<string, unknown>} rule
|
|
629
|
-
* @returns {boolean}
|
|
630
|
-
*/
|
|
631
|
-
function matchesSelectedPoiClass(feature, rule) {
|
|
632
|
-
const selected = objectRule(rule.classes) ?? DEFAULT_POI_CLASSES;
|
|
633
|
-
for (const property of [
|
|
634
|
-
'amenity',
|
|
635
|
-
'tourism',
|
|
636
|
-
'shop',
|
|
637
|
-
'leisure',
|
|
638
|
-
'railway',
|
|
639
|
-
'public_transport',
|
|
640
|
-
'station',
|
|
641
|
-
'aeroway',
|
|
642
|
-
'power',
|
|
643
|
-
'man_made',
|
|
644
|
-
'tower:type',
|
|
645
|
-
'military',
|
|
646
|
-
'emergency',
|
|
647
|
-
'office',
|
|
648
|
-
'government',
|
|
649
|
-
'boundary',
|
|
650
|
-
'protect_class',
|
|
651
|
-
'landuse',
|
|
652
|
-
'industrial'
|
|
653
|
-
]) {
|
|
654
|
-
const value = String(feature.get(property) ?? '');
|
|
655
|
-
const allowed = Array.isArray(selected[property]) ? /** @type {unknown[]} */ (selected[property]).map(String) : [];
|
|
656
|
-
if (value && allowed.includes(value)) {
|
|
657
|
-
return true;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* @param {Feature} feature
|
|
665
|
-
* @returns {string}
|
|
666
|
-
*/
|
|
667
|
-
function poiLabelText(feature) {
|
|
668
|
-
for (const field of LABEL_TEXT_FIELDS) {
|
|
669
|
-
const text = cleanText(feature.get(field));
|
|
670
|
-
if (isMeaningfulLabel(text)) {
|
|
671
|
-
return text;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return poiFallbackLabel(feature);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function poiFallbackLabel(feature) {
|
|
679
|
-
const candidates = [
|
|
680
|
-
['military', {
|
|
681
|
-
airbase: 'Airbase',
|
|
682
|
-
airfield: 'Airfield',
|
|
683
|
-
barracks: 'Barracks',
|
|
684
|
-
bunker: 'Bunker',
|
|
685
|
-
checkpoint: 'Checkpoint',
|
|
686
|
-
naval_base: 'Naval base',
|
|
687
|
-
range: 'Range',
|
|
688
|
-
training_area: 'Training area',
|
|
689
|
-
danger_area: 'Danger area',
|
|
690
|
-
ammunition_dump: 'Ammunition dump',
|
|
691
|
-
base: 'Base'
|
|
692
|
-
}],
|
|
693
|
-
['emergency', {
|
|
694
|
-
ambulance_station: 'Ambulance station',
|
|
695
|
-
siren: 'Siren',
|
|
696
|
-
assembly_point: 'Assembly point',
|
|
697
|
-
disaster_response: 'Disaster response'
|
|
698
|
-
}],
|
|
699
|
-
['amenity', {
|
|
700
|
-
hospital: 'Hospital',
|
|
701
|
-
police: 'Police',
|
|
702
|
-
fire_station: 'Fire station',
|
|
703
|
-
shelter: 'Shelter',
|
|
704
|
-
bunker: 'Bunker',
|
|
705
|
-
checkpoint: 'Checkpoint',
|
|
706
|
-
depot: 'Depot',
|
|
707
|
-
warehouse: 'Warehouse',
|
|
708
|
-
prison: 'Prison',
|
|
709
|
-
courthouse: 'Courthouse',
|
|
710
|
-
townhall: 'Town hall',
|
|
711
|
-
bus_station: 'Bus station',
|
|
712
|
-
ferry_terminal: 'Ferry terminal'
|
|
713
|
-
}],
|
|
714
|
-
['power', {
|
|
715
|
-
plant: 'Power plant',
|
|
716
|
-
substation: 'Substation',
|
|
717
|
-
generator: 'Generator',
|
|
718
|
-
tower: 'Power tower',
|
|
719
|
-
transformer: 'Transformer',
|
|
720
|
-
line: 'Power line'
|
|
721
|
-
}],
|
|
722
|
-
['man_made', {
|
|
723
|
-
communications_tower: 'Comms tower',
|
|
724
|
-
mast: 'Mast',
|
|
725
|
-
antenna: 'Antenna'
|
|
726
|
-
}],
|
|
727
|
-
['boundary', {
|
|
728
|
-
protected_area: 'Protected area'
|
|
729
|
-
}],
|
|
730
|
-
['landuse', {
|
|
731
|
-
industrial: 'Industrial'
|
|
732
|
-
}],
|
|
733
|
-
['industrial', {
|
|
734
|
-
refinery: 'Refinery',
|
|
735
|
-
depot: 'Depot',
|
|
736
|
-
storage: 'Storage',
|
|
737
|
-
logistics: 'Logistics'
|
|
738
|
-
}]
|
|
739
|
-
];
|
|
740
|
-
|
|
741
|
-
for (const [property, labels] of candidates) {
|
|
742
|
-
const value = cleanText(feature.get(property));
|
|
743
|
-
if (value && labels[value]) {
|
|
744
|
-
return labels[value];
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const category = poiCategoryForFeature(feature);
|
|
749
|
-
return category === 'consumer' ? '' : titleCase(category);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* @param {Feature} feature
|
|
754
|
-
* @returns {string}
|
|
755
|
-
*/
|
|
756
|
-
function poiCategoryForFeature(feature) {
|
|
757
|
-
const existing = cleanText(feature.get('poi_category'));
|
|
758
|
-
if (existing) {
|
|
759
|
-
return existing;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const amenity = cleanText(feature.get('amenity'));
|
|
763
|
-
const tourism = cleanText(feature.get('tourism'));
|
|
764
|
-
const shop = cleanText(feature.get('shop'));
|
|
765
|
-
const leisure = cleanText(feature.get('leisure'));
|
|
766
|
-
const railway = cleanText(feature.get('railway'));
|
|
767
|
-
const publicTransport = cleanText(feature.get('public_transport'));
|
|
768
|
-
const station = cleanText(feature.get('station'));
|
|
769
|
-
const aeroway = cleanText(feature.get('aeroway'));
|
|
770
|
-
const power = cleanText(feature.get('power'));
|
|
771
|
-
const manMade = cleanText(feature.get('man_made'));
|
|
772
|
-
const towerType = cleanText(feature.get('tower:type'));
|
|
773
|
-
const military = cleanText(feature.get('military'));
|
|
774
|
-
const emergency = cleanText(feature.get('emergency'));
|
|
775
|
-
const office = cleanText(feature.get('office'));
|
|
776
|
-
const government = cleanText(feature.get('government'));
|
|
777
|
-
const boundary = cleanText(feature.get('boundary'));
|
|
778
|
-
const protectClass = cleanText(feature.get('protect_class'));
|
|
779
|
-
const landuse = cleanText(feature.get('landuse'));
|
|
780
|
-
const industrial = cleanText(feature.get('industrial'));
|
|
781
|
-
|
|
782
|
-
if (['railway_station', 'train_station', 'subway_station', 'bus_station', 'ferry_terminal', 'airport'].includes(amenity) ||
|
|
783
|
-
railway === 'station' || publicTransport === 'station' || station === 'subway' || aeroway === 'aerodrome' || aeroway === 'airport') {
|
|
784
|
-
return 'transport';
|
|
785
|
-
}
|
|
786
|
-
if (['hospital', 'police', 'fire_station', 'shelter'].includes(amenity) || ['ambulance_station', 'siren', 'assembly_point', 'disaster_response'].includes(emergency)) return 'emergency';
|
|
787
|
-
if (['townhall', 'courthouse', 'prison', 'embassy'].includes(amenity) || office === 'government' || government) return 'government';
|
|
788
|
-
if (power) return 'energy';
|
|
789
|
-
if (amenity === 'communications_tower' || manMade === 'communications_tower' || manMade === 'mast' || manMade === 'antenna' || towerType === 'communication') return 'communications';
|
|
790
|
-
if (boundary === 'protected_area' || leisure === 'nature_reserve' || tourism === 'national_park' || protectClass) return 'protected';
|
|
791
|
-
if (landuse === 'industrial' || industrial || ['depot', 'warehouse'].includes(amenity)) return 'industrial';
|
|
792
|
-
if (military || ['bunker', 'checkpoint'].includes(amenity)) return 'military';
|
|
793
|
-
if (shop || tourism || leisure || ['restaurant', 'cafe', 'bar', 'fast_food', 'pub'].includes(amenity)) return 'consumer';
|
|
794
|
-
return 'operational';
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* @param {number} priority
|
|
799
|
-
* @returns {string}
|
|
800
|
-
*/
|
|
801
|
-
function priorityClassFromNumber(priority) {
|
|
802
|
-
if (priority >= 900) return 'critical';
|
|
803
|
-
if (priority >= 760) return 'important';
|
|
804
|
-
if (priority >= 500) return 'normal';
|
|
805
|
-
return 'low';
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/**
|
|
809
|
-
* @param {Feature} feature
|
|
810
|
-
* @returns {string}
|
|
811
|
-
*/
|
|
812
|
-
function poiPriorityClass(feature) {
|
|
813
|
-
const category = String(feature.get('poi_category') ?? '');
|
|
814
|
-
if (category === 'emergency' || category === 'military') {
|
|
815
|
-
return 'critical';
|
|
816
|
-
}
|
|
817
|
-
if (['transport', 'energy', 'communications', 'government'].includes(category)) {
|
|
818
|
-
return 'important';
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
const amenity = String(feature.get('amenity') ?? '');
|
|
822
|
-
if (amenity === 'hospital' || amenity === 'police' || amenity === 'fire_station') {
|
|
823
|
-
return 'critical';
|
|
824
|
-
}
|
|
825
|
-
return 'normal';
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
/**
|
|
829
|
-
* @param {Feature} feature
|
|
830
|
-
* @returns {number}
|
|
831
|
-
*/
|
|
832
|
-
function poiZIndex(feature) {
|
|
833
|
-
const priority = poiPriorityClass(feature);
|
|
834
|
-
if (priority === 'critical') return 920;
|
|
835
|
-
if (priority === 'important') return 820;
|
|
836
|
-
return 620;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
/**
|
|
840
|
-
* @param {Record<string, unknown>} rule
|
|
841
|
-
* @param {Record<string, unknown>} priorityRule
|
|
842
|
-
* @param {number} zoom
|
|
843
|
-
* @returns {string}
|
|
844
|
-
*/
|
|
845
|
-
function labelFont(rule, priorityRule, zoom) {
|
|
846
|
-
const configured = String(priorityRule.font ?? rule.font ?? '').trim();
|
|
847
|
-
if (configured) {
|
|
848
|
-
return configured;
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
const weight = String(priorityRule.weight ?? rule.weight ?? 600);
|
|
852
|
-
const size = zoom >= 17 ? 12 : zoom >= 15 ? 11 : 10;
|
|
853
|
-
return `${weight} ${size}px sans-serif`;
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* @param {Record<string, unknown>} styleDocument
|
|
858
|
-
* @param {string} className
|
|
859
|
-
* @returns {Record<string, unknown>}
|
|
860
|
-
*/
|
|
861
|
-
function priorityClassRule(styleDocument, className) {
|
|
862
|
-
const labels = labelConfigRoot(styleDocument);
|
|
863
|
-
const classes = labels?.priorityClasses && typeof labels.priorityClasses === 'object'
|
|
864
|
-
? /** @type {Record<string, unknown>} */ (labels.priorityClasses)
|
|
865
|
-
: {};
|
|
866
|
-
const rule = classes[className];
|
|
867
|
-
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : {};
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* @param {number} zoom
|
|
872
|
-
* @param {number} base
|
|
873
|
-
* @returns {number}
|
|
874
|
-
*/
|
|
875
|
-
function labelOpacityForZoom(zoom, base) {
|
|
876
|
-
if (zoom < 12.5) return base * 0.6;
|
|
877
|
-
if (zoom < 14) return base * 0.76;
|
|
878
|
-
return base;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/**
|
|
882
|
-
* @param {unknown} value
|
|
883
|
-
* @returns {string}
|
|
884
|
-
*/
|
|
885
|
-
function cleanText(value) {
|
|
886
|
-
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
function titleCase(value) {
|
|
890
|
-
return cleanText(value)
|
|
891
|
-
.replace(/_/g, ' ')
|
|
892
|
-
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
/**
|
|
896
|
-
* @param {string} text
|
|
897
|
-
* @returns {boolean}
|
|
898
|
-
*/
|
|
899
|
-
function isMeaningfulLabel(text) {
|
|
900
|
-
const normalized = cleanText(text);
|
|
901
|
-
if (normalized.length < 2) {
|
|
902
|
-
return false;
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const key = normalized.toLowerCase().replace(/\s+/g, '_');
|
|
906
|
-
if (GENERIC_LABEL_VALUES.has(key)) {
|
|
907
|
-
return false;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
if (/^(yes|no|true|false|0|1)$/i.test(normalized)) {
|
|
911
|
-
return false;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
return true;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/**
|
|
918
|
-
* @param {Feature} feature
|
|
919
|
-
* @param {string} sourceLayer
|
|
920
|
-
* @param {string} text
|
|
921
|
-
* @returns {boolean}
|
|
922
|
-
*/
|
|
923
|
-
function isExplicitAviationLabel(feature, sourceLayer, text) {
|
|
924
|
-
if (!AIP_SOURCE_LAYERS.has(sourceLayer)) {
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
const aeroway = cleanText(feature.get('aeroway') || feature.get('class'));
|
|
929
|
-
if (text === 'H') {
|
|
930
|
-
return aeroway === 'helipad' || aeroway === 'heliport';
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
if (text === 'RWY') {
|
|
934
|
-
return aeroway === 'runway';
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
return /^[A-Z]$/.test(text) && ['helipad', 'heliport', 'runway'].includes(aeroway);
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
/**
|
|
941
|
-
* @param {string} ref
|
|
942
|
-
* @returns {string}
|
|
943
|
-
*/
|
|
944
|
-
function aviationRefText(ref) {
|
|
945
|
-
return ref && !/^(yes|no|true|false|0|1)$/i.test(ref) ? ref : '';
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
/**
|
|
949
|
-
* @param {string} text
|
|
950
|
-
* @param {string} aeroway
|
|
951
|
-
* @returns {boolean}
|
|
952
|
-
*/
|
|
953
|
-
function isRenderableAviationText(text, aeroway) {
|
|
954
|
-
if (isMeaningfulLabel(text)) {
|
|
955
|
-
return true;
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
if (text === 'H') {
|
|
959
|
-
return aeroway === 'helipad' || aeroway === 'heliport';
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (text === 'RWY') {
|
|
963
|
-
return aeroway === 'runway';
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
return /^[A-Z]$/.test(text) && ['helipad', 'heliport', 'runway'].includes(aeroway);
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* @param {Record<string, unknown>} styleDocument
|
|
971
|
-
* @returns {Record<string, unknown> | null}
|
|
972
|
-
*/
|
|
973
|
-
function labelConfigRoot(styleDocument) {
|
|
974
|
-
return styleDocument.labels && typeof styleDocument.labels === 'object'
|
|
975
|
-
? /** @type {Record<string, unknown>} */ (styleDocument.labels)
|
|
976
|
-
: null;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* @param {Record<string, unknown>} styleDocument
|
|
981
|
-
* @param {string} sourceLayer
|
|
982
|
-
* @returns {Record<string, unknown> | null}
|
|
983
|
-
*/
|
|
984
|
-
function labelRuleForSource(styleDocument, sourceLayer) {
|
|
985
|
-
const labels = labelConfigRoot(styleDocument);
|
|
986
|
-
const rule = labels?.[sourceLayer] ?? labels?.[labelSourceAlias(sourceLayer)];
|
|
987
|
-
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : null;
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/**
|
|
991
|
-
* @param {string} sourceLayer
|
|
992
|
-
* @returns {string}
|
|
993
|
-
*/
|
|
994
|
-
function labelSourceAlias(sourceLayer) {
|
|
995
|
-
if (sourceLayer === 'aip') return 'aviation';
|
|
996
|
-
if (sourceLayer === 'aviation') return 'aip';
|
|
997
|
-
return sourceLayer;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
/**
|
|
1001
|
-
* @param {unknown} rule
|
|
1002
|
-
* @returns {Record<string, unknown> | null}
|
|
1003
|
-
*/
|
|
1004
|
-
function objectRule(rule) {
|
|
1005
|
-
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : null;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* @param {number} zoom
|
|
1010
|
-
* @param {Record<string, unknown>} rule
|
|
1011
|
-
* @returns {boolean}
|
|
1012
|
-
*/
|
|
1013
|
-
function zoomInRule(zoom, rule) {
|
|
1014
|
-
const minZoom = Number(rule.minZoom ?? 0);
|
|
1015
|
-
const maxZoom = Number(rule.maxZoom ?? 22);
|
|
1016
|
-
return (!Number.isFinite(minZoom) || zoom >= minZoom) && (!Number.isFinite(maxZoom) || zoom <= maxZoom);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
/**
|
|
1020
|
-
* @param {number} resolution
|
|
1021
|
-
* @returns {number}
|
|
1022
|
-
*/
|
|
1023
|
-
function resolutionToZoom(resolution) {
|
|
1024
|
-
const initialResolution = 156543.03392804097;
|
|
1025
|
-
return Math.log2(initialResolution / Number(resolution));
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
/**
|
|
1029
|
-
* @param {string} color
|
|
1030
|
-
* @param {number} opacity
|
|
1031
|
-
* @returns {string}
|
|
1032
|
-
*/
|
|
1033
|
-
function rgba(color, opacity) {
|
|
1034
|
-
const rgb = hexToRgb(color);
|
|
1035
|
-
if (!rgb) {
|
|
1036
|
-
return color;
|
|
1037
|
-
}
|
|
1038
|
-
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp(opacity, 0, 1)})`;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
/**
|
|
1042
|
-
* @param {string} color
|
|
1043
|
-
* @returns {[number, number, number] | null}
|
|
1044
|
-
*/
|
|
1045
|
-
function hexToRgb(color) {
|
|
1046
|
-
const match = /^#?([0-9a-f]{6})$/i.exec(color);
|
|
1047
|
-
if (!match) {
|
|
1048
|
-
return null;
|
|
1049
|
-
}
|
|
1050
|
-
const value = Number.parseInt(match[1], 16);
|
|
1051
|
-
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
/**
|
|
1055
|
-
* @param {number} value
|
|
1056
|
-
* @param {number} min
|
|
1057
|
-
* @param {number} max
|
|
1058
|
-
* @returns {number}
|
|
1059
|
-
*/
|
|
1060
|
-
function clamp(value, min, max) {
|
|
1061
|
-
if (!Number.isFinite(value)) {
|
|
1062
|
-
return min;
|
|
1063
|
-
}
|
|
1064
|
-
return Math.max(min, Math.min(max, value));
|
|
1065
|
-
}
|
package/src/zoom.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const WEB_MERCATOR_RESOLUTION_AT_ZOOM_0 = 156543.03392804097;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The WebGL style compiler exposes resolution in all supported render paths.
|
|
5
|
+
* Convert the standard Web Mercator zoom thresholds used by map-zero styles.
|
|
6
|
+
*
|
|
7
|
+
* @param {number} zoom
|
|
8
|
+
* @returns {number}
|
|
9
|
+
*/
|
|
10
|
+
export function resolutionForZoom(zoom) {
|
|
11
|
+
return WEB_MERCATOR_RESOLUTION_AT_ZOOM_0 / (2 ** zoom);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @param {number} zoom @returns {unknown[]} */
|
|
15
|
+
export function minZoomExpression(zoom) {
|
|
16
|
+
return ['<=', ['resolution'], resolutionForZoom(zoom)];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @param {number} zoom @returns {unknown[]} */
|
|
20
|
+
export function maxZoomExpression(zoom) {
|
|
21
|
+
return ['>=', ['resolution'], resolutionForZoom(zoom)];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {Array<[number, number]>} stops
|
|
26
|
+
* @returns {unknown[] | null}
|
|
27
|
+
*/
|
|
28
|
+
export function zoomInterpolateExpression(stops) {
|
|
29
|
+
const entries = stops
|
|
30
|
+
.filter(([zoom, value]) => Number.isFinite(zoom) && Number.isFinite(value))
|
|
31
|
+
.map(([zoom, value]) => ({ resolution: resolutionForZoom(zoom), value }))
|
|
32
|
+
.sort((left, right) => left.resolution - right.resolution);
|
|
33
|
+
|
|
34
|
+
if (entries.length < 2) return null;
|
|
35
|
+
return ['interpolate', ['linear'], ['resolution'], ...entries.flatMap(({ resolution, value }) => [resolution, value])];
|
|
36
|
+
}
|