map-zero 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/docs/api.md +66 -0
- package/docs/architecture.md +87 -0
- package/docs/cartography.md +77 -0
- package/docs/cesium.md +107 -0
- package/docs/openlayers.md +98 -0
- package/docs/styles.md +103 -0
- package/package.json +51 -0
- package/packages/cesium/package.json +13 -0
- package/packages/cesium/src/index.js +405 -0
- package/packages/ol/package.json +14 -0
- package/packages/ol/src/index.js +1705 -0
- package/packages/ol/src/labels.js +977 -0
- package/src/3dtiles/b3dm.js +38 -0
- package/src/3dtiles/clipper-surfaces.js +317 -0
- package/src/3dtiles/export.js +768 -0
- package/src/3dtiles/extrude.js +301 -0
- package/src/3dtiles/flat.js +531 -0
- package/src/3dtiles/glb.js +178 -0
- package/src/3dtiles/gpkg-buildings.js +240 -0
- package/src/3dtiles/gpkg-features.js +157 -0
- package/src/3dtiles/tileset.js +75 -0
- package/src/build.js +134 -0
- package/src/cli.js +656 -0
- package/src/export-pmtiles.js +962 -0
- package/src/geometry-read.js +50 -0
- package/src/gpkg-read.js +460 -0
- package/src/gpkg.js +567 -0
- package/src/html.js +593 -0
- package/src/layers.js +357 -0
- package/src/manifest.js +29 -0
- package/src/mvt.js +2593 -0
- package/src/ol.js +5 -0
- package/src/osm.js +2110 -0
- package/src/pmtiles-worker.js +70 -0
- package/src/pmtiles.js +260 -0
- package/src/server.js +720 -0
- package/src/style-command.js +78 -0
- package/src/style-filters.js +76 -0
- package/src/style-presets.js +93 -0
- package/src/style-themes.js +235 -0
- package/src/style.js +13 -0
- package/src/tile-cache.js +59 -0
- package/src/utils.js +222 -0
- package/styles/presets/light.json +4655 -0
- package/styles/presets/monochrome.json +4655 -0
- package/styles/presets/neon-dark-3d.json +90 -0
- package/styles/presets/neon-dark.json +4690 -0
- package/styles/presets/tactical.json +4690 -0
- package/styles/themes/neon-dark.theme.json +20 -0
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
import Feature from 'ol/Feature.js';
|
|
2
|
+
import MVT from 'ol/format/MVT.js';
|
|
3
|
+
import VectorTileLayer from 'ol/layer/VectorTile.js';
|
|
4
|
+
import VectorTileSource from 'ol/source/VectorTile.js';
|
|
5
|
+
import Fill from 'ol/style/Fill.js';
|
|
6
|
+
import Stroke from 'ol/style/Stroke.js';
|
|
7
|
+
import Style from 'ol/style/Style.js';
|
|
8
|
+
import Text from 'ol/style/Text.js';
|
|
9
|
+
|
|
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';
|
|
14
|
+
|
|
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
|
+
export function createMapZeroLabelLayer(options) {
|
|
158
|
+
const format = new MVT({ featureClass: Feature });
|
|
159
|
+
const source = new VectorTileSource({
|
|
160
|
+
format,
|
|
161
|
+
maxZoom: 22,
|
|
162
|
+
...(options.sourceOptions ?? {}),
|
|
163
|
+
cacheSize: 512,
|
|
164
|
+
transition: 0,
|
|
165
|
+
wrapX: false,
|
|
166
|
+
tileUrlFunction: options.tileUrlFunction,
|
|
167
|
+
tileLoadFunction: (tile) => {
|
|
168
|
+
tile.setLoader((extent, resolution, projection) => {
|
|
169
|
+
const tileCoord = tile.getTileCoord();
|
|
170
|
+
const url = options.tileUrlFunction(tileCoord);
|
|
171
|
+
if (!url) {
|
|
172
|
+
tile.setFeatures([]);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
options.loadTileData(tileCoord, url)
|
|
177
|
+
.then((data) => {
|
|
178
|
+
if (!data) {
|
|
179
|
+
tile.setFeatures([]);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
tile.setFeatures(format.readFeatures(data, {
|
|
184
|
+
extent,
|
|
185
|
+
featureProjection: projection
|
|
186
|
+
}));
|
|
187
|
+
})
|
|
188
|
+
.catch(() => {
|
|
189
|
+
tile.setFeatures([]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (options.onTileLoadStart) source.on('tileloadstart', options.onTileLoadStart);
|
|
196
|
+
if (options.onTileLoadEnd) source.on('tileloadend', options.onTileLoadEnd);
|
|
197
|
+
if (options.onTileLoadError) source.on('tileloaderror', options.onTileLoadError);
|
|
198
|
+
|
|
199
|
+
const styleCache = new Map();
|
|
200
|
+
const layer = new VectorTileLayer({
|
|
201
|
+
source,
|
|
202
|
+
declutter: true,
|
|
203
|
+
updateWhileAnimating: false,
|
|
204
|
+
updateWhileInteracting: false,
|
|
205
|
+
style: (feature, resolution) => labelStyle(feature, resolution, options.styleDocument, styleCache)
|
|
206
|
+
});
|
|
207
|
+
if (typeof layer.set === 'function' && options.instanceId) {
|
|
208
|
+
layer.set('mapzero:id', options.instanceId);
|
|
209
|
+
layer.set('mapzero:role', 'labels');
|
|
210
|
+
layer.set('mapzero:sourceLayerIds', LABEL_SOURCE_LAYERS.map((layerId) => `${options.instanceId}:${layerId}`));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
layer,
|
|
215
|
+
source,
|
|
216
|
+
attachMap() {},
|
|
217
|
+
detachMap() {},
|
|
218
|
+
refresh() {
|
|
219
|
+
source.setTileUrlFunction(options.tileUrlFunction, String(Date.now()));
|
|
220
|
+
source.clear();
|
|
221
|
+
styleCache.clear();
|
|
222
|
+
layer.changed();
|
|
223
|
+
},
|
|
224
|
+
destroy() {
|
|
225
|
+
source.clear();
|
|
226
|
+
layer.dispose();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
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
|
+
function labelStyle(feature, resolution, styleDocument, styleCache) {
|
|
280
|
+
const zoom = resolutionToZoom(resolution);
|
|
281
|
+
const sourceLayer = cleanText(feature.get('sourceLayer'));
|
|
282
|
+
if (sourceLayer && cleanText(feature.get('text'))) {
|
|
283
|
+
return candidateLabelStyle(feature, sourceLayer, zoom, styleDocument, styleCache);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (feature.get('highway')) {
|
|
287
|
+
return roadLabelStyle(feature, zoom, styleDocument, styleCache);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (feature.get('aeroway')) {
|
|
291
|
+
return aviationLabelStyle(feature, zoom, styleDocument, styleCache);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (isPoiLikeFeature(feature)) {
|
|
295
|
+
return poiLabelStyle(feature, zoom, styleDocument, styleCache);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
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
|
+
function candidateLabelStyle(feature, sourceLayer, zoom, styleDocument, styleCache) {
|
|
338
|
+
const rule = labelRuleForSource(styleDocument, sourceLayer);
|
|
339
|
+
const minZoom = Number(feature.get('minZoom') ?? rule?.minZoom ?? 0);
|
|
340
|
+
const text = cleanText(feature.get('text'));
|
|
341
|
+
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule) || zoom < minZoom) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!isMeaningfulLabel(text) && !isExplicitAviationLabel(feature, sourceLayer, text)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (sourceLayer === POI_SOURCE_LAYER && !isSelectedPoiCandidate(feature, rule)) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return textStyle({
|
|
354
|
+
cache: styleCache,
|
|
355
|
+
styleDocument,
|
|
356
|
+
rule,
|
|
357
|
+
text,
|
|
358
|
+
placement: 'point',
|
|
359
|
+
priorityClass: priorityClassFromNumber(Number(feature.get('priority') ?? 0)),
|
|
360
|
+
zIndex: Number(feature.get('priority') ?? 0),
|
|
361
|
+
zoom
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
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
|
+
function roadLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
388
|
+
const rule = labelRuleForSource(styleDocument, ROAD_SOURCE_LAYER);
|
|
389
|
+
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule)) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const highway = String(feature.get('highway') ?? '');
|
|
394
|
+
const label = roadLabel(feature, highway, zoom, rule);
|
|
395
|
+
if (!label) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return textStyle({
|
|
400
|
+
cache: styleCache,
|
|
401
|
+
styleDocument,
|
|
402
|
+
rule,
|
|
403
|
+
text: label.text,
|
|
404
|
+
placement: 'line',
|
|
405
|
+
priorityClass: label.priorityClass,
|
|
406
|
+
zIndex: label.zIndex,
|
|
407
|
+
zoom
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
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
|
+
function aviationLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
419
|
+
const rule = labelRuleForSource(styleDocument, 'aip');
|
|
420
|
+
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule)) {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const aeroway = String(feature.get('aeroway') ?? '');
|
|
425
|
+
const label = aviationLabel(feature, aeroway, zoom);
|
|
426
|
+
if (!label) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return textStyle({
|
|
431
|
+
cache: styleCache,
|
|
432
|
+
styleDocument,
|
|
433
|
+
rule,
|
|
434
|
+
text: label.text,
|
|
435
|
+
placement: label.placement,
|
|
436
|
+
priorityClass: label.priorityClass,
|
|
437
|
+
zIndex: label.zIndex,
|
|
438
|
+
zoom
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
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
|
+
function poiLabelStyle(feature, zoom, styleDocument, styleCache) {
|
|
450
|
+
const rule = labelRuleForSource(styleDocument, POI_SOURCE_LAYER);
|
|
451
|
+
if (!rule || rule.enabled === false || !zoomInRule(zoom, rule) || !isSelectedPoi(feature, rule)) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const text = poiLabelText(feature);
|
|
456
|
+
if (!text) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return textStyle({
|
|
461
|
+
cache: styleCache,
|
|
462
|
+
styleDocument,
|
|
463
|
+
rule,
|
|
464
|
+
text,
|
|
465
|
+
placement: 'point',
|
|
466
|
+
priorityClass: poiPriorityClass(feature),
|
|
467
|
+
zIndex: poiZIndex(feature),
|
|
468
|
+
zoom
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
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) || (aeroway === 'heliport' ? 'H' : '');
|
|
529
|
+
return isRenderableAviationText(text, aeroway) && zoom >= 11 && zoom < 15
|
|
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 >= 12 ? { text, placement: 'line', priorityClass: 'important', zIndex: 900 } : null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (aeroway === 'terminal' || aeroway === 'apron') {
|
|
540
|
+
const text = name || ref;
|
|
541
|
+
return isMeaningfulLabel(text) && zoom >= 15 ? { 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 >= 14 ? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 } : null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @param {{
|
|
554
|
+
* cache: Map<string, Style>,
|
|
555
|
+
* styleDocument: Record<string, unknown>,
|
|
556
|
+
* rule: Record<string, unknown>,
|
|
557
|
+
* text: string,
|
|
558
|
+
* placement: 'line' | 'point',
|
|
559
|
+
* priorityClass: string,
|
|
560
|
+
* zIndex: number,
|
|
561
|
+
* zoom: number
|
|
562
|
+
* }} options
|
|
563
|
+
* @returns {Style}
|
|
564
|
+
*/
|
|
565
|
+
function textStyle(options) {
|
|
566
|
+
const priorityRule = priorityClassRule(options.styleDocument, options.priorityClass);
|
|
567
|
+
const opacity = labelOpacityForZoom(options.zoom, Number(priorityRule.opacity ?? options.rule.opacity ?? 0.82));
|
|
568
|
+
const font = labelFont(options.rule, priorityRule, options.zoom);
|
|
569
|
+
const fill = rgba(String(priorityRule.fill ?? options.rule.fill ?? '#d9fbff'), opacity);
|
|
570
|
+
const halo = rgba(String(priorityRule.halo ?? options.rule.halo ?? '#001014'), Math.min(1, opacity + 0.12));
|
|
571
|
+
const haloWidth = Number(priorityRule.haloWidth ?? options.rule.haloWidth ?? 3);
|
|
572
|
+
const key = [
|
|
573
|
+
options.text,
|
|
574
|
+
options.placement,
|
|
575
|
+
font,
|
|
576
|
+
fill,
|
|
577
|
+
halo,
|
|
578
|
+
haloWidth,
|
|
579
|
+
options.zIndex
|
|
580
|
+
].join('|');
|
|
581
|
+
|
|
582
|
+
if (options.cache.has(key)) {
|
|
583
|
+
return /** @type {Style} */ (options.cache.get(key));
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const style = new Style({
|
|
587
|
+
zIndex: options.zIndex,
|
|
588
|
+
text: new Text({
|
|
589
|
+
text: options.text,
|
|
590
|
+
placement: options.placement,
|
|
591
|
+
font,
|
|
592
|
+
fill: new Fill({ color: fill }),
|
|
593
|
+
stroke: new Stroke({ color: halo, width: haloWidth }),
|
|
594
|
+
overflow: false,
|
|
595
|
+
maxAngle: Math.PI / 5
|
|
596
|
+
})
|
|
597
|
+
});
|
|
598
|
+
options.cache.set(key, style);
|
|
599
|
+
return style;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* @param {Feature} feature
|
|
604
|
+
* @param {Record<string, unknown>} rule
|
|
605
|
+
* @returns {boolean}
|
|
606
|
+
*/
|
|
607
|
+
function isSelectedPoi(feature, rule) {
|
|
608
|
+
const categories = objectRule(rule.categories);
|
|
609
|
+
const category = poiCategoryForFeature(feature);
|
|
610
|
+
if (category && categories?.[category] === false) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const matchesClass = matchesSelectedPoiClass(feature, rule);
|
|
615
|
+
return matchesClass;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* @param {Feature} feature
|
|
620
|
+
* @param {Record<string, unknown>} rule
|
|
621
|
+
* @returns {boolean}
|
|
622
|
+
*/
|
|
623
|
+
function matchesSelectedPoiClass(feature, rule) {
|
|
624
|
+
const selected = objectRule(rule.classes) ?? DEFAULT_POI_CLASSES;
|
|
625
|
+
for (const property of [
|
|
626
|
+
'amenity',
|
|
627
|
+
'tourism',
|
|
628
|
+
'shop',
|
|
629
|
+
'leisure',
|
|
630
|
+
'railway',
|
|
631
|
+
'public_transport',
|
|
632
|
+
'station',
|
|
633
|
+
'aeroway',
|
|
634
|
+
'power',
|
|
635
|
+
'man_made',
|
|
636
|
+
'tower:type',
|
|
637
|
+
'military',
|
|
638
|
+
'emergency',
|
|
639
|
+
'office',
|
|
640
|
+
'government',
|
|
641
|
+
'boundary',
|
|
642
|
+
'protect_class',
|
|
643
|
+
'landuse',
|
|
644
|
+
'industrial'
|
|
645
|
+
]) {
|
|
646
|
+
const value = String(feature.get(property) ?? '');
|
|
647
|
+
const allowed = Array.isArray(selected[property]) ? /** @type {unknown[]} */ (selected[property]).map(String) : [];
|
|
648
|
+
if (value && allowed.includes(value)) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* @param {Feature} feature
|
|
657
|
+
* @returns {string}
|
|
658
|
+
*/
|
|
659
|
+
function poiLabelText(feature) {
|
|
660
|
+
for (const field of LABEL_TEXT_FIELDS) {
|
|
661
|
+
const text = cleanText(feature.get(field));
|
|
662
|
+
if (isMeaningfulLabel(text)) {
|
|
663
|
+
return text;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return '';
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* @param {Feature} feature
|
|
672
|
+
* @returns {string}
|
|
673
|
+
*/
|
|
674
|
+
function poiCategoryForFeature(feature) {
|
|
675
|
+
const existing = cleanText(feature.get('poi_category'));
|
|
676
|
+
if (existing) {
|
|
677
|
+
return existing;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const amenity = cleanText(feature.get('amenity'));
|
|
681
|
+
const tourism = cleanText(feature.get('tourism'));
|
|
682
|
+
const shop = cleanText(feature.get('shop'));
|
|
683
|
+
const leisure = cleanText(feature.get('leisure'));
|
|
684
|
+
const railway = cleanText(feature.get('railway'));
|
|
685
|
+
const publicTransport = cleanText(feature.get('public_transport'));
|
|
686
|
+
const station = cleanText(feature.get('station'));
|
|
687
|
+
const aeroway = cleanText(feature.get('aeroway'));
|
|
688
|
+
const power = cleanText(feature.get('power'));
|
|
689
|
+
const manMade = cleanText(feature.get('man_made'));
|
|
690
|
+
const towerType = cleanText(feature.get('tower:type'));
|
|
691
|
+
const military = cleanText(feature.get('military'));
|
|
692
|
+
const emergency = cleanText(feature.get('emergency'));
|
|
693
|
+
const office = cleanText(feature.get('office'));
|
|
694
|
+
const government = cleanText(feature.get('government'));
|
|
695
|
+
const boundary = cleanText(feature.get('boundary'));
|
|
696
|
+
const protectClass = cleanText(feature.get('protect_class'));
|
|
697
|
+
const landuse = cleanText(feature.get('landuse'));
|
|
698
|
+
const industrial = cleanText(feature.get('industrial'));
|
|
699
|
+
|
|
700
|
+
if (['railway_station', 'train_station', 'subway_station', 'bus_station', 'ferry_terminal', 'airport'].includes(amenity) ||
|
|
701
|
+
railway === 'station' || publicTransport === 'station' || station === 'subway' || aeroway === 'aerodrome' || aeroway === 'airport') {
|
|
702
|
+
return 'transport';
|
|
703
|
+
}
|
|
704
|
+
if (['hospital', 'police', 'fire_station', 'shelter'].includes(amenity) || ['ambulance_station', 'siren', 'assembly_point', 'disaster_response'].includes(emergency)) return 'emergency';
|
|
705
|
+
if (['townhall', 'courthouse', 'prison', 'embassy'].includes(amenity) || office === 'government' || government) return 'government';
|
|
706
|
+
if (power) return 'energy';
|
|
707
|
+
if (amenity === 'communications_tower' || manMade === 'communications_tower' || manMade === 'mast' || manMade === 'antenna' || towerType === 'communication') return 'communications';
|
|
708
|
+
if (boundary === 'protected_area' || leisure === 'nature_reserve' || tourism === 'national_park' || protectClass) return 'protected';
|
|
709
|
+
if (landuse === 'industrial' || industrial || ['depot', 'warehouse'].includes(amenity)) return 'industrial';
|
|
710
|
+
if (military || ['bunker', 'checkpoint'].includes(amenity)) return 'military';
|
|
711
|
+
if (shop || tourism || leisure || ['restaurant', 'cafe', 'bar', 'fast_food', 'pub'].includes(amenity)) return 'consumer';
|
|
712
|
+
return 'operational';
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* @param {number} priority
|
|
717
|
+
* @returns {string}
|
|
718
|
+
*/
|
|
719
|
+
function priorityClassFromNumber(priority) {
|
|
720
|
+
if (priority >= 900) return 'critical';
|
|
721
|
+
if (priority >= 760) return 'important';
|
|
722
|
+
if (priority >= 500) return 'normal';
|
|
723
|
+
return 'low';
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* @param {Feature} feature
|
|
728
|
+
* @returns {string}
|
|
729
|
+
*/
|
|
730
|
+
function poiPriorityClass(feature) {
|
|
731
|
+
const category = String(feature.get('poi_category') ?? '');
|
|
732
|
+
if (category === 'emergency' || category === 'military') {
|
|
733
|
+
return 'critical';
|
|
734
|
+
}
|
|
735
|
+
if (['transport', 'energy', 'communications', 'government'].includes(category)) {
|
|
736
|
+
return 'important';
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const amenity = String(feature.get('amenity') ?? '');
|
|
740
|
+
if (amenity === 'hospital' || amenity === 'police' || amenity === 'fire_station') {
|
|
741
|
+
return 'critical';
|
|
742
|
+
}
|
|
743
|
+
return 'normal';
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* @param {Feature} feature
|
|
748
|
+
* @returns {number}
|
|
749
|
+
*/
|
|
750
|
+
function poiZIndex(feature) {
|
|
751
|
+
const priority = poiPriorityClass(feature);
|
|
752
|
+
if (priority === 'critical') return 920;
|
|
753
|
+
if (priority === 'important') return 820;
|
|
754
|
+
return 620;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* @param {Record<string, unknown>} rule
|
|
759
|
+
* @param {Record<string, unknown>} priorityRule
|
|
760
|
+
* @param {number} zoom
|
|
761
|
+
* @returns {string}
|
|
762
|
+
*/
|
|
763
|
+
function labelFont(rule, priorityRule, zoom) {
|
|
764
|
+
const configured = String(priorityRule.font ?? rule.font ?? '').trim();
|
|
765
|
+
if (configured) {
|
|
766
|
+
return configured;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const weight = String(priorityRule.weight ?? rule.weight ?? 600);
|
|
770
|
+
const size = zoom >= 17 ? 12 : zoom >= 15 ? 11 : 10;
|
|
771
|
+
return `${weight} ${size}px sans-serif`;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* @param {Record<string, unknown>} styleDocument
|
|
776
|
+
* @param {string} className
|
|
777
|
+
* @returns {Record<string, unknown>}
|
|
778
|
+
*/
|
|
779
|
+
function priorityClassRule(styleDocument, className) {
|
|
780
|
+
const labels = labelConfigRoot(styleDocument);
|
|
781
|
+
const classes = labels?.priorityClasses && typeof labels.priorityClasses === 'object'
|
|
782
|
+
? /** @type {Record<string, unknown>} */ (labels.priorityClasses)
|
|
783
|
+
: {};
|
|
784
|
+
const rule = classes[className];
|
|
785
|
+
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : {};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* @param {number} zoom
|
|
790
|
+
* @param {number} base
|
|
791
|
+
* @returns {number}
|
|
792
|
+
*/
|
|
793
|
+
function labelOpacityForZoom(zoom, base) {
|
|
794
|
+
if (zoom < 12.5) return base * 0.6;
|
|
795
|
+
if (zoom < 14) return base * 0.76;
|
|
796
|
+
return base;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* @param {unknown} value
|
|
801
|
+
* @returns {string}
|
|
802
|
+
*/
|
|
803
|
+
function cleanText(value) {
|
|
804
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* @param {string} text
|
|
809
|
+
* @returns {boolean}
|
|
810
|
+
*/
|
|
811
|
+
function isMeaningfulLabel(text) {
|
|
812
|
+
const normalized = cleanText(text);
|
|
813
|
+
if (normalized.length < 2) {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const key = normalized.toLowerCase().replace(/\s+/g, '_');
|
|
818
|
+
if (GENERIC_LABEL_VALUES.has(key)) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (/^(yes|no|true|false|0|1)$/i.test(normalized)) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* @param {Feature} feature
|
|
831
|
+
* @param {string} sourceLayer
|
|
832
|
+
* @param {string} text
|
|
833
|
+
* @returns {boolean}
|
|
834
|
+
*/
|
|
835
|
+
function isExplicitAviationLabel(feature, sourceLayer, text) {
|
|
836
|
+
if (!AIP_SOURCE_LAYERS.has(sourceLayer)) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const aeroway = cleanText(feature.get('aeroway') || feature.get('class'));
|
|
841
|
+
if (text === 'H') {
|
|
842
|
+
return aeroway === 'helipad' || aeroway === 'heliport';
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (text === 'RWY') {
|
|
846
|
+
return aeroway === 'runway';
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return /^[A-Z]$/.test(text) && ['helipad', 'heliport', 'runway'].includes(aeroway);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* @param {string} ref
|
|
854
|
+
* @returns {string}
|
|
855
|
+
*/
|
|
856
|
+
function aviationRefText(ref) {
|
|
857
|
+
return ref && !/^(yes|no|true|false|0|1)$/i.test(ref) ? ref : '';
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* @param {string} text
|
|
862
|
+
* @param {string} aeroway
|
|
863
|
+
* @returns {boolean}
|
|
864
|
+
*/
|
|
865
|
+
function isRenderableAviationText(text, aeroway) {
|
|
866
|
+
if (isMeaningfulLabel(text)) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (text === 'H') {
|
|
871
|
+
return aeroway === 'helipad' || aeroway === 'heliport';
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (text === 'RWY') {
|
|
875
|
+
return aeroway === 'runway';
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return /^[A-Z]$/.test(text) && ['helipad', 'heliport', 'runway'].includes(aeroway);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* @param {Record<string, unknown>} styleDocument
|
|
883
|
+
* @returns {Record<string, unknown> | null}
|
|
884
|
+
*/
|
|
885
|
+
function labelConfigRoot(styleDocument) {
|
|
886
|
+
return styleDocument.labels && typeof styleDocument.labels === 'object'
|
|
887
|
+
? /** @type {Record<string, unknown>} */ (styleDocument.labels)
|
|
888
|
+
: null;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* @param {Record<string, unknown>} styleDocument
|
|
893
|
+
* @param {string} sourceLayer
|
|
894
|
+
* @returns {Record<string, unknown> | null}
|
|
895
|
+
*/
|
|
896
|
+
function labelRuleForSource(styleDocument, sourceLayer) {
|
|
897
|
+
const labels = labelConfigRoot(styleDocument);
|
|
898
|
+
const rule = labels?.[sourceLayer] ?? labels?.[labelSourceAlias(sourceLayer)];
|
|
899
|
+
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/**
|
|
903
|
+
* @param {string} sourceLayer
|
|
904
|
+
* @returns {string}
|
|
905
|
+
*/
|
|
906
|
+
function labelSourceAlias(sourceLayer) {
|
|
907
|
+
if (sourceLayer === 'aip') return 'aviation';
|
|
908
|
+
if (sourceLayer === 'aviation') return 'aip';
|
|
909
|
+
return sourceLayer;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* @param {unknown} rule
|
|
914
|
+
* @returns {Record<string, unknown> | null}
|
|
915
|
+
*/
|
|
916
|
+
function objectRule(rule) {
|
|
917
|
+
return rule && typeof rule === 'object' ? /** @type {Record<string, unknown>} */ (rule) : null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* @param {number} zoom
|
|
922
|
+
* @param {Record<string, unknown>} rule
|
|
923
|
+
* @returns {boolean}
|
|
924
|
+
*/
|
|
925
|
+
function zoomInRule(zoom, rule) {
|
|
926
|
+
const minZoom = Number(rule.minZoom ?? 0);
|
|
927
|
+
const maxZoom = Number(rule.maxZoom ?? 22);
|
|
928
|
+
return (!Number.isFinite(minZoom) || zoom >= minZoom) && (!Number.isFinite(maxZoom) || zoom <= maxZoom);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* @param {number} resolution
|
|
933
|
+
* @returns {number}
|
|
934
|
+
*/
|
|
935
|
+
function resolutionToZoom(resolution) {
|
|
936
|
+
const initialResolution = 156543.03392804097;
|
|
937
|
+
return Math.log2(initialResolution / Number(resolution));
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* @param {string} color
|
|
942
|
+
* @param {number} opacity
|
|
943
|
+
* @returns {string}
|
|
944
|
+
*/
|
|
945
|
+
function rgba(color, opacity) {
|
|
946
|
+
const rgb = hexToRgb(color);
|
|
947
|
+
if (!rgb) {
|
|
948
|
+
return color;
|
|
949
|
+
}
|
|
950
|
+
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp(opacity, 0, 1)})`;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* @param {string} color
|
|
955
|
+
* @returns {[number, number, number] | null}
|
|
956
|
+
*/
|
|
957
|
+
function hexToRgb(color) {
|
|
958
|
+
const match = /^#?([0-9a-f]{6})$/i.exec(color);
|
|
959
|
+
if (!match) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
const value = Number.parseInt(match[1], 16);
|
|
963
|
+
return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* @param {number} value
|
|
968
|
+
* @param {number} min
|
|
969
|
+
* @param {number} max
|
|
970
|
+
* @returns {number}
|
|
971
|
+
*/
|
|
972
|
+
function clamp(value, min, max) {
|
|
973
|
+
if (!Number.isFinite(value)) {
|
|
974
|
+
return min;
|
|
975
|
+
}
|
|
976
|
+
return Math.max(min, Math.min(max, value));
|
|
977
|
+
}
|