@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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +2 -2
  3. package/src/index.js +10 -234
  4. 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.2",
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/raster": "0.3.2"
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 '../../raster/src/shared/layers.js';
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' | 'raster'} role
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
- return new PMTiles(resolveUrl(tiles.url, context.manifestBaseUrl));
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
- const LABEL_SOURCE_LAYERS = ['roads', 'aip', 'aviation', 'pois'];
11
- const ROAD_SOURCE_LAYER = 'roads';
12
- const AIP_SOURCE_LAYERS = new Set(['aip', 'aviation']);
13
- const POI_SOURCE_LAYER = 'pois';
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 Map();
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
- }