@map-zero/ol 0.2.0 → 0.3.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/README.md ADDED
@@ -0,0 +1,8 @@
1
+ # @map-zero/ol
2
+
3
+ OpenLayers integration helpers for map-zero packages.
4
+
5
+ See the main repository for documentation, examples, and release notes:
6
+
7
+ https://github.com/mploscos/map-zero
8
+
package/package.json CHANGED
@@ -1,12 +1,29 @@
1
1
  {
2
2
  "name": "@map-zero/ol",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "OpenLayers integration helper for map-zero packages.",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/mploscos/map-zero#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mploscos/map-zero.git",
11
+ "directory": "packages/ol"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/mploscos/map-zero/issues"
15
+ },
6
16
  "main": "./src/index.js",
7
17
  "exports": {
8
18
  ".": "./src/index.js"
9
19
  },
20
+ "files": [
21
+ "src",
22
+ "README.md"
23
+ ],
24
+ "dependencies": {
25
+ "@map-zero/raster": "0.3.0"
26
+ },
10
27
  "peerDependencies": {
11
28
  "ol": "^10.9.0",
12
29
  "pmtiles": "^4.4.1"
package/src/index.js CHANGED
@@ -7,6 +7,10 @@ import VectorTileSource from 'ol/source/VectorTile.js';
7
7
  import { createXYZ } from 'ol/tilegrid.js';
8
8
  import { PMTiles } from 'pmtiles';
9
9
 
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';
13
+
10
14
  import {
11
15
  activeLabelLayerIdsForZoom,
12
16
  createMapZeroLabelLayer,
@@ -15,6 +19,35 @@ import {
15
19
 
16
20
  let autoInstanceCounter = 0;
17
21
 
22
+ const ROAD_HIGHWAY_DRAW_ORDER = [
23
+ 'platform',
24
+ 'corridor',
25
+ 'steps',
26
+ 'footway',
27
+ 'cycleway',
28
+ 'path',
29
+ 'track',
30
+ 'service',
31
+ 'construction',
32
+ 'proposed',
33
+ 'living_street',
34
+ 'pedestrian',
35
+ 'road',
36
+ 'unclassified',
37
+ 'residential',
38
+ 'busway',
39
+ 'tertiary_link',
40
+ 'tertiary',
41
+ 'secondary_link',
42
+ 'secondary',
43
+ 'primary_link',
44
+ 'primary',
45
+ 'trunk_link',
46
+ 'trunk',
47
+ 'motorway_link',
48
+ 'motorway'
49
+ ];
50
+
18
51
  /**
19
52
  * @typedef {{
20
53
  * id?: string,
@@ -25,7 +58,6 @@ let autoInstanceCounter = 0;
25
58
  * source?: 'auto' | 'pmtiles' | 'dynamic',
26
59
  * renderMode?: 'vector' | 'raster-worker',
27
60
  * workerUrl?: string | URL,
28
- * rasterWorkerUrl?: string | URL,
29
61
  * rasterPixelRatio?: number,
30
62
  * overzoomLevels?: number,
31
63
  * edgeGuardPixels?: number,
@@ -131,7 +163,7 @@ export async function createMapZeroOpenLayersLayers(options) {
131
163
  applyLayerZIndex(layer, orderedLayers, styleDocument, options.zIndexBase);
132
164
  if (labelController) {
133
165
  tagOpenLayersLayer(labelController.layer, instanceId, orderedLayers.map((item) => item.id), 'labels');
134
- labelController.layer.setZIndex(layer.getZIndex() + 100);
166
+ labelController.layer.setZIndex(layer.getZIndex() + 1);
135
167
  }
136
168
 
137
169
  const refresh = () => {
@@ -208,7 +240,7 @@ function createRasterWorkerController(context, options) {
208
240
  manifestUrl: context.manifestUrl,
209
241
  styleDocument: context.styleDocument,
210
242
  layers: context.orderedLayers.map((layer) => layer.id),
211
- workerUrl: options.rasterWorkerUrl ?? options.workerUrl ?? new URL('@map-zero/cesium/imagery-worker.js', import.meta.url),
243
+ workerUrl: options.workerUrl ?? new URL('@map-zero/raster/imagery-worker.js', import.meta.url),
212
244
  rasterPixelRatio: options.rasterPixelRatio,
213
245
  overzoomLevels: options.overzoomLevels,
214
246
  edgeGuardPixels: options.edgeGuardPixels
@@ -244,7 +276,7 @@ function createRasterWorkerController(context, options) {
244
276
 
245
277
  const refresh = () => {
246
278
  worker.setVisibleLayers(context.layerVisibility);
247
- source.clear();
279
+ refreshRasterSource(source);
248
280
  layer.changed();
249
281
  };
250
282
 
@@ -269,13 +301,18 @@ function createRasterWorkerController(context, options) {
269
301
  layer.setOpacity(clamp(Number(opacity), 0, 1));
270
302
  },
271
303
  destroy() {
272
- source.clear();
304
+ refreshRasterSource(source);
273
305
  worker.destroy();
274
306
  layer.dispose();
275
307
  }
276
308
  };
277
309
  }
278
310
 
311
+ function refreshRasterSource(source) {
312
+ source.clear();
313
+ source.refresh?.();
314
+ }
315
+
279
316
  class MapZeroRasterTileWorker {
280
317
  /**
281
318
  * @param {{
@@ -283,10 +320,10 @@ class MapZeroRasterTileWorker {
283
320
  * manifestUrl: string,
284
321
  * styleDocument: Record<string, unknown>,
285
322
  * layers: string[],
286
- * workerUrl: string | URL,
287
- * rasterPixelRatio?: number,
288
- * overzoomLevels?: number,
289
- * edgeGuardPixels?: number
323
+ * workerUrl: string | URL,
324
+ * rasterPixelRatio?: number,
325
+ * overzoomLevels?: number,
326
+ * edgeGuardPixels?: number
290
327
  * }} options
291
328
  */
292
329
  constructor(options) {
@@ -533,11 +570,7 @@ function createLayerVisibility(orderedLayers, styleDocument, visibleLayers) {
533
570
  * @param {number | undefined} zIndexBase
534
571
  */
535
572
  function applyLayerZIndex(layer, orderedLayers, styleDocument, zIndexBase) {
536
- const maxOrder = orderedLayers.reduce((max, item) => {
537
- const order = Number(getLayerRule(styleDocument, item).order);
538
- return Number.isFinite(order) ? Math.max(max, order) : max;
539
- }, 0);
540
- layer.setZIndex((Number.isFinite(Number(zIndexBase)) ? Number(zIndexBase) : 0) + maxOrder);
573
+ layer.setZIndex(Number.isFinite(Number(zIndexBase)) ? Number(zIndexBase) : 0);
541
574
  }
542
575
 
543
576
  /**
@@ -968,11 +1001,54 @@ function createWebGlStyles(context) {
968
1001
  function createRoadStyleRules(filter, rule, layerOpacity) {
969
1002
  return [
970
1003
  ...createRoadSemanticUnderlayRules(filter, rule, layerOpacity),
971
- ...createLayerStyleRules(filter, rule, 'line', 'roads', layerOpacity),
1004
+ ...createRoadPriorityStyleRules(filter, rule, layerOpacity),
972
1005
  ...createRoadSemanticOverlayRules(filter, rule, layerOpacity)
973
1006
  ];
974
1007
  }
975
1008
 
1009
+ /**
1010
+ * Draw road classes from least important to most important so local/darker
1011
+ * roads stay below brighter major roads at intersections.
1012
+ *
1013
+ * @param {unknown[]} filter
1014
+ * @param {Record<string, unknown>} rule
1015
+ * @param {Map<string, number>} layerOpacity
1016
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
1017
+ */
1018
+ function createRoadPriorityStyleRules(filter, rule, layerOpacity) {
1019
+ const highwayRules = roadHighwayRules(rule);
1020
+ if (!highwayRules) {
1021
+ return createLayerStyleRules(filter, rule, 'line', 'roads', layerOpacity);
1022
+ }
1023
+
1024
+ const entries = [];
1025
+ const orderedHighways = orderedRoadHighwayValues(highwayRules);
1026
+ const baseRule = removeStylePropertyRules(rule, 'highway');
1027
+ const fallbackFilter = ['all', filter, ['!', propertyIn('highway', orderedHighways)]];
1028
+ entries.push({
1029
+ filter: fallbackFilter,
1030
+ rule: baseRule
1031
+ });
1032
+
1033
+ for (const highway of orderedHighways) {
1034
+ const override = highwayRules[highway];
1035
+ if (styleOverrideVisible(override) === false) {
1036
+ continue;
1037
+ }
1038
+
1039
+ const highwayFilter = roadOverrideZoomFilter(
1040
+ ['all', filter, propertyEquals('highway', highway)],
1041
+ override
1042
+ );
1043
+ entries.push({
1044
+ filter: highwayFilter,
1045
+ rule: mergeStyleOverride(baseRule, override)
1046
+ });
1047
+ }
1048
+
1049
+ return createPhasedRoadStyleRules(entries, layerOpacity);
1050
+ }
1051
+
976
1052
  /**
977
1053
  * @param {unknown[]} filter
978
1054
  * @param {Record<string, unknown>} rule
@@ -1229,6 +1305,132 @@ function createLayerStyleRules(filter, rule, layerType, layerId, layerOpacity) {
1229
1305
  }));
1230
1306
  }
1231
1307
 
1308
+ /**
1309
+ * @param {Array<{ filter: unknown[], rule: Record<string, unknown> }>} entries
1310
+ * @param {Map<string, number>} layerOpacity
1311
+ * @returns {Array<{ filter: unknown[], style: Record<string, unknown> }>}
1312
+ */
1313
+ function createPhasedRoadStyleRules(entries, layerOpacity) {
1314
+ const phases = [];
1315
+ for (const entry of entries) {
1316
+ const visibleFilter = createPropertyVisibilityFilter(entry.filter, entry.rule);
1317
+ const parts = createLayerStyleParts(entry.rule, 'line', 'roads', layerOpacity);
1318
+ for (let index = 0; index < parts.length; index += 1) {
1319
+ if (!phases[index]) {
1320
+ phases[index] = [];
1321
+ }
1322
+ phases[index].push({
1323
+ filter: visibleFilter,
1324
+ style: parts[index]
1325
+ });
1326
+ }
1327
+ }
1328
+
1329
+ return phases.flat();
1330
+ }
1331
+
1332
+ /**
1333
+ * @param {Record<string, unknown>} rule
1334
+ * @returns {Record<string, unknown> | null}
1335
+ */
1336
+ function roadHighwayRules(rule) {
1337
+ const byProperty = rule.byProperty && typeof rule.byProperty === 'object'
1338
+ ? /** @type {Record<string, unknown>} */ (rule.byProperty)
1339
+ : null;
1340
+ const highway = byProperty?.highway;
1341
+ return highway && typeof highway === 'object'
1342
+ ? /** @type {Record<string, unknown>} */ (highway)
1343
+ : null;
1344
+ }
1345
+
1346
+ /**
1347
+ * @param {Record<string, unknown>} values
1348
+ * @returns {string[]}
1349
+ */
1350
+ function orderedRoadHighwayValues(values) {
1351
+ const configured = Object.keys(values);
1352
+ const ordered = ROAD_HIGHWAY_DRAW_ORDER.filter((value) => configured.includes(value));
1353
+ return [
1354
+ ...configured.filter((value) => !ROAD_HIGHWAY_DRAW_ORDER.includes(value)),
1355
+ ...ordered
1356
+ ];
1357
+ }
1358
+
1359
+ /**
1360
+ * @param {Record<string, unknown>} rule
1361
+ * @param {string} property
1362
+ * @returns {Record<string, unknown>}
1363
+ */
1364
+ function removeStylePropertyRules(rule, property) {
1365
+ const byProperty = rule.byProperty && typeof rule.byProperty === 'object'
1366
+ ? { .../** @type {Record<string, unknown>} */ (rule.byProperty) }
1367
+ : null;
1368
+ if (!byProperty || !(property in byProperty)) {
1369
+ return rule;
1370
+ }
1371
+
1372
+ delete byProperty[property];
1373
+ const stripped = { ...rule };
1374
+ if (Object.keys(byProperty).length === 0) {
1375
+ delete stripped.byProperty;
1376
+ } else {
1377
+ stripped.byProperty = byProperty;
1378
+ }
1379
+ return stripped;
1380
+ }
1381
+
1382
+ /**
1383
+ * @param {Record<string, unknown>} rule
1384
+ * @param {unknown} override
1385
+ * @returns {Record<string, unknown>}
1386
+ */
1387
+ function mergeStyleOverride(rule, override) {
1388
+ if (!override || typeof override !== 'object') {
1389
+ return rule;
1390
+ }
1391
+
1392
+ const merged = {
1393
+ ...rule,
1394
+ .../** @type {Record<string, unknown>} */ (override)
1395
+ };
1396
+ for (const key of ['body', 'casing', 'center', 'centerLine', 'glow', 'semantics', 'visibility']) {
1397
+ const baseValue = rule[key];
1398
+ const overrideValue = /** @type {Record<string, unknown>} */ (override)[key];
1399
+ if (
1400
+ baseValue && typeof baseValue === 'object' &&
1401
+ overrideValue && typeof overrideValue === 'object'
1402
+ ) {
1403
+ merged[key] = {
1404
+ .../** @type {Record<string, unknown>} */ (baseValue),
1405
+ .../** @type {Record<string, unknown>} */ (overrideValue)
1406
+ };
1407
+ }
1408
+ }
1409
+
1410
+ return normalizeStyleRule(merged);
1411
+ }
1412
+
1413
+ /**
1414
+ * @param {unknown[]} filter
1415
+ * @param {unknown} override
1416
+ * @returns {unknown[]}
1417
+ */
1418
+ function roadOverrideZoomFilter(filter, override) {
1419
+ if (!override || typeof override !== 'object') {
1420
+ return filter;
1421
+ }
1422
+
1423
+ const normalized = normalizeStyleRule(/** @type {Record<string, unknown>} */ (override));
1424
+ let next = filter;
1425
+ if (Number.isFinite(normalized.minZoom)) {
1426
+ next = ['all', next, ['>=', ['zoom'], Number(normalized.minZoom)]];
1427
+ }
1428
+ if (Number.isFinite(normalized.maxZoom)) {
1429
+ next = ['all', next, ['<=', ['zoom'], Number(normalized.maxZoom)]];
1430
+ }
1431
+ return next;
1432
+ }
1433
+
1232
1434
  /**
1233
1435
  * @param {unknown[]} filter
1234
1436
  * @param {Record<string, unknown>} rule
@@ -1480,24 +1682,6 @@ function getLayerRule(styleDocument, layer) {
1480
1682
  return normalizeStyleRule(layers[id] || layers[layerAlias(id)] || {});
1481
1683
  }
1482
1684
 
1483
- /**
1484
- * @param {string} layerId
1485
- * @returns {boolean}
1486
- */
1487
- function isAipLayer(layerId) {
1488
- return layerId === 'aip' || layerId === 'aviation';
1489
- }
1490
-
1491
- /**
1492
- * @param {string} layerId
1493
- * @returns {string}
1494
- */
1495
- function layerAlias(layerId) {
1496
- if (layerId === 'aip') return 'aviation';
1497
- if (layerId === 'aviation') return 'aip';
1498
- return layerId;
1499
- }
1500
-
1501
1685
  /**
1502
1686
  * Normalize the public declarative style schema to the internal style keys.
1503
1687
  *
@@ -1912,15 +2096,6 @@ function isValidTileCoord(z, x, y) {
1912
2096
  return Number.isInteger(x) && Number.isInteger(y) && x >= 0 && y >= 0 && x < maxIndex && y < maxIndex;
1913
2097
  }
1914
2098
 
1915
- /**
1916
- * @param {Record<string, unknown>} manifest
1917
- * @returns {{ url?: string, minZoom?: unknown, maxZoom?: unknown }}
1918
- */
1919
- function pmtilesInfo(manifest) {
1920
- const tiles = manifest.tiles && typeof manifest.tiles === 'object' ? manifest.tiles : {};
1921
- return /** @type {{ url?: string, minZoom?: unknown, maxZoom?: unknown }} */ (tiles);
1922
- }
1923
-
1924
2099
  /**
1925
2100
  * @param {number} width
1926
2101
  * @param {number} height
@@ -1953,28 +2128,6 @@ function preferNearestZoomLevel(value, high, low) {
1953
2128
  return value - low * Math.sqrt(high / low);
1954
2129
  }
1955
2130
 
1956
- /**
1957
- * @param {unknown} value
1958
- * @param {number} min
1959
- * @param {number} max
1960
- * @returns {number}
1961
- */
1962
- function clampInteger(value, min, max) {
1963
- const number = Math.trunc(Number(value));
1964
- return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : min;
1965
- }
1966
-
1967
- /**
1968
- * @param {unknown} value
1969
- * @param {number} min
1970
- * @param {number} max
1971
- * @returns {number}
1972
- */
1973
- function clampNumber(value, min, max) {
1974
- const number = Number(value);
1975
- return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : min;
1976
- }
1977
-
1978
2131
  /**
1979
2132
  * @param {number} z
1980
2133
  * @param {number} x
package/src/labels.js CHANGED
@@ -525,30 +525,38 @@ function aviationLabel(feature, aeroway, zoom) {
525
525
  const name = cleanText(feature.get('name'));
526
526
 
527
527
  if (aeroway === 'aerodrome' || aeroway === 'heliport') {
528
- const text = name || aviationRefText(ref) || (aeroway === 'heliport' ? 'H' : '');
529
- return isRenderableAviationText(text, aeroway) && zoom >= 11 && zoom < 15
528
+ const text = name || aviationRefText(ref) || aviationFallbackText(aeroway);
529
+ return isRenderableAviationText(text, aeroway) && zoom >= 9
530
530
  ? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 }
531
531
  : null;
532
532
  }
533
533
 
534
534
  if (aeroway === 'runway') {
535
535
  const text = aviationRefText(ref) || name || 'RWY';
536
- return isRenderableAviationText(text, aeroway) && zoom >= 12 ? { text, placement: 'line', priorityClass: 'important', zIndex: 900 } : null;
536
+ return isRenderableAviationText(text, aeroway) && zoom >= 11 ? { text, placement: 'line', priorityClass: 'important', zIndex: 900 } : null;
537
537
  }
538
538
 
539
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;
540
+ const text = name || ref || aviationFallbackText(aeroway);
541
+ return isMeaningfulLabel(text) && zoom >= 14 ? { text, placement: 'point', priorityClass: 'normal', zIndex: 720 } : null;
542
542
  }
543
543
 
544
544
  if (aeroway === 'helipad') {
545
545
  const text = aviationRefText(ref) || name || 'H';
546
- return isRenderableAviationText(text, aeroway) && zoom >= 14 ? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 } : null;
546
+ return isRenderableAviationText(text, aeroway) && zoom >= 12 ? { text, placement: 'point', priorityClass: 'critical', zIndex: 980 } : null;
547
547
  }
548
548
 
549
549
  return null;
550
550
  }
551
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
+
552
560
  /**
553
561
  * @param {{
554
562
  * cache: Map<string, Style>,
@@ -664,7 +672,81 @@ function poiLabelText(feature) {
664
672
  }
665
673
  }
666
674
 
667
- return '';
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);
668
750
  }
669
751
 
670
752
  /**
@@ -804,6 +886,12 @@ function cleanText(value) {
804
886
  return String(value ?? '').replace(/\s+/g, ' ').trim();
805
887
  }
806
888
 
889
+ function titleCase(value) {
890
+ return cleanText(value)
891
+ .replace(/_/g, ' ')
892
+ .replace(/\b\w/g, (char) => char.toUpperCase());
893
+ }
894
+
807
895
  /**
808
896
  * @param {string} text
809
897
  * @returns {boolean}