@map-zero/ol 0.2.1 → 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.1",
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,
@@ -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) {
@@ -964,11 +1001,54 @@ function createWebGlStyles(context) {
964
1001
  function createRoadStyleRules(filter, rule, layerOpacity) {
965
1002
  return [
966
1003
  ...createRoadSemanticUnderlayRules(filter, rule, layerOpacity),
967
- ...createLayerStyleRules(filter, rule, 'line', 'roads', layerOpacity),
1004
+ ...createRoadPriorityStyleRules(filter, rule, layerOpacity),
968
1005
  ...createRoadSemanticOverlayRules(filter, rule, layerOpacity)
969
1006
  ];
970
1007
  }
971
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
+
972
1052
  /**
973
1053
  * @param {unknown[]} filter
974
1054
  * @param {Record<string, unknown>} rule
@@ -1225,6 +1305,132 @@ function createLayerStyleRules(filter, rule, layerType, layerId, layerOpacity) {
1225
1305
  }));
1226
1306
  }
1227
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
+
1228
1434
  /**
1229
1435
  * @param {unknown[]} filter
1230
1436
  * @param {Record<string, unknown>} rule
@@ -1476,24 +1682,6 @@ function getLayerRule(styleDocument, layer) {
1476
1682
  return normalizeStyleRule(layers[id] || layers[layerAlias(id)] || {});
1477
1683
  }
1478
1684
 
1479
- /**
1480
- * @param {string} layerId
1481
- * @returns {boolean}
1482
- */
1483
- function isAipLayer(layerId) {
1484
- return layerId === 'aip' || layerId === 'aviation';
1485
- }
1486
-
1487
- /**
1488
- * @param {string} layerId
1489
- * @returns {string}
1490
- */
1491
- function layerAlias(layerId) {
1492
- if (layerId === 'aip') return 'aviation';
1493
- if (layerId === 'aviation') return 'aip';
1494
- return layerId;
1495
- }
1496
-
1497
1685
  /**
1498
1686
  * Normalize the public declarative style schema to the internal style keys.
1499
1687
  *
@@ -1908,15 +2096,6 @@ function isValidTileCoord(z, x, y) {
1908
2096
  return Number.isInteger(x) && Number.isInteger(y) && x >= 0 && y >= 0 && x < maxIndex && y < maxIndex;
1909
2097
  }
1910
2098
 
1911
- /**
1912
- * @param {Record<string, unknown>} manifest
1913
- * @returns {{ url?: string, minZoom?: unknown, maxZoom?: unknown }}
1914
- */
1915
- function pmtilesInfo(manifest) {
1916
- const tiles = manifest.tiles && typeof manifest.tiles === 'object' ? manifest.tiles : {};
1917
- return /** @type {{ url?: string, minZoom?: unknown, maxZoom?: unknown }} */ (tiles);
1918
- }
1919
-
1920
2099
  /**
1921
2100
  * @param {number} width
1922
2101
  * @param {number} height
@@ -1949,28 +2128,6 @@ function preferNearestZoomLevel(value, high, low) {
1949
2128
  return value - low * Math.sqrt(high / low);
1950
2129
  }
1951
2130
 
1952
- /**
1953
- * @param {unknown} value
1954
- * @param {number} min
1955
- * @param {number} max
1956
- * @returns {number}
1957
- */
1958
- function clampInteger(value, min, max) {
1959
- const number = Math.trunc(Number(value));
1960
- return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : min;
1961
- }
1962
-
1963
- /**
1964
- * @param {unknown} value
1965
- * @param {number} min
1966
- * @param {number} max
1967
- * @returns {number}
1968
- */
1969
- function clampNumber(value, min, max) {
1970
- const number = Number(value);
1971
- return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : min;
1972
- }
1973
-
1974
2131
  /**
1975
2132
  * @param {number} z
1976
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}