@tileflow/core 0.1.0-alpha.35 → 0.1.0-alpha.36

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/dist/build.js CHANGED
@@ -52,13 +52,14 @@ var tileflowDefaultBuildings3DDesign = Object.freeze({
52
52
  layers: Object.freeze({ building: "building", places: "poi" }),
53
53
  color: "#d8d0c0",
54
54
  edges: 0,
55
- lighting: "style",
55
+ edgeRadius: 0.8,
56
+ lighting: "sun",
56
57
  colorVariation: 0,
57
58
  verticalGradient: true,
58
59
  nightLight: 0,
59
60
  windows: false,
60
- shadows: false,
61
- occlusion: false
61
+ shadows: true,
62
+ occlusion: true
62
63
  });
63
64
  function tileflowBuildingsMetadata(design) {
64
65
  return { version: 1, ...design };
@@ -212,6 +213,7 @@ function tileflowWorldV1Schema(options = {}) {
212
213
  ...options,
213
214
  capabilities: { ...options.capabilities, bathymetry: true, globalLandcover: true },
214
215
  fields: {
216
+ hedge: "hedge",
215
217
  shieldKind: "shield_kind",
216
218
  shieldLineLengthMeters: "shield_line_length_m",
217
219
  shieldNetwork: "shield_network",
@@ -225,6 +227,7 @@ function tileflowWorldV1Schema(options = {}) {
225
227
  roadShield: "transportation_shield",
226
228
  sidewalk: "sidewalk",
227
229
  streetFurniture: "street_furniture",
230
+ trafficControl: "traffic_control",
228
231
  ...options.layers
229
232
  },
230
233
  semantics: { ...options.semantics, parkLayer: "protected-only" }
@@ -650,237 +653,98 @@ function compareCodeUnits(left, right) {
650
653
  return left < right ? -1 : left > right ? 1 : 0;
651
654
  }
652
655
 
653
- // src/marine/index.ts
654
- var tileflowBathymetrySourceId = "tileflow-bathymetry";
655
- var tileflowBathymetryDemSourceId = "tileflow-bathymetry-dem";
656
- var tileflowNauticalSourceId = "tileflow-nautical";
657
- var tileflowBathymetryV1Schema = Object.freeze({
658
- fields: Object.freeze({
659
- class: "class",
660
- depth: "depth",
661
- id: "id",
662
- maxZoom: "max_zoom",
663
- minDepth: "min_depth",
664
- minZoom: "min_zoom",
665
- name: "name",
666
- sortKey: "sort_key"
667
- }),
668
- layer: "bathymetry",
669
- layers: Object.freeze({
670
- bands: "bathymetry",
671
- contours: "bathymetry_contour",
672
- coverage: "bathymetry_coverage",
673
- landforms: "seafloor_landform",
674
- waterNames: "water_name"
675
- }),
676
- optionalLayers: Object.freeze(["contours", "coverage", "landforms", "waterNames"]),
677
- product: "bathymetry-v1",
678
- requiredLayers: Object.freeze(["bands"]),
679
- schemaVersion: 1,
680
- sources: Object.freeze({
681
- dem: Object.freeze({
682
- encoding: "terrarium",
683
- sourceId: tileflowBathymetryDemSourceId,
684
- tileSize: 512
685
- }),
686
- vector: Object.freeze({ sourceId: tileflowBathymetrySourceId })
687
- })
688
- });
689
- var tileflowNauticalV1Schema = Object.freeze({
690
- fields: Object.freeze({
691
- cell: "cell",
692
- character: "character",
693
- class: "class",
694
- color: "color",
695
- coverage: "coverage",
696
- depth: "depth",
697
- direction: "direction",
698
- edition: "edition",
699
- licence: "licence",
700
- name: "name",
701
- provider: "provider",
702
- provenance: "provenance",
703
- quality: "quality",
704
- rangeNm: "range_nm",
705
- scale: "scale",
706
- subclass: "subclass",
707
- update: "update"
708
- }),
709
- geometryTypes: Object.freeze({
710
- aids: Object.freeze(["Point"]),
711
- coverage: Object.freeze(["Polygon"]),
712
- hazards: Object.freeze(["Point", "Polygon"]),
713
- lights: Object.freeze(["Point"]),
714
- navigationAreas: Object.freeze(["Polygon"]),
715
- reefs: Object.freeze(["Polygon"]),
716
- soundings: Object.freeze(["Point"]),
717
- wrecks: Object.freeze(["Point", "Polygon"])
718
- }),
719
- layers: Object.freeze({
720
- aids: "aid",
721
- coverage: "coverage",
722
- hazards: "hazard",
723
- lights: "light",
724
- navigationAreas: "navigation_area",
725
- reefs: "reef",
726
- soundings: "sounding",
727
- wrecks: "wreck"
728
- }),
729
- product: "nautical-v1",
730
- schemaVersion: 1
731
- });
732
- var tileflowBathymetryNameLayerLimits = Object.freeze({ maxZoomRanges: 128 });
733
- function resolveMarine(marine, apiBaseUrl = "https://api.tileflow.dev") {
734
- if (marine === void 0) return void 0;
735
- if (typeof marine === "string") {
736
- switch (marine) {
737
- case "none":
738
- return {};
739
- case "bathymetry":
740
- return { bathymetry: resolveBathymetry({}, apiBaseUrl) };
741
- case "nautical":
742
- return { nautical: resolveNauticalSource({}, apiBaseUrl) };
743
- case "chart":
744
- return {
745
- bathymetry: resolveBathymetry({}, apiBaseUrl),
746
- nautical: resolveNauticalSource({}, apiBaseUrl)
747
- };
748
- }
749
- }
750
- return {
751
- ...marine.bathymetry === false ? {} : { bathymetry: resolveBathymetry(marine.bathymetry ?? {}, apiBaseUrl) },
752
- ...marine.nautical === false ? {} : { nautical: resolveNauticalSource(marine.nautical ?? {}, apiBaseUrl) }
753
- };
754
- }
755
- function resolveBathymetry(input, apiBaseUrl) {
756
- const advanced = "type" in input && input.type === "bathymetry" ? input : void 0;
757
- const display = advanced?.display ?? "bands";
758
- const usesVector = display === "bands" || display === "hybrid";
759
- const usesRelief = display === "relief" || display === "hybrid";
760
- const vectorConfig = {
761
- ...input.attribution === void 0 ? {} : { attribution: input.attribution },
762
- ...input.sourceId === void 0 ? {} : { sourceId: input.sourceId },
763
- ...input.url === void 0 ? {} : { url: input.url }
764
- };
765
- const reliefConfig = advanced?.relief === false ? void 0 : advanced?.relief;
766
- const reliefVisible = usesRelief && reliefConfig?.visible !== false && !(typeof reliefConfig?.opacity === "number" && reliefConfig.opacity === 0);
767
- return {
768
- ...advanced?.bands === void 0 ? {} : { bands: advanced.bands },
769
- ...advanced?.contours === void 0 ? {} : { contours: advanced.contours },
770
- display,
771
- ...advanced?.labels === void 0 ? {} : { labels: advanced.labels },
772
- ...advanced?.vectorLayers === void 0 ? {} : { vectorLayers: cloneJson(advanced.vectorLayers) },
773
- ...reliefVisible ? { relief: resolveBathymetryRelief(reliefConfig ?? {}, apiBaseUrl) } : {},
774
- ...usesVector ? {
775
- vector: resolveMarineSource({
776
- config: vectorConfig,
777
- defaultPath: "/tiles/bathymetry/tiles.json",
778
- defaultSourceId: tileflowBathymetrySourceId,
779
- kind: "tileflow-bathymetry",
780
- product: tileflowBathymetryV1Schema.product,
781
- apiBaseUrl
782
- })
783
- } : {}
784
- };
785
- }
786
- function resolveBathymetryRelief(style, apiBaseUrl) {
787
- const sourceId = validateSourceId(style.sourceId ?? tileflowBathymetryDemSourceId);
788
- const url = validateBathymetryDemUrl(
789
- style.url ?? new URL("/tiles/bathymetry/dem/tiles.json", normalizeApiBaseUrl2(apiBaseUrl)).toString()
790
- );
791
- const encoding = style.encoding ?? "terrarium";
792
- const tileSize = style.tileSize ?? 512;
793
- return {
794
- identity: {
795
- encoding,
796
- kind: "tileflow-bathymetry-dem",
797
- product: tileflowBathymetryV1Schema.product,
798
- schemaVersion: 1,
799
- sourceId,
800
- tileSize,
801
- url
802
- },
803
- source: {
804
- ...style.attribution ? { attribution: validateAttribution(style.attribution) } : {},
805
- encoding,
806
- tileSize,
807
- type: "raster-dem",
808
- url
809
- },
810
- sourceId,
811
- style: cloneJson(style)
812
- };
813
- }
814
- function resolveNauticalSource(config, apiBaseUrl) {
815
- return resolveMarineSource({
816
- config,
817
- defaultPath: "/tiles/nautical/tiles.json",
818
- defaultSourceId: tileflowNauticalSourceId,
819
- kind: "tileflow-nautical",
820
- product: tileflowNauticalV1Schema.product,
821
- apiBaseUrl
822
- });
823
- }
824
- function resolveMarineSource(options) {
825
- const sourceId = validateSourceId(options.config.sourceId ?? options.defaultSourceId);
826
- const url = validatePublicVectorUrl(
827
- options.config.url ?? new URL(options.defaultPath, normalizeApiBaseUrl2(options.apiBaseUrl)).toString()
828
- );
829
- const identity = {
830
- kind: options.kind,
831
- product: options.product,
832
- schemaVersion: 1,
833
- sourceId,
834
- url
835
- };
836
- return {
837
- identity,
838
- source: {
839
- ...options.config.attribution ? { attribution: validateAttribution(options.config.attribution) } : {},
840
- type: "vector",
841
- url
842
- },
843
- sourceId
844
- };
845
- }
846
- function validateSourceId(value) {
847
- const normalized = value.trim();
848
- if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(normalized) || normalized !== value) {
849
- throw new Error(
850
- "Tileflow marine sourceId must be a portable identifier of at most 64 characters."
851
- );
852
- }
853
- return normalized;
656
+ // src/cartography/contributions.ts
657
+ var tileflowLayerSlots = [
658
+ "background",
659
+ "land",
660
+ "terrain",
661
+ "hydro",
662
+ "building-areas",
663
+ "transport-areas",
664
+ "transport-tunnel-shadow",
665
+ "transport-tunnel-casing",
666
+ "transport-tunnel-fill",
667
+ "transport-pedestrian-areas",
668
+ "aeroways",
669
+ "transport-surface-shadow",
670
+ "transport-surface-casing",
671
+ "transport-surface-fill",
672
+ "transport-bridge-shadow",
673
+ "transport-bridge-casing",
674
+ "transport-bridge-fill",
675
+ "transport-symbols",
676
+ "boundaries",
677
+ "buildings",
678
+ "vegetation",
679
+ "symbols"
680
+ ];
681
+ var tileflowLayerBands = [
682
+ "land",
683
+ "relief",
684
+ "water",
685
+ "roads",
686
+ "boundaries",
687
+ "buildings",
688
+ "vegetation",
689
+ "labels"
690
+ ];
691
+ var bandSlots = {
692
+ land: ["background", "land"],
693
+ relief: ["terrain", "terrain"],
694
+ water: ["hydro", "hydro"],
695
+ roads: ["transport-areas", "transport-symbols"],
696
+ boundaries: ["boundaries", "boundaries"],
697
+ buildings: ["buildings", "buildings"],
698
+ vegetation: ["vegetation", "vegetation"],
699
+ labels: ["symbols", "symbols"]
700
+ };
701
+ var tileflowBandPlacements = tileflowLayerBands.flatMap(
702
+ (band) => [`below-${band}`, `above-${band}`]
703
+ );
704
+ function tileflowBandPlacementSlot(placement) {
705
+ const [side, band] = splitBandPlacement(placement);
706
+ return bandSlots[band][side === "below" ? 0 : 1];
854
707
  }
855
- function validateAttribution(value) {
856
- const normalized = value.trim();
857
- if (!normalized || normalized !== value || normalized.length > 2048) {
858
- throw new Error(
859
- "Tileflow marine attribution must be non-empty, trimmed, and at most 2048 characters."
860
- );
861
- }
862
- return normalized;
708
+ function tileflowBandPlacementIndex(slotRanks, placement) {
709
+ const [side, band] = splitBandPlacement(placement);
710
+ const [first, last] = bandSlots[band];
711
+ const threshold = side === "below" ? tileflowLayerSlots.indexOf(first) : tileflowLayerSlots.indexOf(last) + 1;
712
+ const index = slotRanks.findIndex((rank) => rank !== void 0 && rank >= threshold);
713
+ return index < 0 ? slotRanks.length : index;
863
714
  }
864
- function validateBathymetryDemUrl(value) {
865
- const url = validatePublicVectorUrl(value);
866
- if (url.startsWith("pmtiles://")) {
867
- throw new Error(
868
- "Tileflow bathymetry DEM TileJSON must use HTTPS, loopback HTTP, or a root-relative URL."
869
- );
870
- }
871
- return url;
715
+ function splitBandPlacement(placement) {
716
+ const separator = placement.indexOf("-");
717
+ return [
718
+ placement.slice(0, separator),
719
+ placement.slice(separator + 1)
720
+ ];
872
721
  }
873
- function normalizeApiBaseUrl2(value) {
874
- const url = new URL(value);
875
- if (url.username || url.password) {
876
- throw new Error("Tileflow API base URL must not contain user information.");
877
- }
878
- return url.toString();
722
+ var tileflowSemanticTargetPattern = /^[a-z][a-z0-9-]*(?:\.[A-Za-z0-9_-]+)*$/u;
723
+ var tileflowRenderStackOperationNamePattern = /^[a-z][A-Za-z0-9_-]*$/u;
724
+ var tileflowCompilerMetadataKeys = Object.freeze({
725
+ owner: "tileflow:compiler-owner",
726
+ slot: "tileflow:compiler-slot",
727
+ target: "tileflow:compiler-target"
728
+ });
729
+
730
+ // src/modules/addresses/index.ts
731
+ function addresses(options = {}) {
732
+ return { type: "addresses", ...cloneJson(options) };
879
733
  }
880
734
  function cloneJson(value) {
881
735
  return JSON.parse(JSON.stringify(value));
882
736
  }
883
737
 
738
+ // src/cartography/context.ts
739
+ function typographyTextStyle(typography) {
740
+ return {
741
+ font: typography.font,
742
+ ...typography.fallbacks ? { fallbacks: typography.fallbacks } : {},
743
+ ...typography.letterSpacing === void 0 ? {} : { letterSpacing: typography.letterSpacing },
744
+ ...typography.transform === void 0 ? {} : { transform: typography.transform }
745
+ };
746
+ }
747
+
884
748
  // src/cartography/layer-style.ts
885
749
  import { Color as Color2 } from "@maplibre/maplibre-gl-style-spec";
886
750
 
@@ -1351,494 +1215,194 @@ function applyIconStyle(layer, style) {
1351
1215
  setPaint(paint, "icon-color", style.color);
1352
1216
  setPaint(paint, "icon-halo-blur", style.haloBlur);
1353
1217
  setPaint(paint, "icon-halo-color", style.haloColor);
1354
- setPaint(paint, "icon-halo-width", style.haloWidth);
1355
- setPaint(paint, "icon-opacity", style.opacity);
1356
- return applyLayerRange(withLayerParts(layer, layout, paint), style);
1357
- }
1358
- function applySymbolPlacement(layer, style) {
1359
- const layout = { ...asRecord(layer.layout) };
1360
- if (style.placement !== void 0) layout["symbol-placement"] = style.placement;
1361
- if (style.priority !== void 0) {
1362
- layout["symbol-sort-key"] = resolvePriority(style.priority, style.priorityOrder);
1363
- }
1364
- setLayout(layout, "symbol-spacing", style.spacing);
1365
- if (style.zOrder !== void 0) layout["symbol-z-order"] = style.zOrder;
1366
- return applyLayerRange(withLayerParts(layer, layout), style);
1367
- }
1368
- function applySymbolStyle(layer, style) {
1369
- const range2 = resolveSymbolRange(style);
1370
- let result = applySymbolPlacement(layer, withoutRange(style));
1371
- if (style.text) result = applyTextStyle(result, withoutRange(style.text));
1372
- if (style.icon) result = applyIconStyle(result, withoutRange(style.icon));
1373
- return applyLayerRange(result, range2);
1374
- }
1375
- function createAreaLayers(base, style) {
1376
- const layers = [];
1377
- if (style.fill && style.fill.visible !== false) {
1378
- layers.push({
1379
- layer: applyFillStyle({ ...base, type: "fill" }, style.fill),
1380
- phase: "fill"
1381
- });
1382
- }
1383
- if (style.outline && style.outline.visible !== false) {
1384
- layers.push({
1385
- layer: applyLineStyle({ ...base, id: `${base.id}-outline`, type: "line" }, style.outline),
1386
- phase: "outline"
1387
- });
1388
- }
1389
- return layers;
1390
- }
1391
- function applyCircleStyle(layer, style) {
1392
- const paint = { ...asRecord(layer.paint) };
1393
- const layout = { ...asRecord(layer.layout) };
1394
- setPaint(paint, "circle-blur", style.blur);
1395
- setPaint(paint, "circle-color", style.color);
1396
- setPaint(paint, "circle-opacity", style.opacity);
1397
- if (style.pitchAlignment !== void 0) {
1398
- paint["circle-pitch-alignment"] = style.pitchAlignment;
1399
- }
1400
- if (style.pitchScale !== void 0) paint["circle-pitch-scale"] = style.pitchScale;
1401
- if (style.priority !== void 0) {
1402
- layout["circle-sort-key"] = resolvePriority(style.priority, style.priorityOrder);
1403
- }
1404
- setPaint(paint, "circle-radius", style.radius);
1405
- setPaint(paint, "circle-stroke-color", style.strokeColor);
1406
- setPaint(paint, "circle-stroke-opacity", style.strokeOpacity);
1407
- setPaint(paint, "circle-stroke-width", style.strokeWidth);
1408
- setPaint(paint, "circle-translate", style.translate);
1409
- if (style.translateAnchor !== void 0) {
1410
- paint["circle-translate-anchor"] = style.translateAnchor;
1411
- }
1412
- return applyLayerRange(withLayerParts(layer, layout, paint), style);
1413
- }
1414
- function applyExtrusionStyle(layer, style) {
1415
- const paint = { ...asRecord(layer.paint) };
1416
- setPaint(paint, "fill-extrusion-base", style.base);
1417
- setPaint(paint, "fill-extrusion-color", style.color);
1418
- setPaint(paint, "fill-extrusion-height", style.height);
1419
- setPaint(paint, "fill-extrusion-opacity", style.opacity);
1420
- setPaint(paint, "fill-extrusion-pattern", style.pattern);
1421
- if (style.verticalGradient !== void 0) {
1422
- paint["fill-extrusion-vertical-gradient"] = style.verticalGradient;
1423
- }
1424
- return applyLayerRange(withLayerParts(layer, void 0, paint), style);
1425
- }
1426
- function applyLayerRange(layer, range2) {
1427
- const layout = { ...asRecord(layer.layout) };
1428
- if (range2.visible !== void 0) layout.visibility = range2.visible ? "visible" : "none";
1429
- return {
1430
- ...layer,
1431
- ...range2.minZoom === void 0 ? {} : { minzoom: range2.minZoom },
1432
- ...range2.maxZoom === void 0 ? {} : { maxzoom: range2.maxZoom },
1433
- ...Object.keys(layout).length ? { layout } : {}
1434
- };
1435
- }
1436
- function withLayerParts(layer, layout, paint) {
1437
- return {
1438
- ...layer,
1439
- ...layout && Object.keys(layout).length ? { layout } : {},
1440
- ...paint && Object.keys(paint).length ? { paint } : {}
1441
- };
1442
- }
1443
- function resolveSymbolRange(style) {
1444
- const ranges = [style, style.text, style.icon].filter(
1445
- (value) => value !== void 0
1446
- );
1447
- const result = {};
1448
- for (const property of ["maxZoom", "minZoom", "visible"]) {
1449
- const values = ranges.flatMap(
1450
- (range2) => range2[property] === void 0 ? [] : [range2[property]]
1451
- );
1452
- if (new Set(values).size > 1) {
1453
- throw new Error(
1454
- `Tileflow symbol range conflict for ${property}; placement, text, and icon share one MapLibre layer.`
1455
- );
1456
- }
1457
- if (values.length > 0) result[property] = values[0];
1458
- }
1459
- return result;
1460
- }
1461
- function withoutRange(value) {
1462
- const { maxZoom: _maxZoom, minZoom: _minZoom, visible: _visible, ...rest } = value;
1463
- return rest;
1464
- }
1465
- function setOverlap(layout, part, style) {
1466
- if (style.overlap !== void 0) layout[`${part}-overlap`] = style.overlap;
1467
- else if (style.allowOverlap !== void 0) layout[`${part}-allow-overlap`] = style.allowOverlap;
1468
- if (layout[`${part}-overlap`] !== void 0) delete layout[`${part}-allow-overlap`];
1218
+ setPaint(paint, "icon-halo-width", style.haloWidth);
1219
+ setPaint(paint, "icon-opacity", style.opacity);
1220
+ return applyLayerRange(withLayerParts(layer, layout, paint), style);
1469
1221
  }
1470
- function withoutGovernedAllowOverlap(layout) {
1471
- const result = { ...layout };
1472
- for (const part of ["icon", "text"]) {
1473
- if (result[`${part}-overlap`] !== void 0) delete result[`${part}-allow-overlap`];
1222
+ function applySymbolPlacement(layer, style) {
1223
+ const layout = { ...asRecord(layer.layout) };
1224
+ if (style.placement !== void 0) layout["symbol-placement"] = style.placement;
1225
+ if (style.priority !== void 0) {
1226
+ layout["symbol-sort-key"] = resolvePriority(style.priority, style.priorityOrder);
1474
1227
  }
1475
- return result;
1476
- }
1477
- function setPaint(paint, property, value) {
1478
- if (value !== void 0) paint[property] = toMapLibreStyleValue(value);
1228
+ setLayout(layout, "symbol-spacing", style.spacing);
1229
+ if (style.zOrder !== void 0) layout["symbol-z-order"] = style.zOrder;
1230
+ return applyLayerRange(withLayerParts(layer, layout), style);
1479
1231
  }
1480
- function setLayout(layout, property, value) {
1481
- if (value !== void 0) layout[property] = toMapLibreStyleValue(value);
1232
+ function applySymbolStyle(layer, style) {
1233
+ const range2 = resolveSymbolRange(style);
1234
+ let result = applySymbolPlacement(layer, withoutRange(style));
1235
+ if (style.text) result = applyTextStyle(result, withoutRange(style.text));
1236
+ if (style.icon) result = applyIconStyle(result, withoutRange(style.icon));
1237
+ return applyLayerRange(result, range2);
1482
1238
  }
1483
- function invertPriority(value) {
1484
- const resolved = toMapLibreStyleValue(value);
1485
- if (typeof resolved === "number") return -resolved;
1486
- if (Array.isArray(resolved) && resolved[0] === "interpolate" && isZoomInput2(resolved[2])) {
1487
- return resolved.map(
1488
- (entry, index) => index >= 4 && index % 2 === 0 ? negateExpressionOutput(entry) : entry
1489
- );
1239
+ function createAreaLayers(base, style) {
1240
+ const layers = [];
1241
+ if (style.fill && style.fill.visible !== false) {
1242
+ layers.push({
1243
+ layer: applyFillStyle({ ...base, type: "fill" }, style.fill),
1244
+ phase: "fill"
1245
+ });
1490
1246
  }
1491
- if (Array.isArray(resolved) && resolved[0] === "step" && isZoomInput2(resolved[1])) {
1492
- return resolved.map(
1493
- (entry, index) => index === 2 || index >= 4 && index % 2 === 0 ? negateExpressionOutput(entry) : entry
1494
- );
1247
+ if (style.outline && style.outline.visible !== false) {
1248
+ layers.push({
1249
+ layer: applyLineStyle({ ...base, id: `${base.id}-outline`, type: "line" }, style.outline),
1250
+ phase: "outline"
1251
+ });
1495
1252
  }
1496
- return ["*", -1, resolved];
1497
- }
1498
- function resolvePriority(value, order) {
1499
- return order === "lower-first" ? toMapLibreStyleValue(value) : invertPriority(value);
1500
- }
1501
- function negateExpressionOutput(value) {
1502
- return typeof value === "number" ? -value : ["*", -1, value];
1503
- }
1504
- function isZoomInput2(value) {
1505
- return Array.isArray(value) && value.length === 1 && value[0] === "zoom";
1506
- }
1507
- function asRecord(value) {
1508
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1253
+ return layers;
1509
1254
  }
1510
-
1511
- // src/cartography/merge.ts
1512
- function mergeTileflowDesign(base, ...overlays) {
1513
- let resolved = cloneJson3(base);
1514
- for (const overlay of overlays) {
1515
- resolved = mergeValue(resolved, overlay);
1255
+ function applyCircleStyle(layer, style) {
1256
+ const paint = { ...asRecord(layer.paint) };
1257
+ const layout = { ...asRecord(layer.layout) };
1258
+ setPaint(paint, "circle-blur", style.blur);
1259
+ setPaint(paint, "circle-color", style.color);
1260
+ setPaint(paint, "circle-opacity", style.opacity);
1261
+ if (style.pitchAlignment !== void 0) {
1262
+ paint["circle-pitch-alignment"] = style.pitchAlignment;
1516
1263
  }
1517
- return resolved;
1518
- }
1519
- function mergeValue(base, overlay) {
1520
- if (overlay === void 0) return cloneJson3(base);
1521
- if (Array.isArray(overlay) || isTileflowExpression(overlay) || isTileflowThemeValueNode(overlay) || isTileflowZoomValue(overlay) || !isRecord2(overlay)) {
1522
- return cloneJson3(overlay);
1264
+ if (style.pitchScale !== void 0) paint["circle-pitch-scale"] = style.pitchScale;
1265
+ if (style.priority !== void 0) {
1266
+ layout["circle-sort-key"] = resolvePriority(style.priority, style.priorityOrder);
1523
1267
  }
1524
- const baseRecord = isRecord2(base) ? base : {};
1525
- const result = {};
1526
- for (const key of /* @__PURE__ */ new Set([...Object.keys(baseRecord), ...Object.keys(overlay)])) {
1527
- const overlayValue = overlay[key];
1528
- if (overlayValue === void 0) {
1529
- if (key in baseRecord) result[key] = cloneJson3(baseRecord[key]);
1530
- continue;
1531
- }
1532
- result[key] = mergeValue(baseRecord[key], overlayValue);
1268
+ setPaint(paint, "circle-radius", style.radius);
1269
+ setPaint(paint, "circle-stroke-color", style.strokeColor);
1270
+ setPaint(paint, "circle-stroke-opacity", style.strokeOpacity);
1271
+ setPaint(paint, "circle-stroke-width", style.strokeWidth);
1272
+ setPaint(paint, "circle-translate", style.translate);
1273
+ if (style.translateAnchor !== void 0) {
1274
+ paint["circle-translate-anchor"] = style.translateAnchor;
1533
1275
  }
1534
- return result;
1535
- }
1536
- function cloneJson3(value) {
1537
- if (value === void 0) return value;
1538
- return JSON.parse(JSON.stringify(value));
1539
- }
1540
- function isRecord2(value) {
1541
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1542
- }
1543
-
1544
- // src/trees-3d.ts
1545
- var tileflowWorldTreeFields = Object.freeze({
1546
- height: "height",
1547
- diameterCrown: "diameter_crown",
1548
- circumference: "circumference",
1549
- genus: "genus",
1550
- species: "species",
1551
- leafType: "leaf_type",
1552
- taxon: "taxon",
1553
- canopyRank: "rank",
1554
- canopyClearance: "clearance_px_z15",
1555
- canopyCover: "cover_pct",
1556
- canopyHeight: "height_m"
1557
- });
1558
- var tileflowDefaultUnknownTreeForms = Object.freeze({
1559
- round: 1,
1560
- open: 1,
1561
- tall: 1
1562
- });
1563
- var broadleaves = Object.freeze({
1564
- ...Object.fromEntries(
1565
- [
1566
- "acacia",
1567
- "albizia",
1568
- "arbutus",
1569
- "bursera",
1570
- "ceratonia",
1571
- "delonix",
1572
- "ficus",
1573
- "gleditsia",
1574
- "jacaranda",
1575
- "olea",
1576
- "quercus",
1577
- "robinia",
1578
- "schinus",
1579
- "sophora",
1580
- "styphnolobium",
1581
- "tamarix"
1582
- ].map((genus) => [genus, "open"])
1583
- ),
1584
- ...Object.fromEntries(
1585
- [
1586
- "betula",
1587
- "celtis",
1588
- "fraxinus",
1589
- "ginkgo",
1590
- "liquidambar",
1591
- "liriodendron",
1592
- "magnolia",
1593
- "platanus",
1594
- "populus",
1595
- "pyrus",
1596
- "ulmus",
1597
- "zelkova"
1598
- ].map((genus) => [genus, "tall"])
1599
- ),
1600
- ...Object.fromEntries(
1601
- [
1602
- "acer",
1603
- "aesculus",
1604
- "carpinus",
1605
- "castanea",
1606
- "catalpa",
1607
- "cercis",
1608
- "citrus",
1609
- "fagus",
1610
- "lagerstroemia",
1611
- "malus",
1612
- "melia",
1613
- "morus",
1614
- "prunus",
1615
- "sorbus",
1616
- "tilia"
1617
- ].map((genus) => [genus, "round"])
1618
- )
1619
- });
1620
- var tileflowTreeModelIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1621
- var tileflowVegetationMetadataKey = "tileflow:vegetation";
1622
- function tileflowVegetationMetadata(vegetation2) {
1623
- return { version: 1, ...vegetation2 };
1276
+ return applyLayerRange(withLayerParts(layer, layout, paint), style);
1624
1277
  }
1625
-
1626
- // src/modules/vegetation/compiler.ts
1627
- function compileVegetation(request, context) {
1628
- const minZoom = request?.flat?.minZoom ?? 16;
1629
- const mode = request?.mode ?? "3d";
1630
- const config = mergeTileflowDesign(
1631
- {
1632
- type: "vegetation",
1633
- enabled: true,
1634
- flat: {
1635
- color: context.colors.landcover.wood,
1636
- minZoom,
1637
- opacity: mode === "3d" ? 0.82 : 0.9,
1638
- pitchAlignment: "map",
1639
- pitchScale: "map",
1640
- radius: minZoom < 24 ? zoom.linear([
1641
- [minZoom, 2.2],
1642
- [minZoom < 20 ? 20 : 24, 8]
1643
- ]) : 2.2,
1644
- strokeColor: context.colors.park,
1645
- strokeOpacity: 0.55,
1646
- strokeWidth: 0.8
1647
- },
1648
- mode,
1649
- threeDimensional: {
1650
- barkColor: context.colors.vegetation.tree.bark,
1651
- broadleafColors: context.colors.vegetation.tree.broadleaf,
1652
- coniferColors: context.colors.vegetation.tree.conifer,
1653
- palmColors: context.colors.vegetation.tree.palm,
1654
- crownScale: 1,
1655
- heightScale: 1
1656
- }
1657
- },
1658
- request
1659
- );
1660
- if (config.enabled === false || !context.data.schema.layers.tree) return [];
1661
- const resolvedMode = config.mode ?? "3d";
1662
- return [
1663
- {
1664
- kind: "layer",
1665
- layer: applyCircleStyle(
1666
- {
1667
- id: "tileflow-vegetation-trees",
1668
- type: "circle",
1669
- source: context.data.sourceId,
1670
- "source-layer": context.data.schema.layers.tree,
1671
- // The layer also holds rows of trees as lines; a circle on each vertex would misdraw them.
1672
- filter: ["==", ["geometry-type"], "Point"],
1673
- metadata: {
1674
- "tileflow:vegetation-mode": resolvedMode,
1675
- ...resolvedMode === "3d" ? { "tileflow:vegetation-fallback": "flat-circle" } : {}
1676
- }
1677
- },
1678
- config.flat ?? {}
1679
- ),
1680
- localOrder: 1,
1681
- owner: "vegetation",
1682
- slot: "vegetation",
1683
- target: "vegetation.trees"
1684
- }
1685
- ];
1278
+ function applyExtrusionStyle(layer, style) {
1279
+ const paint = { ...asRecord(layer.paint) };
1280
+ setPaint(paint, "fill-extrusion-base", style.base);
1281
+ setPaint(paint, "fill-extrusion-color", style.color);
1282
+ setPaint(paint, "fill-extrusion-height", style.height);
1283
+ setPaint(paint, "fill-extrusion-opacity", style.opacity);
1284
+ setPaint(paint, "fill-extrusion-pattern", style.pattern);
1285
+ if (style.verticalGradient !== void 0) {
1286
+ paint["fill-extrusion-vertical-gradient"] = style.verticalGradient;
1287
+ }
1288
+ return applyLayerRange(withLayerParts(layer, void 0, paint), style);
1686
1289
  }
1687
- function tileflowVegetationDescription(request, context) {
1688
- const { schema } = context.data;
1689
- if (request?.enabled === false || request?.mode === "flat" || !schema.layers.tree)
1690
- return void 0;
1691
- const tree = context.colors.vegetation.tree;
1692
- const style = request?.threeDimensional ?? {};
1693
- const colours = (value, fallback) => Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string") ? value : [...fallback];
1694
- const number = (value) => {
1695
- const resolved = value === void 0 ? void 0 : toMapLibreStyleValue(value);
1696
- return typeof resolved === "number" && resolved > 0 ? resolved : 1;
1290
+ function applyLayerRange(layer, range2) {
1291
+ const layout = { ...asRecord(layer.layout) };
1292
+ if (range2.visible !== void 0) layout.visibility = range2.visible ? "visible" : "none";
1293
+ return {
1294
+ ...layer,
1295
+ ...range2.minZoom === void 0 ? {} : { minzoom: range2.minZoom },
1296
+ ...range2.maxZoom === void 0 ? {} : { maxzoom: range2.maxZoom },
1297
+ ...Object.keys(layout).length ? { layout } : {}
1697
1298
  };
1698
- const field2 = (name, fallback) => schema.fields[name] ?? fallback;
1299
+ }
1300
+ function withLayerParts(layer, layout, paint) {
1699
1301
  return {
1700
- source: context.data.sourceId,
1701
- layers: {
1702
- tree: schema.layers.tree,
1703
- ...schema.layers.canopy ? { canopy: schema.layers.canopy } : {}
1704
- },
1705
- fields: {
1706
- ...tileflowWorldTreeFields,
1707
- height: field2("height", tileflowWorldTreeFields.height),
1708
- diameterCrown: field2("diameterCrown", tileflowWorldTreeFields.diameterCrown),
1709
- genus: field2("genus", tileflowWorldTreeFields.genus),
1710
- species: field2("species", tileflowWorldTreeFields.species),
1711
- leafType: field2("leafType", tileflowWorldTreeFields.leafType),
1712
- canopyRank: field2("canopyRank", tileflowWorldTreeFields.canopyRank),
1713
- canopyClearance: field2("canopyClearanceAtZoom15", tileflowWorldTreeFields.canopyClearance),
1714
- canopyCover: field2("canopyCoverPercent", tileflowWorldTreeFields.canopyCover)
1715
- },
1716
- colors: {
1717
- bark: typeof style.barkColor === "string" ? style.barkColor : tree.bark,
1718
- broadleaf: colours(style.broadleafColors, tree.broadleaf),
1719
- conifer: colours(style.coniferColors, tree.conifer),
1720
- palm: colours(style.palmColors, tree.palm)
1721
- },
1722
- heightScale: number(style.heightScale),
1723
- crownScale: number(style.crownScale),
1724
- forms: Object.fromEntries(
1725
- Object.entries(style.forms ?? {}).map(([form, own]) => [
1726
- form,
1727
- {
1728
- heightScale: number(own?.heightScale),
1729
- crownScale: number(own?.crownScale),
1730
- ...own?.models?.length ? { models: [...own.models] } : {}
1731
- }
1732
- ])
1733
- ),
1734
- // Filled by the compiler from the map's prepared models.
1735
- models: {},
1736
- finish: style.finish ?? "faceted",
1737
- edges: (() => {
1738
- const edges = toMapLibreStyleValue(style.edges ?? 0);
1739
- return typeof edges === "number" ? edges : 0;
1740
- })(),
1741
- ...(() => {
1742
- const edgeColor = style.edgeColor === void 0 ? void 0 : toMapLibreStyleValue(style.edgeColor);
1743
- return typeof edgeColor === "string" ? { edgeColor } : {};
1744
- })(),
1745
- unknownForms: style.unknownForms ?? tileflowDefaultUnknownTreeForms,
1746
- regions: style.regions ?? [],
1747
- // Filled by the compiler once the style's layers are final.
1748
- flatLayers: []
1302
+ ...layer,
1303
+ ...layout && Object.keys(layout).length ? { layout } : {},
1304
+ ...paint && Object.keys(paint).length ? { paint } : {}
1749
1305
  };
1750
1306
  }
1751
-
1752
- // src/cartography/contributions.ts
1753
- var tileflowLayerSlots = [
1754
- "background",
1755
- "land",
1756
- "terrain",
1757
- "hydro",
1758
- "building-areas",
1759
- "transport-areas",
1760
- "transport-tunnel-shadow",
1761
- "transport-tunnel-casing",
1762
- "transport-tunnel-fill",
1763
- "transport-pedestrian-areas",
1764
- "aeroways",
1765
- "transport-surface-shadow",
1766
- "transport-surface-casing",
1767
- "transport-surface-fill",
1768
- "transport-bridge-shadow",
1769
- "transport-bridge-casing",
1770
- "transport-bridge-fill",
1771
- "transport-symbols",
1772
- "boundaries",
1773
- "buildings",
1774
- "vegetation",
1775
- "symbols"
1776
- ];
1777
- var tileflowLayerBands = [
1778
- "land",
1779
- "relief",
1780
- "water",
1781
- "roads",
1782
- "boundaries",
1783
- "buildings",
1784
- "vegetation",
1785
- "labels"
1786
- ];
1787
- var bandSlots = {
1788
- land: ["background", "land"],
1789
- relief: ["terrain", "terrain"],
1790
- water: ["hydro", "hydro"],
1791
- roads: ["transport-areas", "transport-symbols"],
1792
- boundaries: ["boundaries", "boundaries"],
1793
- buildings: ["buildings", "buildings"],
1794
- vegetation: ["vegetation", "vegetation"],
1795
- labels: ["symbols", "symbols"]
1796
- };
1797
- var tileflowBandPlacements = tileflowLayerBands.flatMap(
1798
- (band) => [`below-${band}`, `above-${band}`]
1799
- );
1800
- function tileflowBandPlacementSlot(placement) {
1801
- const [side, band] = splitBandPlacement(placement);
1802
- return bandSlots[band][side === "below" ? 0 : 1];
1307
+ function resolveSymbolRange(style) {
1308
+ const ranges = [style, style.text, style.icon].filter(
1309
+ (value) => value !== void 0
1310
+ );
1311
+ const result = {};
1312
+ for (const property of ["maxZoom", "minZoom", "visible"]) {
1313
+ const values = ranges.flatMap(
1314
+ (range2) => range2[property] === void 0 ? [] : [range2[property]]
1315
+ );
1316
+ if (new Set(values).size > 1) {
1317
+ throw new Error(
1318
+ `Tileflow symbol range conflict for ${property}; placement, text, and icon share one MapLibre layer.`
1319
+ );
1320
+ }
1321
+ if (values.length > 0) result[property] = values[0];
1322
+ }
1323
+ return result;
1803
1324
  }
1804
- function tileflowBandPlacementIndex(slotRanks, placement) {
1805
- const [side, band] = splitBandPlacement(placement);
1806
- const [first, last] = bandSlots[band];
1807
- const threshold = side === "below" ? tileflowLayerSlots.indexOf(first) : tileflowLayerSlots.indexOf(last) + 1;
1808
- const index = slotRanks.findIndex((rank) => rank !== void 0 && rank >= threshold);
1809
- return index < 0 ? slotRanks.length : index;
1325
+ function withoutRange(value) {
1326
+ const { maxZoom: _maxZoom, minZoom: _minZoom, visible: _visible, ...rest } = value;
1327
+ return rest;
1810
1328
  }
1811
- function splitBandPlacement(placement) {
1812
- const separator = placement.indexOf("-");
1813
- return [
1814
- placement.slice(0, separator),
1815
- placement.slice(separator + 1)
1816
- ];
1329
+ function setOverlap(layout, part, style) {
1330
+ if (style.overlap !== void 0) layout[`${part}-overlap`] = style.overlap;
1331
+ else if (style.allowOverlap !== void 0) layout[`${part}-allow-overlap`] = style.allowOverlap;
1332
+ if (layout[`${part}-overlap`] !== void 0) delete layout[`${part}-allow-overlap`];
1333
+ }
1334
+ function withoutGovernedAllowOverlap(layout) {
1335
+ const result = { ...layout };
1336
+ for (const part of ["icon", "text"]) {
1337
+ if (result[`${part}-overlap`] !== void 0) delete result[`${part}-allow-overlap`];
1338
+ }
1339
+ return result;
1340
+ }
1341
+ function setPaint(paint, property, value) {
1342
+ if (value !== void 0) paint[property] = toMapLibreStyleValue(value);
1343
+ }
1344
+ function setLayout(layout, property, value) {
1345
+ if (value !== void 0) layout[property] = toMapLibreStyleValue(value);
1346
+ }
1347
+ function invertPriority(value) {
1348
+ const resolved = toMapLibreStyleValue(value);
1349
+ if (typeof resolved === "number") return -resolved;
1350
+ if (Array.isArray(resolved) && resolved[0] === "interpolate" && isZoomInput2(resolved[2])) {
1351
+ return resolved.map(
1352
+ (entry, index) => index >= 4 && index % 2 === 0 ? negateExpressionOutput(entry) : entry
1353
+ );
1354
+ }
1355
+ if (Array.isArray(resolved) && resolved[0] === "step" && isZoomInput2(resolved[1])) {
1356
+ return resolved.map(
1357
+ (entry, index) => index === 2 || index >= 4 && index % 2 === 0 ? negateExpressionOutput(entry) : entry
1358
+ );
1359
+ }
1360
+ return ["*", -1, resolved];
1361
+ }
1362
+ function resolvePriority(value, order) {
1363
+ return order === "lower-first" ? toMapLibreStyleValue(value) : invertPriority(value);
1364
+ }
1365
+ function negateExpressionOutput(value) {
1366
+ return typeof value === "number" ? -value : ["*", -1, value];
1367
+ }
1368
+ function isZoomInput2(value) {
1369
+ return Array.isArray(value) && value.length === 1 && value[0] === "zoom";
1370
+ }
1371
+ function asRecord(value) {
1372
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1817
1373
  }
1818
- var tileflowSemanticTargetPattern = /^[a-z][a-z0-9-]*(?:\.[A-Za-z0-9_-]+)*$/u;
1819
- var tileflowRenderStackOperationNamePattern = /^[a-z][A-Za-z0-9_-]*$/u;
1820
- var tileflowCompilerMetadataKeys = Object.freeze({
1821
- owner: "tileflow:compiler-owner",
1822
- slot: "tileflow:compiler-slot",
1823
- target: "tileflow:compiler-target"
1824
- });
1825
1374
 
1826
- // src/modules/addresses/index.ts
1827
- function addresses(options = {}) {
1828
- return { type: "addresses", ...cloneJson4(options) };
1375
+ // src/cartography/merge.ts
1376
+ function mergeTileflowDesign(base, ...overlays) {
1377
+ let resolved = cloneJson3(base);
1378
+ for (const overlay of overlays) {
1379
+ resolved = mergeValue(resolved, overlay);
1380
+ }
1381
+ return resolved;
1829
1382
  }
1830
- function cloneJson4(value) {
1383
+ function mergeValue(base, overlay) {
1384
+ if (overlay === void 0) return cloneJson3(base);
1385
+ if (Array.isArray(overlay) || isTileflowExpression(overlay) || isTileflowThemeValueNode(overlay) || isTileflowZoomValue(overlay) || !isRecord2(overlay)) {
1386
+ return cloneJson3(overlay);
1387
+ }
1388
+ const baseRecord = isRecord2(base) ? base : {};
1389
+ const result = {};
1390
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(baseRecord), ...Object.keys(overlay)])) {
1391
+ const overlayValue = overlay[key];
1392
+ if (overlayValue === void 0) {
1393
+ if (key in baseRecord) result[key] = cloneJson3(baseRecord[key]);
1394
+ continue;
1395
+ }
1396
+ result[key] = mergeValue(baseRecord[key], overlayValue);
1397
+ }
1398
+ return result;
1399
+ }
1400
+ function cloneJson3(value) {
1401
+ if (value === void 0) return value;
1831
1402
  return JSON.parse(JSON.stringify(value));
1832
1403
  }
1833
-
1834
- // src/cartography/context.ts
1835
- function typographyTextStyle(typography) {
1836
- return {
1837
- font: typography.font,
1838
- ...typography.fallbacks ? { fallbacks: typography.fallbacks } : {},
1839
- ...typography.letterSpacing === void 0 ? {} : { letterSpacing: typography.letterSpacing },
1840
- ...typography.transform === void 0 ? {} : { transform: typography.transform }
1841
- };
1404
+ function isRecord2(value) {
1405
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1842
1406
  }
1843
1407
 
1844
1408
  // src/modules/addresses/compiler.ts
@@ -1893,9 +1457,9 @@ function compileAddresses(request, context) {
1893
1457
 
1894
1458
  // src/modules/aeroways/index.ts
1895
1459
  function aeroways(options = {}) {
1896
- return { type: "aeroways", ...cloneJson5(options) };
1460
+ return { type: "aeroways", ...cloneJson4(options) };
1897
1461
  }
1898
- function cloneJson5(value) {
1462
+ function cloneJson4(value) {
1899
1463
  return JSON.parse(JSON.stringify(value));
1900
1464
  }
1901
1465
 
@@ -2053,9 +1617,9 @@ function compileAeroways(request, context) {
2053
1617
 
2054
1618
  // src/modules/boundaries/index.ts
2055
1619
  function boundaries(options = {}) {
2056
- return { type: "boundaries", ...cloneJson6(options) };
1620
+ return { type: "boundaries", ...cloneJson5(options) };
2057
1621
  }
2058
- function cloneJson6(value) {
1622
+ function cloneJson5(value) {
2059
1623
  return JSON.parse(JSON.stringify(value));
2060
1624
  }
2061
1625
 
@@ -2169,9 +1733,9 @@ function compileBoundaries(request, context) {
2169
1733
 
2170
1734
  // src/modules/buildings/index.ts
2171
1735
  function buildings(options = {}) {
2172
- return { type: "buildings", ...cloneJson7(options) };
1736
+ return { type: "buildings", ...cloneJson6(options) };
2173
1737
  }
2174
- function cloneJson7(value) {
1738
+ function cloneJson6(value) {
2175
1739
  return JSON.parse(JSON.stringify(value));
2176
1740
  }
2177
1741
 
@@ -2448,6 +2012,7 @@ var defaults2 = {
2448
2012
  hierarchy: "clear",
2449
2013
  oneWayMarkers: false,
2450
2014
  outline: "subtle",
2015
+ rampOrder: "below-roads",
2451
2016
  weight: "regular"
2452
2017
  };
2453
2018
  var defaultRoadWidthScale = {
@@ -2492,7 +2057,7 @@ function visibleRoadClasses(options) {
2492
2057
  ].filter((roadClass) => !disabled.has(roadClass));
2493
2058
  }
2494
2059
  function roads(options = {}) {
2495
- return { type: "roads", ...cloneJson8(options) };
2060
+ return { type: "roads", ...cloneJson7(options) };
2496
2061
  }
2497
2062
  function resolveRoads(request) {
2498
2063
  return {
@@ -2504,6 +2069,7 @@ function resolveRoads(request) {
2504
2069
  hierarchy: request?.hierarchy ?? defaults2.hierarchy,
2505
2070
  oneWayMarkers: request?.oneWayMarkers ?? defaults2.oneWayMarkers,
2506
2071
  outline: request?.outline ?? defaults2.outline,
2072
+ rampOrder: request?.rampOrder ?? defaults2.rampOrder,
2507
2073
  weight: request?.weight ?? defaults2.weight,
2508
2074
  ...request?.widthScale ? {
2509
2075
  widthScale: {
@@ -2540,7 +2106,7 @@ function roadStyleMetrics(options) {
2540
2106
  weightScale: { thin: 0.78, regular: 1, bold: 1.24 }[options.weight]
2541
2107
  };
2542
2108
  }
2543
- function cloneJson8(value) {
2109
+ function cloneJson7(value) {
2544
2110
  return JSON.parse(JSON.stringify(value));
2545
2111
  }
2546
2112
 
@@ -2551,12 +2117,13 @@ var defaults3 = {
2551
2117
  language: "auto",
2552
2118
  junctions: true,
2553
2119
  places: "major",
2120
+ roadPlacement: "foreground",
2554
2121
  roads: "major",
2555
2122
  shields: "major",
2556
2123
  water: "major"
2557
2124
  };
2558
2125
  function labels(options = {}) {
2559
- return { type: "labels", ...cloneJson9(options) };
2126
+ return { type: "labels", ...cloneJson8(options) };
2560
2127
  }
2561
2128
  function resolveLabels(request) {
2562
2129
  return {
@@ -2566,6 +2133,7 @@ function resolveLabels(request) {
2566
2133
  junctions: request?.junctions ?? defaults3.junctions,
2567
2134
  places: request?.places ?? defaults3.places,
2568
2135
  ...request?.roadClasses ? { roadClasses: [...request.roadClasses] } : {},
2136
+ roadPlacement: request?.roadPlacement ?? defaults3.roadPlacement,
2569
2137
  roads: request?.roads ?? defaults3.roads,
2570
2138
  shields: request?.shields ?? defaults3.shields,
2571
2139
  water: request?.water ?? defaults3.water
@@ -2577,7 +2145,7 @@ function visibleRoadLabelClasses(detail, roads2, explicitClasses) {
2577
2145
  const eligible = explicit ? tileflowRoadClasses.filter((roadClass) => explicit.has(roadClass)) : roadClassesWithPaths(detail, detail === "all" && roads2.extras.paths);
2578
2146
  return eligible.filter((roadClass) => visibleGeometry.has(roadClass));
2579
2147
  }
2580
- function cloneJson9(value) {
2148
+ function cloneJson8(value) {
2581
2149
  return JSON.parse(JSON.stringify(value));
2582
2150
  }
2583
2151
 
@@ -2634,6 +2202,7 @@ var placeLabelOrder = [
2634
2202
  function compileLabels(request, roadRequest, context) {
2635
2203
  if (request?.enabled === false) return [];
2636
2204
  const semantics = resolveLabels(request);
2205
+ const groundRoadNames = semantics.roadPlacement === "ground";
2637
2206
  const roads2 = resolveRoads(roadRequest);
2638
2207
  const { colors } = context;
2639
2208
  const rankField = context.data.schema.fields.rank;
@@ -2722,6 +2291,7 @@ function compileLabels(request, roadRequest, context) {
2722
2291
  color: colors.labels.road,
2723
2292
  minZoom: ["motorway", "trunk", "primary"].includes(roadClass) ? 10 : ["pathway", "footway", "cycleway", "steps", "pedestrian"].includes(roadClass) ? 15 : 13,
2724
2293
  placement: "line",
2294
+ ...groundRoadNames ? { pitchAlignment: "map", rotationAlignment: "map" } : {},
2725
2295
  size: zoom.linear([
2726
2296
  [10, 10],
2727
2297
  [16, 14]
@@ -2840,7 +2410,7 @@ function compileLabels(request, roadRequest, context) {
2840
2410
  result.push(
2841
2411
  symbolContribution(
2842
2412
  `labels.roads.${roadClass}`,
2843
- (navigationPriority ? 1200 : 200) + order++,
2413
+ (groundRoadNames ? -1e3 : navigationPriority ? 1200 : 200) + order++,
2844
2414
  applySymbolStyle(
2845
2415
  {
2846
2416
  id: id2,
@@ -2855,7 +2425,7 @@ function compileLabels(request, roadRequest, context) {
2855
2425
  },
2856
2426
  mergeTileflowDesign(style, { text: { field: field2 } })
2857
2427
  ),
2858
- "symbols",
2428
+ groundRoadNames ? "buildings" : "symbols",
2859
2429
  { group: "roads", kind: "road-label", member: roadClass }
2860
2430
  )
2861
2431
  );
@@ -3248,13 +2818,18 @@ function symbolContribution(target, localOrder, layer, slot = "symbols", family)
3248
2818
 
3249
2819
  // src/modules/land/index.ts
3250
2820
  function land(options = {}) {
3251
- return { type: "land", ...cloneJson10(options) };
2821
+ return { type: "land", ...cloneJson9(options) };
3252
2822
  }
3253
- function cloneJson10(value) {
2823
+ function cloneJson9(value) {
3254
2824
  return JSON.parse(JSON.stringify(value));
3255
2825
  }
3256
2826
 
3257
2827
  // src/modules/land/compiler.ts
2828
+ var barrierClasses = [
2829
+ "fence",
2830
+ "wall",
2831
+ "hedge"
2832
+ ];
3258
2833
  var landuseClasses = {
3259
2834
  cemetery: ["cemetery"],
3260
2835
  civic: ["community_centre", "place_of_worship", "social_facility"],
@@ -3281,20 +2856,26 @@ var typedGrassSubclasses = [
3281
2856
  var protectedLandcoverMatch = { class: ["protected_area"] };
3282
2857
  var landcoverClasses = [
3283
2858
  ["farmland", { class: ["farmland"] }],
3284
- ["grass", { class: ["grass"], excludeSubclass: typedGrassSubclasses }],
3285
2859
  ["ice", { class: ["ice", "glacier"] }],
3286
- ["meadow", { class: ["grass"], subclass: ["meadow"] }],
3287
2860
  ["rock", { class: ["rock"] }],
3288
2861
  ["sand", { class: ["sand", "beach"] }],
3289
- ["scrub", { class: ["grass"], subclass: ["scrub"] }],
3290
2862
  ["wetland", { class: ["wetland"] }],
3291
2863
  ["wood", { class: ["wood", "forest"] }],
3292
2864
  ["protected", protectedLandcoverMatch],
3293
2865
  ["urbanPark", { class: ["grass"], subclass: ["park", "garden"] }],
3294
2866
  ["recreationGround", { class: ["grass"], subclass: ["recreation_ground"] }],
3295
2867
  ["villageGreen", { class: ["grass"], subclass: ["village_green"] }],
2868
+ ["grass", { class: ["grass"], excludeSubclass: typedGrassSubclasses }],
2869
+ ["meadow", { class: ["grass"], subclass: ["meadow"] }],
2870
+ ["scrub", { class: ["grass"], subclass: ["scrub"] }],
3296
2871
  ["flowerbed", { class: ["grass"], subclass: ["flowerbed"] }]
3297
2872
  ];
2873
+ var detailedLandcover = /* @__PURE__ */ new Set([
2874
+ "grass",
2875
+ "meadow",
2876
+ "scrub",
2877
+ "flowerbed"
2878
+ ]);
3298
2879
  function compileLand(request, context) {
3299
2880
  const colors = context.colors;
3300
2881
  const schema = context.data.schema;
@@ -3442,12 +3023,12 @@ function compileLand(request, context) {
3442
3023
  }
3443
3024
  };
3444
3025
  for (const [name, classes] of Object.entries(landuseClasses)) {
3445
- if (name === "recreation") continue;
3026
+ if (name === "recreation" || name === "parking") continue;
3446
3027
  appendLanduse(name, classes);
3447
3028
  }
3448
- for (const [name, match3] of landcoverClasses) {
3029
+ const appendLandcover = (name, match3) => {
3449
3030
  const style = config.landcover?.[name];
3450
- if (!style) continue;
3031
+ if (!style) return;
3451
3032
  const protectedSource = name === "protected";
3452
3033
  const sourceLayer = protectedSource ? schema.layers.park : schema.layers.landcover;
3453
3034
  const filter = protectedSource && schema.semantics.parkLayer === "protected-only" ? void 0 : landcoverFilter(classField, subclassField, match3);
@@ -3478,6 +3059,9 @@ function compileLand(request, context) {
3478
3059
  target: `land.landcover.${name}.${area.phase}`
3479
3060
  });
3480
3061
  }
3062
+ };
3063
+ for (const [name, match3] of landcoverClasses) {
3064
+ if (!detailedLandcover.has(name)) appendLandcover(name, match3);
3481
3065
  }
3482
3066
  if (schema.semantics.parkLayer === "mixed" && config.landcover?.urbanPark) {
3483
3067
  for (const area of createAreaLayers(
@@ -3500,7 +3084,56 @@ function compileLand(request, context) {
3500
3084
  });
3501
3085
  }
3502
3086
  }
3087
+ for (const [name, match3] of landcoverClasses) {
3088
+ if (detailedLandcover.has(name)) appendLandcover(name, match3);
3089
+ }
3090
+ appendLanduse("parking", landuseClasses.parking);
3503
3091
  appendLanduse("recreation", landuseClasses.recreation);
3092
+ const barrierLayer = schema.layers.trafficControl;
3093
+ for (const name of barrierLayer ? barrierClasses : []) {
3094
+ const style = config.barriers?.[name];
3095
+ const kind = ["==", ["get", subclassField], name];
3096
+ if (style?.area) {
3097
+ contributions.push({
3098
+ kind: "layer",
3099
+ layer: applyFillStyle(
3100
+ {
3101
+ id: `tileflow-barrier-${name}-area`,
3102
+ type: "fill",
3103
+ source,
3104
+ "source-layer": barrierLayer,
3105
+ filter: ["all", ["==", ["geometry-type"], "Polygon"], kind]
3106
+ },
3107
+ style.area
3108
+ ),
3109
+ localOrder: localOrder++,
3110
+ owner: "land",
3111
+ slot: "land",
3112
+ target: `land.barriers.${name}.area`
3113
+ });
3114
+ }
3115
+ for (const phase of ["shadow", "line"]) {
3116
+ const line = style?.[phase];
3117
+ if (!line) continue;
3118
+ contributions.push({
3119
+ kind: "layer",
3120
+ layer: applyLineStyle(
3121
+ {
3122
+ id: `tileflow-barrier-${name}${phase === "shadow" ? "-shadow" : ""}`,
3123
+ type: "line",
3124
+ source,
3125
+ "source-layer": barrierLayer,
3126
+ filter: kind
3127
+ },
3128
+ line
3129
+ ),
3130
+ localOrder: localOrder++,
3131
+ owner: "land",
3132
+ slot: "land",
3133
+ target: `land.barriers.${name}.${phase}`
3134
+ });
3135
+ }
3136
+ }
3504
3137
  return contributions;
3505
3138
  }
3506
3139
  function classFilter2(field2, classes) {
@@ -3525,9 +3158,9 @@ var tileflowLandformClasses = [
3525
3158
  "arete"
3526
3159
  ];
3527
3160
  function landforms(options = {}) {
3528
- return { type: "landforms", ...cloneJson11(options) };
3161
+ return { type: "landforms", ...cloneJson10(options) };
3529
3162
  }
3530
- function cloneJson11(value) {
3163
+ function cloneJson10(value) {
3531
3164
  return JSON.parse(JSON.stringify(value));
3532
3165
  }
3533
3166
 
@@ -3618,44 +3251,275 @@ function compileLandforms(request, language, context) {
3618
3251
  ];
3619
3252
  });
3620
3253
  }
3621
- function landformFilter(fields, classes) {
3622
- const classFilter4 = classes.length === 1 ? ["==", ["get", fields.class], classes[0]] : ["match", ["get", fields.class], classes, true, false];
3623
- const classZoom = [
3624
- "match",
3625
- ["get", fields.class],
3626
- "volcano",
3627
- [">=", ["zoom"], 9],
3628
- "peak",
3629
- [">=", ["zoom"], 10],
3630
- "saddle",
3631
- [">=", ["zoom"], 13],
3632
- ["ridge", "cliff", "arete"],
3633
- [">=", ["zoom"], 14],
3634
- false
3635
- ];
3636
- return [
3637
- "all",
3638
- ["==", ["geometry-type"], "Point"],
3639
- ["has", fields.name],
3640
- classFilter4,
3641
- classZoom,
3642
- ["<=", ["to-number", ["get", fields.rank], 99], ["step", ["zoom"], 3, 12, 10, 14, 99]]
3643
- ];
3254
+ function landformFilter(fields, classes) {
3255
+ const classFilter4 = classes.length === 1 ? ["==", ["get", fields.class], classes[0]] : ["match", ["get", fields.class], classes, true, false];
3256
+ const classZoom = [
3257
+ "match",
3258
+ ["get", fields.class],
3259
+ "volcano",
3260
+ [">=", ["zoom"], 9],
3261
+ "peak",
3262
+ [">=", ["zoom"], 10],
3263
+ "saddle",
3264
+ [">=", ["zoom"], 13],
3265
+ ["ridge", "cliff", "arete"],
3266
+ [">=", ["zoom"], 14],
3267
+ false
3268
+ ];
3269
+ return [
3270
+ "all",
3271
+ ["==", ["geometry-type"], "Point"],
3272
+ ["has", fields.name],
3273
+ classFilter4,
3274
+ classZoom,
3275
+ ["<=", ["to-number", ["get", fields.rank], 99], ["step", ["zoom"], 3, 12, 10, 14, 99]]
3276
+ ];
3277
+ }
3278
+ function landformContribution(target, localOrder, layer) {
3279
+ return {
3280
+ kind: "layer",
3281
+ layer,
3282
+ localOrder,
3283
+ owner: "landforms",
3284
+ slot: "symbols",
3285
+ target
3286
+ };
3287
+ }
3288
+
3289
+ // src/modules/nautical/index.ts
3290
+ function nautical(options = {}) {
3291
+ return { type: "nautical", ...cloneJson11(options) };
3292
+ }
3293
+ function cloneJson11(value) {
3294
+ return JSON.parse(JSON.stringify(value));
3295
+ }
3296
+
3297
+ // src/marine/index.ts
3298
+ var tileflowBathymetrySourceId = "tileflow-bathymetry";
3299
+ var tileflowBathymetryDemSourceId = "tileflow-bathymetry-dem";
3300
+ var tileflowNauticalSourceId = "tileflow-nautical";
3301
+ var tileflowBathymetryV1Schema = Object.freeze({
3302
+ fields: Object.freeze({
3303
+ class: "class",
3304
+ depth: "depth",
3305
+ id: "id",
3306
+ maxZoom: "max_zoom",
3307
+ minDepth: "min_depth",
3308
+ minZoom: "min_zoom",
3309
+ name: "name",
3310
+ sortKey: "sort_key"
3311
+ }),
3312
+ layer: "bathymetry",
3313
+ layers: Object.freeze({
3314
+ bands: "bathymetry",
3315
+ contours: "bathymetry_contour",
3316
+ coverage: "bathymetry_coverage",
3317
+ landforms: "seafloor_landform",
3318
+ waterNames: "water_name"
3319
+ }),
3320
+ optionalLayers: Object.freeze(["contours", "coverage", "landforms", "waterNames"]),
3321
+ product: "bathymetry-v1",
3322
+ requiredLayers: Object.freeze(["bands"]),
3323
+ schemaVersion: 1,
3324
+ sources: Object.freeze({
3325
+ dem: Object.freeze({
3326
+ encoding: "terrarium",
3327
+ sourceId: tileflowBathymetryDemSourceId,
3328
+ tileSize: 512
3329
+ }),
3330
+ vector: Object.freeze({ sourceId: tileflowBathymetrySourceId })
3331
+ })
3332
+ });
3333
+ var tileflowNauticalV1Schema = Object.freeze({
3334
+ fields: Object.freeze({
3335
+ cell: "cell",
3336
+ character: "character",
3337
+ class: "class",
3338
+ color: "color",
3339
+ coverage: "coverage",
3340
+ depth: "depth",
3341
+ direction: "direction",
3342
+ edition: "edition",
3343
+ licence: "licence",
3344
+ name: "name",
3345
+ provider: "provider",
3346
+ provenance: "provenance",
3347
+ quality: "quality",
3348
+ rangeNm: "range_nm",
3349
+ scale: "scale",
3350
+ subclass: "subclass",
3351
+ update: "update"
3352
+ }),
3353
+ geometryTypes: Object.freeze({
3354
+ aids: Object.freeze(["Point"]),
3355
+ coverage: Object.freeze(["Polygon"]),
3356
+ hazards: Object.freeze(["Point", "Polygon"]),
3357
+ lights: Object.freeze(["Point"]),
3358
+ navigationAreas: Object.freeze(["Polygon"]),
3359
+ reefs: Object.freeze(["Polygon"]),
3360
+ soundings: Object.freeze(["Point"]),
3361
+ wrecks: Object.freeze(["Point", "Polygon"])
3362
+ }),
3363
+ layers: Object.freeze({
3364
+ aids: "aid",
3365
+ coverage: "coverage",
3366
+ hazards: "hazard",
3367
+ lights: "light",
3368
+ navigationAreas: "navigation_area",
3369
+ reefs: "reef",
3370
+ soundings: "sounding",
3371
+ wrecks: "wreck"
3372
+ }),
3373
+ product: "nautical-v1",
3374
+ schemaVersion: 1
3375
+ });
3376
+ var tileflowBathymetryNameLayerLimits = Object.freeze({ maxZoomRanges: 128 });
3377
+ function resolveMarine(marine, apiBaseUrl = "https://api.tileflow.dev") {
3378
+ if (marine === void 0) return void 0;
3379
+ if (typeof marine === "string") {
3380
+ switch (marine) {
3381
+ case "none":
3382
+ return {};
3383
+ case "bathymetry":
3384
+ return { bathymetry: resolveBathymetry({}, apiBaseUrl) };
3385
+ case "nautical":
3386
+ return { nautical: resolveNauticalSource({}, apiBaseUrl) };
3387
+ case "chart":
3388
+ return {
3389
+ bathymetry: resolveBathymetry({}, apiBaseUrl),
3390
+ nautical: resolveNauticalSource({}, apiBaseUrl)
3391
+ };
3392
+ }
3393
+ }
3394
+ return {
3395
+ ...marine.bathymetry === false ? {} : { bathymetry: resolveBathymetry(marine.bathymetry ?? {}, apiBaseUrl) },
3396
+ ...marine.nautical === false ? {} : { nautical: resolveNauticalSource(marine.nautical ?? {}, apiBaseUrl) }
3397
+ };
3398
+ }
3399
+ function resolveBathymetry(input, apiBaseUrl) {
3400
+ const advanced = "type" in input && input.type === "bathymetry" ? input : void 0;
3401
+ const display = advanced?.display ?? "bands";
3402
+ const usesVector = display === "bands" || display === "hybrid";
3403
+ const usesRelief = display === "relief" || display === "hybrid";
3404
+ const vectorConfig = {
3405
+ ...input.attribution === void 0 ? {} : { attribution: input.attribution },
3406
+ ...input.sourceId === void 0 ? {} : { sourceId: input.sourceId },
3407
+ ...input.url === void 0 ? {} : { url: input.url }
3408
+ };
3409
+ const reliefConfig = advanced?.relief === false ? void 0 : advanced?.relief;
3410
+ const reliefVisible = usesRelief && reliefConfig?.visible !== false && !(typeof reliefConfig?.opacity === "number" && reliefConfig.opacity === 0);
3411
+ return {
3412
+ ...advanced?.bands === void 0 ? {} : { bands: advanced.bands },
3413
+ ...advanced?.contours === void 0 ? {} : { contours: advanced.contours },
3414
+ display,
3415
+ ...advanced?.labels === void 0 ? {} : { labels: advanced.labels },
3416
+ ...advanced?.vectorLayers === void 0 ? {} : { vectorLayers: cloneJson12(advanced.vectorLayers) },
3417
+ ...reliefVisible ? { relief: resolveBathymetryRelief(reliefConfig ?? {}, apiBaseUrl) } : {},
3418
+ ...usesVector ? {
3419
+ vector: resolveMarineSource({
3420
+ config: vectorConfig,
3421
+ defaultPath: "/tiles/bathymetry/tiles.json",
3422
+ defaultSourceId: tileflowBathymetrySourceId,
3423
+ kind: "tileflow-bathymetry",
3424
+ product: tileflowBathymetryV1Schema.product,
3425
+ apiBaseUrl
3426
+ })
3427
+ } : {}
3428
+ };
3429
+ }
3430
+ function resolveBathymetryRelief(style, apiBaseUrl) {
3431
+ const sourceId = validateSourceId(style.sourceId ?? tileflowBathymetryDemSourceId);
3432
+ const url = validateBathymetryDemUrl(
3433
+ style.url ?? new URL("/tiles/bathymetry/dem/tiles.json", normalizeApiBaseUrl2(apiBaseUrl)).toString()
3434
+ );
3435
+ const encoding = style.encoding ?? "terrarium";
3436
+ const tileSize = style.tileSize ?? 512;
3437
+ return {
3438
+ identity: {
3439
+ encoding,
3440
+ kind: "tileflow-bathymetry-dem",
3441
+ product: tileflowBathymetryV1Schema.product,
3442
+ schemaVersion: 1,
3443
+ sourceId,
3444
+ tileSize,
3445
+ url
3446
+ },
3447
+ source: {
3448
+ ...style.attribution ? { attribution: validateAttribution(style.attribution) } : {},
3449
+ encoding,
3450
+ tileSize,
3451
+ type: "raster-dem",
3452
+ url
3453
+ },
3454
+ sourceId,
3455
+ style: cloneJson12(style)
3456
+ };
3457
+ }
3458
+ function resolveNauticalSource(config, apiBaseUrl) {
3459
+ return resolveMarineSource({
3460
+ config,
3461
+ defaultPath: "/tiles/nautical/tiles.json",
3462
+ defaultSourceId: tileflowNauticalSourceId,
3463
+ kind: "tileflow-nautical",
3464
+ product: tileflowNauticalV1Schema.product,
3465
+ apiBaseUrl
3466
+ });
3644
3467
  }
3645
- function landformContribution(target, localOrder, layer) {
3468
+ function resolveMarineSource(options) {
3469
+ const sourceId = validateSourceId(options.config.sourceId ?? options.defaultSourceId);
3470
+ const url = validatePublicVectorUrl(
3471
+ options.config.url ?? new URL(options.defaultPath, normalizeApiBaseUrl2(options.apiBaseUrl)).toString()
3472
+ );
3473
+ const identity = {
3474
+ kind: options.kind,
3475
+ product: options.product,
3476
+ schemaVersion: 1,
3477
+ sourceId,
3478
+ url
3479
+ };
3646
3480
  return {
3647
- kind: "layer",
3648
- layer,
3649
- localOrder,
3650
- owner: "landforms",
3651
- slot: "symbols",
3652
- target
3481
+ identity,
3482
+ source: {
3483
+ ...options.config.attribution ? { attribution: validateAttribution(options.config.attribution) } : {},
3484
+ type: "vector",
3485
+ url
3486
+ },
3487
+ sourceId
3653
3488
  };
3654
3489
  }
3655
-
3656
- // src/modules/nautical/index.ts
3657
- function nautical(options = {}) {
3658
- return { type: "nautical", ...cloneJson12(options) };
3490
+ function validateSourceId(value) {
3491
+ const normalized = value.trim();
3492
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u.test(normalized) || normalized !== value) {
3493
+ throw new Error(
3494
+ "Tileflow marine sourceId must be a portable identifier of at most 64 characters."
3495
+ );
3496
+ }
3497
+ return normalized;
3498
+ }
3499
+ function validateAttribution(value) {
3500
+ const normalized = value.trim();
3501
+ if (!normalized || normalized !== value || normalized.length > 2048) {
3502
+ throw new Error(
3503
+ "Tileflow marine attribution must be non-empty, trimmed, and at most 2048 characters."
3504
+ );
3505
+ }
3506
+ return normalized;
3507
+ }
3508
+ function validateBathymetryDemUrl(value) {
3509
+ const url = validatePublicVectorUrl(value);
3510
+ if (url.startsWith("pmtiles://")) {
3511
+ throw new Error(
3512
+ "Tileflow bathymetry DEM TileJSON must use HTTPS, loopback HTTP, or a root-relative URL."
3513
+ );
3514
+ }
3515
+ return url;
3516
+ }
3517
+ function normalizeApiBaseUrl2(value) {
3518
+ const url = new URL(value);
3519
+ if (url.username || url.password) {
3520
+ throw new Error("Tileflow API base URL must not contain user information.");
3521
+ }
3522
+ return url.toString();
3659
3523
  }
3660
3524
  function cloneJson12(value) {
3661
3525
  return JSON.parse(JSON.stringify(value));
@@ -4846,6 +4710,29 @@ function withoutVisibility(value) {
4846
4710
  return rest;
4847
4711
  }
4848
4712
 
4713
+ // src/maps/operations.ts
4714
+ var resetValue = deepFreeze({ $tileflow: "reset" });
4715
+ var disableValue = deepFreeze({ op: "disable" });
4716
+ function isTileflowReset(value) {
4717
+ if (!isPlainRecord(value)) return false;
4718
+ return value.$tileflow === "reset" && Object.keys(value).length === 1;
4719
+ }
4720
+ function isTileflowModuleOperation(value) {
4721
+ if (!isPlainRecord(value) || typeof value.op !== "string") return false;
4722
+ return value.op === "disable" || value.op === "refine";
4723
+ }
4724
+ function deepFreeze(value) {
4725
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
4726
+ Object.freeze(value);
4727
+ for (const child of Object.values(value)) deepFreeze(child);
4728
+ return value;
4729
+ }
4730
+ function isPlainRecord(value) {
4731
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
4732
+ const prototype = Object.getPrototypeOf(value);
4733
+ return prototype === Object.prototype || prototype === null;
4734
+ }
4735
+
4849
4736
  // src/portable-identity-rules.ts
4850
4737
  var tileflowPortableIdMaximumLength = 64;
4851
4738
  var tileflowPortableIdPattern = /^[a-z][a-z0-9-]{0,63}$/u;
@@ -4856,6 +4743,38 @@ function isTileflowThemeName(value) {
4856
4743
  return isTileflowPortableId(value) && value !== "system";
4857
4744
  }
4858
4745
 
4746
+ // src/sun.ts
4747
+ var tileflowSunMetadataKey = "tileflow:sun";
4748
+ var tileflowDefaultSunPreset = Object.freeze({
4749
+ azimuth: 210,
4750
+ altitude: 60,
4751
+ color: "#ffffff",
4752
+ intensity: 0.2,
4753
+ shadowIntensity: 1,
4754
+ ambient: Object.freeze({ color: "#ffffff", intensity: 0.8 })
4755
+ });
4756
+ var degrees = Math.PI / 180;
4757
+ function normalizeTileflowAzimuth(azimuth) {
4758
+ const turned = azimuth % 360;
4759
+ return turned < 0 ? turned + 360 : turned === 0 ? 0 : turned;
4760
+ }
4761
+ function tileflowSunMetadata(preset) {
4762
+ return Object.freeze({ version: 1, ...preset });
4763
+ }
4764
+ function tileflowSunLight(direction, base) {
4765
+ const radial = base?.position?.[0] ?? 1.15;
4766
+ return {
4767
+ ...base ?? {},
4768
+ anchor: "map",
4769
+ position: [
4770
+ radial,
4771
+ Number(normalizeTileflowAzimuth(direction.azimuth).toFixed(3)),
4772
+ // MapLibre lights the extrusions from above the horizon only.
4773
+ Number(Math.min(90, Math.max(0, 90 - direction.altitude)).toFixed(3))
4774
+ ]
4775
+ };
4776
+ }
4777
+
4859
4778
  // src/time-of-day.ts
4860
4779
  var tileflowTimesOfDay = ["day", "dawn", "dusk", "night"];
4861
4780
  function isTileflowTimeOfDay(value) {
@@ -4906,38 +4825,6 @@ function resolveTileflowTimeOfDayTheme(variants, theme, timeOfDay) {
4906
4825
  return result;
4907
4826
  }
4908
4827
 
4909
- // src/sun.ts
4910
- var tileflowSunMetadataKey = "tileflow:sun";
4911
- var tileflowDefaultSunPreset = Object.freeze({
4912
- azimuth: 210,
4913
- altitude: 60,
4914
- color: "#ffffff",
4915
- intensity: 0.5,
4916
- shadowIntensity: 1,
4917
- ambient: Object.freeze({ color: "#ffffff", intensity: 0.5 })
4918
- });
4919
- var degrees = Math.PI / 180;
4920
- function normalizeTileflowAzimuth(azimuth) {
4921
- const turned = azimuth % 360;
4922
- return turned < 0 ? turned + 360 : turned === 0 ? 0 : turned;
4923
- }
4924
- function tileflowSunMetadata(preset) {
4925
- return Object.freeze({ version: 1, ...preset });
4926
- }
4927
- function tileflowSunLight(direction, base) {
4928
- const radial = base?.position?.[0] ?? 1.15;
4929
- return {
4930
- ...base ?? {},
4931
- anchor: "map",
4932
- position: [
4933
- radial,
4934
- Number(normalizeTileflowAzimuth(direction.azimuth).toFixed(3)),
4935
- // MapLibre lights the extrusions from above the horizon only.
4936
- Number(Math.min(90, Math.max(0, 90 - direction.altitude)).toFixed(3))
4937
- ]
4938
- };
4939
- }
4940
-
4941
4828
  // src/themes/visual-semantics.ts
4942
4829
  var visualNumberProperties = /* @__PURE__ */ new Set([
4943
4830
  "angle",
@@ -5865,9 +5752,11 @@ function assertThemeTypographyStyleFields(style, path) {
5865
5752
  throw themeTypeError(`${path}.transform`, "text transform", style.transform);
5866
5753
  }
5867
5754
  }
5868
- function assertThemeLighting(value, path) {
5869
- const lighting = assertPlainThemeRecord(value, path);
5870
- assertExactThemeKeys(lighting, path, themeLightingKeys);
5755
+ function assertThemeLighting(value, path, allowReset = false) {
5756
+ if (allowReset && isTileflowReset(value)) return;
5757
+ const authored = assertPlainThemeRecord(value, path);
5758
+ assertExactThemeKeys(authored, path, themeLightingKeys);
5759
+ const lighting = allowReset ? Object.fromEntries(Object.entries(authored).filter(([, entry]) => !isTileflowReset(entry))) : authored;
5871
5760
  if (Object.hasOwn(lighting, "sun")) {
5872
5761
  const sun = assertPlainThemeRecord(lighting.sun, `${path}.sun`);
5873
5762
  assertExactThemeKeys(sun, `${path}.sun`, themeSunKeys);
@@ -5979,7 +5868,7 @@ function assertThemeColorOperationNode(operation, path) {
5979
5868
  throw themeTypeError(`${path}.operation`, "alpha or mix", operation.operation);
5980
5869
  }
5981
5870
  function assertPlainThemeRecord(value, path) {
5982
- if (!isPlainRecord(value)) throw themeTypeError(path, "plain object", value);
5871
+ if (!isPlainRecord2(value)) throw themeTypeError(path, "plain object", value);
5983
5872
  return value;
5984
5873
  }
5985
5874
  function assertExactThemeKeys(value, path, allowedKeys) {
@@ -6260,7 +6149,7 @@ function cloneJson14(value) {
6260
6149
  function isRecord3(value) {
6261
6150
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
6262
6151
  }
6263
- function isPlainRecord(value) {
6152
+ function isPlainRecord2(value) {
6264
6153
  if (!isRecord3(value)) return false;
6265
6154
  const prototype = Object.getPrototypeOf(value);
6266
6155
  return prototype === Object.prototype || prototype === null;
@@ -6459,35 +6348,47 @@ function resolveColors(overrides = {}, defaults4 = defaultColors, moduleOverride
6459
6348
  }
6460
6349
  };
6461
6350
  }
6462
- function resolveThemeColors(theme) {
6351
+ var baseThemeColorRoles = {
6352
+ background: "surface.background",
6353
+ boundary: "boundaries.default",
6354
+ building: "surface.building",
6355
+ land: "surface.land",
6356
+ park: "surface.park",
6357
+ road: "roads.default",
6358
+ roadCasing: "roads.casing",
6359
+ roadMajor: "roads.major",
6360
+ text: "labels.primary",
6361
+ textHalo: "labels.halo",
6362
+ textMuted: "labels.muted",
6363
+ water: "surface.water"
6364
+ };
6365
+ var tileflowRequiredThemeColorRoles = Object.freeze(Object.values(baseThemeColorRoles));
6366
+ var baseThemeColorRoleSet = new Set(tileflowRequiredThemeColorRoles);
6367
+ function resolveThemeColors(theme, onDecision) {
6463
6368
  const tokens = resolveTileflowTheme(theme).tokens.color;
6464
6369
  const requireColor = (name) => {
6465
6370
  const value = tokens[name];
6466
- if (value !== void 0) return value;
6371
+ if (value !== void 0) {
6372
+ onDecision?.({ role: name, source: "theme", value: cssColor(value) });
6373
+ return value;
6374
+ }
6467
6375
  throw new Error(
6468
6376
  `Tileflow semantic compiler requires color token "${name}" in theme "${theme.id}".`
6469
6377
  );
6470
6378
  };
6471
- const optionalGroup = (group, keys) => Object.fromEntries(
6472
- keys.flatMap((key) => {
6473
- const value = tokens[`${group}.${key}`];
6474
- return value === void 0 ? [] : [[key, value]];
6475
- })
6476
- );
6477
- const base = {
6478
- background: requireColor("surface.background"),
6479
- boundary: requireColor("boundaries.default"),
6480
- building: requireColor("surface.building"),
6481
- land: requireColor("surface.land"),
6482
- park: requireColor("surface.park"),
6483
- road: requireColor("roads.default"),
6484
- roadCasing: requireColor("roads.casing"),
6485
- roadMajor: requireColor("roads.major"),
6486
- text: requireColor("labels.primary"),
6487
- textHalo: requireColor("labels.halo"),
6488
- textMuted: requireColor("labels.muted"),
6489
- water: requireColor("surface.water")
6379
+ const groupRoles = [];
6380
+ const optionalGroup = (group, keys) => {
6381
+ if (onDecision) groupRoles.push({ group, keys });
6382
+ return Object.fromEntries(
6383
+ keys.flatMap((key) => {
6384
+ const value = tokens[`${group}.${key}`];
6385
+ return value === void 0 ? [] : [[key, value]];
6386
+ })
6387
+ );
6490
6388
  };
6389
+ const base = Object.fromEntries(
6390
+ Object.entries(baseThemeColorRoles).map(([key, role]) => [key, requireColor(role)])
6391
+ );
6491
6392
  const modules = {
6492
6393
  boundaries: optionalGroup("boundaries", ["admin", "disputed", "major", "maritime"]),
6493
6394
  buildings: optionalGroup("buildings", [
@@ -6583,7 +6484,24 @@ function resolveThemeColors(theme) {
6583
6484
  ])
6584
6485
  };
6585
6486
  const colors = resolveColors({}, base, {}, modules);
6586
- const optionalColor = (name, fallback) => cssColor(tokens[name] ?? fallback);
6487
+ if (onDecision) {
6488
+ for (const { group, keys } of groupRoles) {
6489
+ for (const key of keys) {
6490
+ const role = `${group}.${key}`;
6491
+ if (baseThemeColorRoleSet.has(role)) continue;
6492
+ onDecision({
6493
+ role,
6494
+ source: tokens[role] === void 0 ? "derived" : "theme",
6495
+ value: colors[group][key]
6496
+ });
6497
+ }
6498
+ }
6499
+ }
6500
+ const optionalColor = (name, fallback) => {
6501
+ const value = cssColor(tokens[name] ?? fallback);
6502
+ onDecision?.({ role: name, source: tokens[name] === void 0 ? "derived" : "theme", value });
6503
+ return value;
6504
+ };
6587
6505
  return {
6588
6506
  ...colors,
6589
6507
  hydro: {
@@ -6721,19 +6639,272 @@ function hexToRgba(hex) {
6721
6639
  b: Number.parseInt(normalized.slice(4, 6), 16)
6722
6640
  };
6723
6641
  }
6724
- function cssColor(hex) {
6725
- const value = hex.replace("#", "");
6726
- if (value.length !== 4 && value.length !== 8) {
6727
- return hex;
6642
+ function cssColor(hex) {
6643
+ const value = hex.replace("#", "");
6644
+ if (value.length !== 4 && value.length !== 8) {
6645
+ return hex;
6646
+ }
6647
+ const { a, b, g, r } = hexToRgba(hex);
6648
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
6649
+ }
6650
+ function roundAlpha(value) {
6651
+ return Math.round(value * 1e3) / 1e3;
6652
+ }
6653
+ function rgbToHex(r, g, b) {
6654
+ return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
6655
+ }
6656
+
6657
+ // src/modules/roads/roundabouts.ts
6658
+ function compileRoundabouts(request, surfaces, context, contributions) {
6659
+ const { fields, layers } = context.data.schema;
6660
+ if (!layers.circularFeature) return;
6661
+ const drawn = surfaces.filter(({ fill }) => fill && fill.visible !== false);
6662
+ if (!drawn.length) return;
6663
+ const values = (resolve) => drawn.map((surface) => ({
6664
+ condition: tileflowRoadClassFilter(fields, surface.roadClass),
6665
+ value: toMapLibreStyleValue(resolve(surface))
6666
+ }));
6667
+ const widths = values(({ fill }) => fill?.width ?? 1);
6668
+ const casings = values(({ casing }) => casing?.width ?? 0);
6669
+ const gaps = values(({ casing }) => casing?.gapWidth ?? 0);
6670
+ const fallbackWidth = request?.fallbackWidth === void 0 ? void 0 : toMapLibreStyleValue(request.fallbackWidth);
6671
+ const borderWidth = request?.borderWidth === void 0 ? void 0 : toMapLibreStyleValue(request.borderWidth);
6672
+ const radiusAt15 = ["to-number", ["get", fields.circularRadiusAtZoom15], 0];
6673
+ const radiusMeters = ["to-number", ["get", fields.circularRadiusMeters], 0];
6674
+ const innerMeters = ["to-number", ["get", fields.circularInnerRadiusMeters], 0];
6675
+ const outerMeters = ["to-number", ["get", fields.circularOuterRadiusMeters], 0];
6676
+ const physical = [
6677
+ "all",
6678
+ [">", radiusMeters, 0],
6679
+ [">=", innerMeters, 0],
6680
+ [">", outerMeters, innerMeters]
6681
+ ];
6682
+ const geometryLevels = zoomLevels([
6683
+ ...fallbackWidth === void 0 ? widths.map(({ value }) => value) : [fallbackWidth],
6684
+ ...borderWidth === void 0 ? [...widths, ...casings, ...gaps].map(({ value }) => value) : [borderWidth]
6685
+ ]);
6686
+ const borderAt = (level) => borderWidth === void 0 ? select(
6687
+ values(({ casing, fill }) => {
6688
+ if (!casing || casing.visible === false) return 0;
6689
+ const casingWidth = atZoom(toMapLibreStyleValue(casing.width ?? 0), level);
6690
+ const gap = atZoom(toMapLibreStyleValue(casing.gapWidth ?? 0), level);
6691
+ const fillWidth = atZoom(toMapLibreStyleValue(fill?.width ?? 1), level);
6692
+ return [
6693
+ "max",
6694
+ 0,
6695
+ [
6696
+ "case",
6697
+ [">", gap, 0],
6698
+ ["-", ["+", gap, ["*", 2, casingWidth]], fillWidth],
6699
+ ["-", casingWidth, fillWidth]
6700
+ ]
6701
+ ];
6702
+ }),
6703
+ 0
6704
+ ) : atZoom(borderWidth, level);
6705
+ const geometry = (casing, width) => expression([
6706
+ "interpolate",
6707
+ ["exponential", 2],
6708
+ ["zoom"],
6709
+ ...geometryLevels.flatMap((level) => {
6710
+ const centerline = ["*", radiusAt15, 2 ** (level - 15)];
6711
+ const fallback = fallbackWidth === void 0 ? select(
6712
+ widths.map(({ condition, value }) => ({ condition, value: atZoom(value, level) })),
6713
+ 0
6714
+ ) : atZoom(fallbackWidth, level);
6715
+ const inner = [
6716
+ "case",
6717
+ physical,
6718
+ ["*", centerline, ["/", innerMeters, radiusMeters]],
6719
+ ["-", centerline, ["/", fallback, 2]]
6720
+ ];
6721
+ const fillWidth = [
6722
+ "case",
6723
+ physical,
6724
+ ["*", centerline, ["/", ["-", outerMeters, innerMeters], radiusMeters]],
6725
+ fallback
6726
+ ];
6727
+ const border = casing ? borderAt(level) : 0;
6728
+ return [
6729
+ level,
6730
+ width ? ["max", 0, ["+", fillWidth, border]] : ["max", 0, ["-", inner, ["/", border, 2]]]
6731
+ ];
6732
+ })
6733
+ ]);
6734
+ const phaseStyle = (phase) => ({
6735
+ color: "rgba(0, 0, 0, 0)",
6736
+ minZoom: 15,
6737
+ pitchAlignment: "map",
6738
+ pitchScale: "map",
6739
+ radius: geometry(phase === "casing", false),
6740
+ strokeWidth: geometry(phase === "casing", true),
6741
+ strokeColor: phase === "casing" ? context.colors.roads.casing : expression([
6742
+ "match",
6743
+ ["coalesce", ["get", fields.class], "minor"],
6744
+ "motorway",
6745
+ context.colors.roads.motorway,
6746
+ "trunk",
6747
+ context.colors.roads.trunk,
6748
+ "primary",
6749
+ context.colors.roads.primary,
6750
+ ["secondary", "tertiary"],
6751
+ context.colors.roads.secondary,
6752
+ context.colors.roads.minor
6753
+ ]),
6754
+ strokeOpacity: phaseVisibility(drawn, phase, fields)
6755
+ });
6756
+ for (const phase of ["casing", "fill"]) {
6757
+ const style = mergeTileflowDesign(phaseStyle(phase), request?.[phase]);
6758
+ if (style.visible === false) continue;
6759
+ const eligible = drawn.filter(
6760
+ (surface) => request?.[phase] !== void 0 || surface[phase] && surface[phase]?.visible !== false
6761
+ );
6762
+ if (!eligible.length) continue;
6763
+ contributions.push({
6764
+ kind: "layer",
6765
+ layer: applyCircleStyle(
6766
+ {
6767
+ id: `tileflow-road-circular-${phase}`,
6768
+ type: "circle",
6769
+ source: context.data.sourceId,
6770
+ "source-layer": layers.circularFeature,
6771
+ filter: [
6772
+ "all",
6773
+ ["==", ["get", fields.circularKind], "road_ring"],
6774
+ [
6775
+ "any",
6776
+ ...eligible.map((surface) => tileflowRoadClassFilter(fields, surface.roadClass))
6777
+ ]
6778
+ ]
6779
+ },
6780
+ style
6781
+ ),
6782
+ localOrder: 900,
6783
+ owner: "roads",
6784
+ // Approach fills cover the ring's border where the two meet.
6785
+ slot: phase === "casing" ? "transport-surface-casing" : "transport-surface-fill",
6786
+ target: `roads.roundabouts.${phase}`
6787
+ });
6788
+ }
6789
+ }
6790
+ function select(values, fallback) {
6791
+ if (!values.length) return fallback;
6792
+ const serialized = JSON.stringify(values[0].value);
6793
+ if (values.every(({ value }) => JSON.stringify(value) === serialized)) return values[0].value;
6794
+ return ["case", ...values.flatMap(({ condition, value }) => [condition, value]), fallback];
6795
+ }
6796
+ function phaseVisibility(surfaces, phase, fields) {
6797
+ const levels = [
6798
+ ...new Set(
6799
+ surfaces.flatMap((surface) => {
6800
+ const style = surface[phase];
6801
+ return [style?.minZoom, style?.maxZoom].filter(
6802
+ (value) => value !== void 0
6803
+ );
6804
+ })
6805
+ )
6806
+ ].sort((left, right) => left - right);
6807
+ if (!levels.length) return 1;
6808
+ const at = (level) => select(
6809
+ surfaces.map((surface) => {
6810
+ const style = surface[phase];
6811
+ return {
6812
+ condition: tileflowRoadClassFilter(fields, surface.roadClass),
6813
+ value: (style?.minZoom === void 0 || level >= style.minZoom) && (style?.maxZoom === void 0 || level < style.maxZoom) ? 1 : 0
6814
+ };
6815
+ }),
6816
+ 0
6817
+ );
6818
+ return expression([
6819
+ "step",
6820
+ ["zoom"],
6821
+ at(-1),
6822
+ ...levels.flatMap((level) => [level, at(level)])
6823
+ ]);
6824
+ }
6825
+ function zoomLevels(values) {
6826
+ const levels = new Set(Array.from({ length: 10 }, (_, index) => index + 15));
6827
+ for (const value of values) {
6828
+ if (!isZoomCurve(value)) continue;
6829
+ for (let index = 3; index < value.length - 1; index += 2) {
6830
+ const level = value[index];
6831
+ if (level < 15 || level > 24) continue;
6832
+ levels.add(level);
6833
+ if (value[0] === "step" && level > 15) {
6834
+ const previous = index > 3 ? value[index - 2] : 15;
6835
+ levels.add(level - Math.min(1e-6, (level - Math.max(15, previous)) / 2));
6836
+ }
6837
+ }
6838
+ if (value[0] !== "interpolate") continue;
6839
+ const interpolation = value[1];
6840
+ if (interpolation[0] === "exponential" && interpolation[1] === 2) continue;
6841
+ for (let index = 3; index < value.length - 3; index += 2) {
6842
+ let refine2 = function(left, right) {
6843
+ if (right - left <= 1 / 32) return;
6844
+ const factor = (level) => interpolationFactor2(interpolation, level - lower, upper - lower);
6845
+ const leftFactor = factor(left);
6846
+ const rightFactor = factor(right);
6847
+ const error = Math.max(
6848
+ ...[0.25, 0.5, 0.75].map((fraction) => {
6849
+ const level = left + (right - left) * fraction;
6850
+ const sampled = leftFactor + (rightFactor - leftFactor) * interpolationFactor2(["exponential", 2], level - left, right - left);
6851
+ return Math.abs((factor(level) - sampled) * difference);
6852
+ })
6853
+ );
6854
+ if (error <= 0.1) return;
6855
+ const middle = (left + right) / 2;
6856
+ levels.add(middle);
6857
+ refine2(left, middle);
6858
+ refine2(middle, right);
6859
+ };
6860
+ var refine = refine2;
6861
+ const lower = value[index];
6862
+ const upper = value[index + 2];
6863
+ const from = value[index + 1];
6864
+ const to = value[index + 3];
6865
+ if (typeof from !== "number" || typeof to !== "number" || from === to) continue;
6866
+ const difference = to - from;
6867
+ const start = Math.max(15, lower);
6868
+ const end = Math.min(24, upper);
6869
+ if (start >= end) continue;
6870
+ const knots = [start, ...levels].filter((level) => level >= start && level < end);
6871
+ knots.push(end);
6872
+ knots.sort((left, right) => left - right);
6873
+ for (let knot = 0; knot < knots.length - 1; knot++) {
6874
+ refine2(knots[knot], knots[knot + 1]);
6875
+ }
6876
+ }
6728
6877
  }
6729
- const { a, b, g, r } = hexToRgba(hex);
6730
- return `rgba(${r}, ${g}, ${b}, ${a})`;
6878
+ return [...levels].sort((left, right) => left - right);
6731
6879
  }
6732
- function roundAlpha(value) {
6733
- return Math.round(value * 1e3) / 1e3;
6880
+ function interpolationFactor2(interpolation, distance, span) {
6881
+ if (interpolation[0] === "linear" || interpolation[1] === 1) return distance / span;
6882
+ if (interpolation[0] !== "exponential") return Number.NaN;
6883
+ const logarithm = Math.log(interpolation[1]);
6884
+ return Math.expm1(logarithm * distance) / Math.expm1(logarithm * span);
6734
6885
  }
6735
- function rgbToHex(r, g, b) {
6736
- return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
6886
+ function isZoomCurve(value) {
6887
+ return Array.isArray(value) && (value[0] === "interpolate" && isZoom(value[2]) || value[0] === "step" && isZoom(value[1]));
6888
+ }
6889
+ function isZoom(value) {
6890
+ return Array.isArray(value) && value.length === 1 && value[0] === "zoom";
6891
+ }
6892
+ function atZoom(value, level) {
6893
+ if (!Array.isArray(value) || value[0] === "literal") return value;
6894
+ if (isZoom(value)) return level;
6895
+ if (!isZoomCurve(value)) return value.map((entry) => atZoom(entry, level));
6896
+ const stops = [];
6897
+ for (let index = 3; index < value.length - 1; index += 2) {
6898
+ stops.push({ level: value[index], value: value[index + 1] });
6899
+ }
6900
+ const exact = stops.find((stop) => stop.level === level);
6901
+ if (exact) return exact.value;
6902
+ const lower = [...stops].reverse().find((stop) => stop.level < level);
6903
+ const upper = stops.find((stop) => stop.level > level);
6904
+ if (value[0] === "step") return lower?.value ?? value[2];
6905
+ if (!lower) return upper?.value ?? 0;
6906
+ if (!upper) return lower.value;
6907
+ return ["interpolate", value[1], level, lower.level, lower.value, upper.level, upper.value];
6737
6908
  }
6738
6909
 
6739
6910
  // src/modules/roads/compiler.ts
@@ -6789,6 +6960,7 @@ function compileRoads(request, context) {
6789
6960
  const metrics = roadStyleMetrics(semantics);
6790
6961
  const defaults4 = defaultClassStyles(context, metrics, semantics);
6791
6962
  const contributions = [];
6963
+ const roundaboutSurfaces = [];
6792
6964
  const { sourceId: source, schema } = context.data;
6793
6965
  for (const [classIndex, roadClass] of roadClassOrder.entries()) {
6794
6966
  if (!visible.has(roadClass) || request?.classes?.[roadClass]?.enabled === false) continue;
@@ -6803,6 +6975,8 @@ function compileRoads(request, context) {
6803
6975
  request?.classes?.[roadClass]
6804
6976
  );
6805
6977
  const treatments = resolveRoadTreatments(request, schema.fields, roadClass);
6978
+ const roundaboutSurface = { roadClass };
6979
+ roundaboutSurfaces.push(roundaboutSurface);
6806
6980
  for (const structure of ["tunnel", "surface", "bridge"]) {
6807
6981
  const structureConfig = classConfig[structure];
6808
6982
  if (!structureConfig) continue;
@@ -6825,7 +6999,7 @@ function compileRoads(request, context) {
6825
6999
  phase,
6826
7000
  phaseDefaults.color
6827
7001
  );
6828
- contributions.push({
7002
+ const contribution = {
6829
7003
  family: roadLinePhysicalFamily(roadClass, structure, phase),
6830
7004
  kind: "layer",
6831
7005
  layer: applyLineStyle(
@@ -6853,7 +7027,30 @@ function compileRoads(request, context) {
6853
7027
  owner: "roads",
6854
7028
  slot: structureSlots[structure][phase],
6855
7029
  target: `roads.classes.${roadClass}.${structure}.${phase}`
6856
- });
7030
+ };
7031
+ if (structure === "surface" && phase !== "shadow") {
7032
+ roundaboutSurface[phase] = treatedStyle;
7033
+ }
7034
+ if (phase === "fill" && !isTileflowPathRoadClass(roadClass) && semantics.rampOrder === "below-roads") {
7035
+ const rampFilter = flagFilter(schema.fields.ramp);
7036
+ contributions.push({
7037
+ ...contribution,
7038
+ family: {
7039
+ ...roadLinePhysicalFamily(roadClass, structure, "fill"),
7040
+ group: `${structure}:rampFill:${roadPhysicalCohort(roadClass)}`,
7041
+ outputKey: `roads.cohorts.${structure}.${roadPhysicalCohort(roadClass)}.rampFill`
7042
+ },
7043
+ layer: {
7044
+ ...contribution.layer,
7045
+ id: `tileflow-road-${structure}-${roadClass}-rampFill`,
7046
+ filter: ["all", contribution.layer.filter, rampFilter]
7047
+ },
7048
+ localOrder: -1e3 + classIndex,
7049
+ target: `roads.classes.${roadClass}.${structure}.rampFill`
7050
+ });
7051
+ contribution.layer.filter = ["all", contribution.layer.filter, ["!", rampFilter]];
7052
+ }
7053
+ contributions.push(contribution);
6857
7054
  }
6858
7055
  const hatch = structureConfig.hatch;
6859
7056
  const fill = structureConfig.fill;
@@ -7033,7 +7230,7 @@ function compileRoads(request, context) {
7033
7230
  }
7034
7231
  }
7035
7232
  compileSidewalks(request?.sidewalks, areaStyles.pedestrian?.fill?.color, context, contributions);
7036
- compileRoundabouts(request?.roundabouts, context, contributions);
7233
+ compileRoundabouts(request?.roundabouts, roundaboutSurfaces, context, contributions);
7037
7234
  compileCrossings(request?.crossings, context, contributions);
7038
7235
  if (semantics.oneWayMarkers && visible.size > 0) {
7039
7236
  contributions.push({
@@ -7158,81 +7355,6 @@ function compileSidewalks(request, pedestrianAreaColor, context, contributions)
7158
7355
  });
7159
7356
  }
7160
7357
  }
7161
- function compileRoundabouts(request, context, contributions) {
7162
- const { fields, layers } = context.data.schema;
7163
- if (!layers.circularFeature) return;
7164
- const circularFields = {
7165
- circularInnerRadiusMeters: fields.circularInnerRadiusMeters,
7166
- circularOuterRadiusMeters: fields.circularOuterRadiusMeters,
7167
- circularRadiusAtZoom15: fields.circularRadiusAtZoom15,
7168
- circularRadiusMeters: fields.circularRadiusMeters,
7169
- class: fields.class
7170
- };
7171
- const radius = expression(circularRoadInnerRadius(circularFields, false));
7172
- const casingRadius = expression(circularRoadInnerRadius(circularFields, true));
7173
- const config = mergeTileflowDesign(
7174
- {
7175
- casing: {
7176
- color: "rgba(0, 0, 0, 0)",
7177
- minZoom: 15,
7178
- pitchAlignment: "map",
7179
- pitchScale: "map",
7180
- radius: casingRadius,
7181
- strokeColor: context.colors.roads.casing,
7182
- strokeWidth: expression(circularRoadStrokeWidth(circularFields, true))
7183
- },
7184
- fill: {
7185
- color: "rgba(0, 0, 0, 0)",
7186
- minZoom: 15,
7187
- pitchAlignment: "map",
7188
- pitchScale: "map",
7189
- radius,
7190
- strokeColor: expression([
7191
- "match",
7192
- ["coalesce", ["get", fields.class], "minor"],
7193
- "motorway",
7194
- context.colors.roads.motorway,
7195
- "trunk",
7196
- context.colors.roads.trunk,
7197
- "primary",
7198
- context.colors.roads.primary,
7199
- ["secondary", "tertiary"],
7200
- context.colors.roads.secondary,
7201
- context.colors.roads.minor
7202
- ]),
7203
- strokeWidth: expression(circularRoadStrokeWidth(circularFields, false))
7204
- }
7205
- },
7206
- request
7207
- );
7208
- const base = {
7209
- type: "circle",
7210
- source: context.data.sourceId,
7211
- "source-layer": layers.circularFeature,
7212
- filter: ["==", ["get", fields.circularKind], "road_ring"]
7213
- };
7214
- if (config.casing && config.casing.visible !== false) {
7215
- contributions.push({
7216
- kind: "layer",
7217
- layer: applyCircleStyle({ ...base, id: "tileflow-road-circular-casing" }, config.casing),
7218
- localOrder: 900,
7219
- owner: "roads",
7220
- // Approach fills must cover the circular casing where the roads meet.
7221
- slot: "transport-surface-casing",
7222
- target: "roads.roundabouts.casing"
7223
- });
7224
- }
7225
- if (config.fill && config.fill.visible !== false) {
7226
- contributions.push({
7227
- kind: "layer",
7228
- layer: applyCircleStyle({ ...base, id: "tileflow-road-circular-fill" }, config.fill),
7229
- localOrder: 900,
7230
- owner: "roads",
7231
- slot: "transport-surface-fill",
7232
- target: "roads.roundabouts.fill"
7233
- });
7234
- }
7235
- }
7236
7358
  function compileCrossings(request, context, contributions) {
7237
7359
  const { fields, layers } = context.data.schema;
7238
7360
  if (!request || !layers.streetFurniture) return;
@@ -7289,94 +7411,6 @@ function compileCrossings(request, context, contributions) {
7289
7411
  target: "roads.crossings"
7290
7412
  });
7291
7413
  }
7292
- var circularRoadBorderTotalWidth = 1;
7293
- function circularRoadInnerRadius(fields, casing) {
7294
- const values = circularRoadValues(fields);
7295
- const stops = [];
7296
- for (let level = 15; level <= 24; level += 1) {
7297
- const centerlineRadius = level === 15 ? values.radiusAtZoom15 : ["*", values.radiusAtZoom15, 2 ** (level - 15)];
7298
- const physicalInnerRadius = [
7299
- "*",
7300
- centerlineRadius,
7301
- ["/", values.innerRadiusMeters, values.radiusMeters]
7302
- ];
7303
- const baseWidth = circularRoadBaseWidth(fields.class);
7304
- const fallbackWidth = level === 15 ? baseWidth : ["*", baseWidth, circularRoadFallbackScale(level)];
7305
- const fallbackInnerRadius = ["-", centerlineRadius, ["/", fallbackWidth, 2]];
7306
- const innerRadius = ["case", values.hasPhysicalRadii, physicalInnerRadius, fallbackInnerRadius];
7307
- stops.push(level, [
7308
- "max",
7309
- 0,
7310
- casing ? ["-", innerRadius, circularRoadBorderTotalWidth / 2] : innerRadius
7311
- ]);
7312
- }
7313
- return ["interpolate", ["exponential", 2], ["zoom"], ...stops];
7314
- }
7315
- function circularRoadStrokeWidth(fields, casing) {
7316
- const values = circularRoadValues(fields);
7317
- const baseWidth = circularRoadBaseWidth(fields.class);
7318
- const stops = [];
7319
- for (let level = 15; level <= 24; level += 1) {
7320
- const centerlineRadius = level === 15 ? values.radiusAtZoom15 : ["*", values.radiusAtZoom15, 2 ** (level - 15)];
7321
- const physicalWidth = [
7322
- "*",
7323
- centerlineRadius,
7324
- ["/", ["-", values.outerRadiusMeters, values.innerRadiusMeters], values.radiusMeters]
7325
- ];
7326
- const fallbackWidth = level === 15 ? baseWidth : ["*", baseWidth, circularRoadFallbackScale(level)];
7327
- stops.push(level, [
7328
- "case",
7329
- values.hasPhysicalRadii,
7330
- casing ? ["+", physicalWidth, circularRoadBorderTotalWidth] : physicalWidth,
7331
- casing ? ["+", fallbackWidth, circularRoadBorderTotalWidth] : fallbackWidth
7332
- ]);
7333
- }
7334
- return ["interpolate", ["exponential", 2], ["zoom"], ...stops];
7335
- }
7336
- function circularRoadBaseWidth(classField) {
7337
- return [
7338
- "match",
7339
- ["coalesce", ["get", classField], "minor"],
7340
- "motorway",
7341
- 6,
7342
- "trunk",
7343
- 5.5,
7344
- "primary",
7345
- 5,
7346
- "secondary",
7347
- 4.5,
7348
- "tertiary",
7349
- 4,
7350
- "service",
7351
- 2.5,
7352
- "track",
7353
- 2,
7354
- 3
7355
- ];
7356
- }
7357
- function circularRoadValues(fields) {
7358
- const radiusAtZoom15 = ["to-number", ["get", fields.circularRadiusAtZoom15], 0];
7359
- const radiusMeters = ["to-number", ["get", fields.circularRadiusMeters], 0];
7360
- const outerRadiusMeters = ["to-number", ["get", fields.circularOuterRadiusMeters], 0];
7361
- const innerRadiusMeters = ["to-number", ["get", fields.circularInnerRadiusMeters], 0];
7362
- return {
7363
- hasPhysicalRadii: [
7364
- "all",
7365
- [">", radiusMeters, 0],
7366
- [">", outerRadiusMeters, innerRadiusMeters],
7367
- [">=", innerRadiusMeters, 0]
7368
- ],
7369
- innerRadiusMeters,
7370
- outerRadiusMeters,
7371
- radiusAtZoom15,
7372
- radiusMeters
7373
- };
7374
- }
7375
- function circularRoadFallbackScale(level) {
7376
- const interpolationBase = 1.35;
7377
- const progress = (interpolationBase ** (level - 15) - 1) / (interpolationBase ** (22 - 15) - 1);
7378
- return 1 + progress * 1.2;
7379
- }
7380
7414
  function widthMatchedHatchPattern(pattern, widths, renderedWidth, minZoom, maxZoom) {
7381
7415
  if (typeof pattern !== "string") {
7382
7416
  throw new Error("Road hatch patternWidths requires a literal pattern prefix");
@@ -7492,7 +7526,12 @@ function applyRoadTreatments(base, treatments, structure, phase, fallbackColor)
7492
7526
  for (const property of paintProperties) {
7493
7527
  const branches = treatments.flatMap(({ condition, style }) => {
7494
7528
  const value = style[structure]?.[phase]?.[property];
7495
- return value === void 0 ? [] : [{ condition, resolve: () => expressionOutput(value) }];
7529
+ return value === void 0 ? [] : [
7530
+ {
7531
+ condition,
7532
+ resolve: () => expressionOutput(toMapLibreStyleValue(value))
7533
+ }
7534
+ ];
7496
7535
  });
7497
7536
  if (branches.length > 0) {
7498
7537
  if (property === "dash" && isSolidRoadDash(result[property] ?? treatmentPaintDefaults.dash) && branches.every(({ resolve }) => isSolidRoadDash(resolve()))) {
@@ -7982,13 +8021,227 @@ function modalFilter(classField, subclassField, modes) {
7982
8021
  ["match", ["get", subclassField], modes, true, false]
7983
8022
  ];
7984
8023
  }
7985
-
7986
- // src/modules/vegetation/index.ts
7987
- function vegetation(options = {}) {
7988
- return { type: "vegetation", ...cloneJson16(options) };
7989
- }
7990
- function cloneJson16(value) {
7991
- return JSON.parse(JSON.stringify(value));
8024
+
8025
+ // src/modules/vegetation/index.ts
8026
+ function vegetation(options = {}) {
8027
+ return { type: "vegetation", ...cloneJson16(options) };
8028
+ }
8029
+ function cloneJson16(value) {
8030
+ return JSON.parse(JSON.stringify(value));
8031
+ }
8032
+
8033
+ // src/trees-3d.ts
8034
+ var tileflowWorldTreeFields = Object.freeze({
8035
+ height: "height",
8036
+ diameterCrown: "diameter_crown",
8037
+ circumference: "circumference",
8038
+ genus: "genus",
8039
+ species: "species",
8040
+ leafType: "leaf_type",
8041
+ taxon: "taxon",
8042
+ canopyRank: "rank",
8043
+ canopyClearance: "clearance_px_z15",
8044
+ canopyCover: "cover_pct",
8045
+ canopyHeight: "height_m",
8046
+ subclass: "subclass",
8047
+ hedge: "hedge"
8048
+ });
8049
+ var tileflowDefaultUnknownTreeForms = Object.freeze({
8050
+ round: 1,
8051
+ open: 1,
8052
+ tall: 1
8053
+ });
8054
+ var broadleaves = Object.freeze({
8055
+ ...Object.fromEntries(
8056
+ [
8057
+ "acacia",
8058
+ "albizia",
8059
+ "arbutus",
8060
+ "bursera",
8061
+ "ceratonia",
8062
+ "delonix",
8063
+ "ficus",
8064
+ "gleditsia",
8065
+ "jacaranda",
8066
+ "olea",
8067
+ "quercus",
8068
+ "robinia",
8069
+ "schinus",
8070
+ "sophora",
8071
+ "styphnolobium",
8072
+ "tamarix"
8073
+ ].map((genus) => [genus, "open"])
8074
+ ),
8075
+ ...Object.fromEntries(
8076
+ [
8077
+ "betula",
8078
+ "celtis",
8079
+ "fraxinus",
8080
+ "ginkgo",
8081
+ "liquidambar",
8082
+ "liriodendron",
8083
+ "magnolia",
8084
+ "platanus",
8085
+ "populus",
8086
+ "pyrus",
8087
+ "ulmus",
8088
+ "zelkova"
8089
+ ].map((genus) => [genus, "tall"])
8090
+ ),
8091
+ ...Object.fromEntries(
8092
+ [
8093
+ "acer",
8094
+ "aesculus",
8095
+ "carpinus",
8096
+ "castanea",
8097
+ "catalpa",
8098
+ "cercis",
8099
+ "citrus",
8100
+ "fagus",
8101
+ "lagerstroemia",
8102
+ "malus",
8103
+ "melia",
8104
+ "morus",
8105
+ "prunus",
8106
+ "sorbus",
8107
+ "tilia"
8108
+ ].map((genus) => [genus, "round"])
8109
+ )
8110
+ });
8111
+ var tileflowTreeModelIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
8112
+ var tileflowVegetationMetadataKey = "tileflow:vegetation";
8113
+ function tileflowVegetationMetadata(vegetation2) {
8114
+ return { version: 1, ...vegetation2 };
8115
+ }
8116
+
8117
+ // src/modules/vegetation/compiler.ts
8118
+ function compileVegetation(request, context) {
8119
+ const minZoom = request?.flat?.minZoom ?? 16;
8120
+ const mode = request?.mode ?? "3d";
8121
+ const config = mergeTileflowDesign(
8122
+ {
8123
+ type: "vegetation",
8124
+ enabled: true,
8125
+ flat: {
8126
+ color: context.colors.landcover.wood,
8127
+ minZoom,
8128
+ opacity: mode === "3d" ? 0.82 : 0.9,
8129
+ pitchAlignment: "map",
8130
+ pitchScale: "map",
8131
+ radius: minZoom < 24 ? zoom.linear([
8132
+ [minZoom, 2.2],
8133
+ [minZoom < 20 ? 20 : 24, 8]
8134
+ ]) : 2.2,
8135
+ strokeColor: context.colors.park,
8136
+ strokeOpacity: 0.55,
8137
+ strokeWidth: 0.8
8138
+ },
8139
+ mode,
8140
+ threeDimensional: {
8141
+ barkColor: context.colors.vegetation.tree.bark,
8142
+ broadleafColors: context.colors.vegetation.tree.broadleaf,
8143
+ coniferColors: context.colors.vegetation.tree.conifer,
8144
+ palmColors: context.colors.vegetation.tree.palm,
8145
+ crownScale: 1,
8146
+ heightScale: 1
8147
+ }
8148
+ },
8149
+ request
8150
+ );
8151
+ if (config.enabled === false || !context.data.schema.layers.tree) return [];
8152
+ const resolvedMode = config.mode ?? "3d";
8153
+ return [
8154
+ {
8155
+ kind: "layer",
8156
+ layer: applyCircleStyle(
8157
+ {
8158
+ id: "tileflow-vegetation-trees",
8159
+ type: "circle",
8160
+ source: context.data.sourceId,
8161
+ "source-layer": context.data.schema.layers.tree,
8162
+ // The layer also holds rows of trees as lines; a circle on each vertex would misdraw them.
8163
+ filter: ["==", ["geometry-type"], "Point"],
8164
+ metadata: {
8165
+ "tileflow:vegetation-mode": resolvedMode,
8166
+ ...resolvedMode === "3d" ? { "tileflow:vegetation-fallback": "flat-circle" } : {}
8167
+ }
8168
+ },
8169
+ config.flat ?? {}
8170
+ ),
8171
+ localOrder: 1,
8172
+ owner: "vegetation",
8173
+ slot: "vegetation",
8174
+ target: "vegetation.trees"
8175
+ }
8176
+ ];
8177
+ }
8178
+ function tileflowVegetationDescription(request, context) {
8179
+ const { schema } = context.data;
8180
+ if (request?.enabled === false || request?.mode === "flat" || !schema.layers.tree)
8181
+ return void 0;
8182
+ const tree = context.colors.vegetation.tree;
8183
+ const style = request?.threeDimensional ?? {};
8184
+ const colours = (value, fallback) => Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string") ? value : [...fallback];
8185
+ const number = (value) => {
8186
+ const resolved = value === void 0 ? void 0 : toMapLibreStyleValue(value);
8187
+ return typeof resolved === "number" && resolved > 0 ? resolved : 1;
8188
+ };
8189
+ const field2 = (name, fallback) => schema.fields[name] ?? fallback;
8190
+ return {
8191
+ source: context.data.sourceId,
8192
+ layers: {
8193
+ tree: schema.layers.tree,
8194
+ ...schema.layers.canopy ? { canopy: schema.layers.canopy } : {},
8195
+ ...schema.layers.trafficControl && style.hedges !== false ? { hedge: schema.layers.trafficControl } : {}
8196
+ },
8197
+ fields: {
8198
+ ...tileflowWorldTreeFields,
8199
+ height: field2("height", tileflowWorldTreeFields.height),
8200
+ diameterCrown: field2("diameterCrown", tileflowWorldTreeFields.diameterCrown),
8201
+ genus: field2("genus", tileflowWorldTreeFields.genus),
8202
+ species: field2("species", tileflowWorldTreeFields.species),
8203
+ leafType: field2("leafType", tileflowWorldTreeFields.leafType),
8204
+ canopyRank: field2("canopyRank", tileflowWorldTreeFields.canopyRank),
8205
+ canopyClearance: field2("canopyClearanceAtZoom15", tileflowWorldTreeFields.canopyClearance),
8206
+ canopyCover: field2("canopyCoverPercent", tileflowWorldTreeFields.canopyCover),
8207
+ subclass: field2("subclass", tileflowWorldTreeFields.subclass),
8208
+ hedge: field2("hedge", tileflowWorldTreeFields.hedge)
8209
+ },
8210
+ colors: {
8211
+ bark: typeof style.barkColor === "string" ? style.barkColor : tree.bark,
8212
+ broadleaf: colours(style.broadleafColors, tree.broadleaf),
8213
+ conifer: colours(style.coniferColors, tree.conifer),
8214
+ palm: colours(style.palmColors, tree.palm),
8215
+ hedge: colours(style.hedgeColors, tree.broadleaf.slice(0, 2))
8216
+ },
8217
+ heightScale: number(style.heightScale),
8218
+ crownScale: number(style.crownScale),
8219
+ forms: Object.fromEntries(
8220
+ Object.entries(style.forms ?? {}).map(([form, own]) => [
8221
+ form,
8222
+ {
8223
+ heightScale: number(own?.heightScale),
8224
+ crownScale: number(own?.crownScale),
8225
+ ...own?.models?.length ? { models: [...own.models] } : {}
8226
+ }
8227
+ ])
8228
+ ),
8229
+ // Filled by the compiler from the map's prepared models.
8230
+ models: {},
8231
+ finish: style.finish ?? "faceted",
8232
+ edges: (() => {
8233
+ const edges = toMapLibreStyleValue(style.edges ?? 0);
8234
+ return typeof edges === "number" ? edges : 0;
8235
+ })(),
8236
+ ...(() => {
8237
+ const edgeColor = style.edgeColor === void 0 ? void 0 : toMapLibreStyleValue(style.edgeColor);
8238
+ return typeof edgeColor === "string" ? { edgeColor } : {};
8239
+ })(),
8240
+ unknownForms: style.unknownForms ?? tileflowDefaultUnknownTreeForms,
8241
+ regions: style.regions ?? [],
8242
+ // Filled by the compiler once the style's layers are final.
8243
+ flatLayers: []
8244
+ };
7992
8245
  }
7993
8246
 
7994
8247
  // src/modules/water/index.ts
@@ -8256,6 +8509,9 @@ function compileWater(request, context, language = "auto") {
8256
8509
  }
8257
8510
  if (sidecarBathymetry) {
8258
8511
  const { layers: marineLayers } = tileflowBathymetryV1Schema;
8512
+ const splitPlacement = Boolean(
8513
+ config.bathymetryLandforms?.placementByGeometry || config.bathymetryWaterNames?.placementByGeometry
8514
+ );
8259
8515
  const landforms2 = bathymetryProduct?.vectorLayers?.landforms;
8260
8516
  const waterNames = bathymetryProduct?.vectorLayers?.waterNames;
8261
8517
  if (landforms2) {
@@ -8263,7 +8519,7 @@ function compileWater(request, context, language = "auto") {
8263
8519
  availability: landforms2,
8264
8520
  context,
8265
8521
  language,
8266
- localOrder: -300,
8522
+ localOrder: splitPlacement ? -900 : -300,
8267
8523
  source: sidecarBathymetry.sourceId,
8268
8524
  sourceLayer: marineLayers.landforms,
8269
8525
  style: config.bathymetryLandforms,
@@ -8276,7 +8532,7 @@ function compileWater(request, context, language = "auto") {
8276
8532
  availability: waterNames,
8277
8533
  context,
8278
8534
  language,
8279
- localOrder: -150,
8535
+ localOrder: splitPlacement ? -450 : -150,
8280
8536
  source: sidecarBathymetry.sourceId,
8281
8537
  sourceLayer: marineLayers.waterNames,
8282
8538
  style: config.bathymetryWaterNames,
@@ -8380,65 +8636,80 @@ function appendBathymetryNames(contributions, options) {
8380
8636
  options.style
8381
8637
  );
8382
8638
  if (style.visible === false || style.text?.visible === false) return;
8383
- const layer = applySymbolStyle(
8384
- {
8385
- id: options.id,
8386
- type: "symbol",
8387
- source: options.source,
8388
- "source-layer": options.sourceLayer,
8389
- filter: [
8390
- "all",
8391
- ["match", ["geometry-type"], ["Point", "LineString", "Polygon"], true, false],
8392
- ["!=", name, ""]
8393
- ]
8394
- },
8395
- style
8396
- );
8397
8639
  const sourceMinimum = ["to-number", ["coalesce", ["get", fields.minZoom], 0], 0];
8398
8640
  const sourceMaximum = ["to-number", ["coalesce", ["get", fields.maxZoom], 24], 24];
8399
- const baseFilter = layer.filter;
8400
- const append = (result, target, order = 0) => {
8401
- contributions.push({
8402
- kind: "layer",
8403
- layer: result,
8404
- localOrder: options.localOrder + order,
8405
- owner: "water",
8406
- slot: "symbols",
8407
- target
8408
- });
8409
- };
8410
- if (options.availability === true) {
8411
- append(
8412
- {
8413
- ...layer,
8414
- filter: [...baseFilter, [">=", ["zoom"], sourceMinimum], ["<", ["zoom"], sourceMaximum]]
8415
- },
8416
- options.target
8417
- );
8418
- return;
8419
- }
8420
- const ranges = [...options.availability.zoomRanges].sort(
8641
+ const ranges = options.availability === true ? void 0 : [...options.availability.zoomRanges].sort(
8421
8642
  (left, right) => left.minZoom - right.minZoom || left.maxZoom - right.maxZoom
8422
8643
  );
8423
- for (const [index, range2] of ranges.entries()) {
8424
- const minzoom = Math.max(Number(layer.minzoom ?? 0), range2.minZoom);
8425
- const maxzoom = Math.min(Number(layer.maxzoom ?? 24), range2.maxZoom);
8426
- if (minzoom >= maxzoom) continue;
8427
- append(
8644
+ const policy = style.placementByGeometry;
8645
+ const geometryFamilies = [
8646
+ ["Point", policy?.point],
8647
+ ["LineString", policy?.line],
8648
+ ["Polygon", policy?.polygon]
8649
+ ];
8650
+ const groups = policy ? ["point", "line", "line-center"].flatMap((placement) => {
8651
+ const geometries = geometryFamilies.filter(([, value]) => value === placement).map(([geometry]) => geometry);
8652
+ return geometries.length ? [{ placement, geometries }] : [];
8653
+ }) : [{ placement: style.placement, geometries: ["Point", "LineString", "Polygon"] }];
8654
+ for (const [groupIndex, group] of groups.entries()) {
8655
+ const suffix = policy ? `-placement-${group.placement}` : "";
8656
+ const placementTarget = group.placement === "line-center" ? "LineCenter" : group.placement === "line" ? "Line" : "Point";
8657
+ const target = policy ? `${options.target}.placement${placementTarget}` : options.target;
8658
+ const layer = applySymbolStyle(
8428
8659
  {
8429
- ...layer,
8430
- id: `${options.id}-cohort-${index}`,
8431
- minzoom,
8432
- maxzoom,
8660
+ id: `${options.id}${suffix}`,
8661
+ type: "symbol",
8662
+ source: options.source,
8663
+ "source-layer": options.sourceLayer,
8433
8664
  filter: [
8434
- ...baseFilter,
8435
- ["==", sourceMinimum, range2.minZoom],
8436
- ["==", sourceMaximum, range2.maxZoom]
8665
+ "all",
8666
+ ["match", ["geometry-type"], group.geometries, true, false],
8667
+ ["!=", name, ""]
8437
8668
  ]
8438
8669
  },
8439
- `${options.target}.cohort${index}`,
8440
- index
8670
+ { ...style, placement: group.placement }
8441
8671
  );
8672
+ const baseFilter = layer.filter;
8673
+ const append = (result, rangeTarget, order = 0) => {
8674
+ contributions.push({
8675
+ kind: "layer",
8676
+ layer: result,
8677
+ localOrder: options.localOrder + order * (policy ? 3 : 1) + groupIndex,
8678
+ owner: "water",
8679
+ slot: "symbols",
8680
+ target: rangeTarget
8681
+ });
8682
+ };
8683
+ if (!ranges) {
8684
+ append(
8685
+ {
8686
+ ...layer,
8687
+ filter: [...baseFilter, [">=", ["zoom"], sourceMinimum], ["<", ["zoom"], sourceMaximum]]
8688
+ },
8689
+ target
8690
+ );
8691
+ continue;
8692
+ }
8693
+ for (const [index, range2] of ranges.entries()) {
8694
+ const minzoom = Math.max(Number(layer.minzoom ?? 0), range2.minZoom);
8695
+ const maxzoom = Math.min(Number(layer.maxzoom ?? 24), range2.maxZoom);
8696
+ if (minzoom >= maxzoom) continue;
8697
+ append(
8698
+ {
8699
+ ...layer,
8700
+ id: `${layer.id}-cohort-${index}`,
8701
+ minzoom,
8702
+ maxzoom,
8703
+ filter: [
8704
+ ...baseFilter,
8705
+ ["==", sourceMinimum, range2.minZoom],
8706
+ ["==", sourceMaximum, range2.maxZoom]
8707
+ ]
8708
+ },
8709
+ `${target}.cohort${index}`,
8710
+ index
8711
+ );
8712
+ }
8442
8713
  }
8443
8714
  }
8444
8715
  function appendBathymetryRelief(contributions, relief, context) {
@@ -9018,7 +9289,7 @@ var tileflowOverlayPlacements = [
9018
9289
  ];
9019
9290
  var tileflowHostedSourceLimit = 16;
9020
9291
  function isTileflowRemoveOperation(value) {
9021
- return isPlainRecord2(value) && value.op === "remove" && Object.keys(value).length === 1;
9292
+ return isPlainRecord3(value) && value.op === "remove" && Object.keys(value).length === 1;
9022
9293
  }
9023
9294
  var TileflowOverlayCompilationError = class extends Error {
9024
9295
  code;
@@ -9112,7 +9383,7 @@ function insertTileflowOverlays(input) {
9112
9383
  ...cloneJson18(layer),
9113
9384
  source: overlay.source,
9114
9385
  metadata: {
9115
- ...isPlainRecord2(layer.metadata) ? cloneJson18(layer.metadata) : {},
9386
+ ...isPlainRecord3(layer.metadata) ? cloneJson18(layer.metadata) : {},
9116
9387
  "tileflow:overlay": overlayId
9117
9388
  }
9118
9389
  };
@@ -9150,7 +9421,7 @@ function isSafeLocalPmtilesPath(value) {
9150
9421
  }
9151
9422
  function cloneJson18(value) {
9152
9423
  if (Array.isArray(value)) return value.map(cloneJson18);
9153
- if (!isPlainRecord2(value)) return value;
9424
+ if (!isPlainRecord3(value)) return value;
9154
9425
  return Object.fromEntries(
9155
9426
  Object.entries(value).map(([key, child]) => [key, cloneJson18(child)])
9156
9427
  );
@@ -9167,12 +9438,202 @@ function tileflowOverlayInsertionIndex(layers, placement) {
9167
9438
  function compareCodeUnits4(left, right) {
9168
9439
  return left < right ? -1 : left > right ? 1 : 0;
9169
9440
  }
9170
- function isPlainRecord2(value) {
9441
+ function isPlainRecord3(value) {
9171
9442
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
9172
9443
  const prototype = Object.getPrototypeOf(value);
9173
9444
  return prototype === Object.prototype || prototype === null;
9174
9445
  }
9175
9446
 
9447
+ // src/boundary-areas/schema.ts
9448
+ var tileflowBoundaryClasses = [
9449
+ "country",
9450
+ "dependency",
9451
+ "region",
9452
+ "province_or_county",
9453
+ "municipality",
9454
+ "district",
9455
+ "neighbourhood"
9456
+ ];
9457
+ var tileflowBoundaryClassTiers = ["primary", "secondary"];
9458
+ var tileflowBoundaryAreasV1Schema = Object.freeze({
9459
+ classes: tileflowBoundaryClasses,
9460
+ /** First zoom at which each class appears, by tier. */
9461
+ classMinzoom: Object.freeze({
9462
+ country: Object.freeze({ primary: 0, secondary: 0 }),
9463
+ dependency: Object.freeze({ primary: 0, secondary: 0 }),
9464
+ region: Object.freeze({ primary: 3, secondary: 3 }),
9465
+ province_or_county: Object.freeze({ primary: 6, secondary: 7 }),
9466
+ municipality: Object.freeze({ primary: 8, secondary: 8 }),
9467
+ district: Object.freeze({ primary: 10, secondary: 10 }),
9468
+ neighbourhood: Object.freeze({ primary: 11, secondary: 11 })
9469
+ }),
9470
+ fields: Object.freeze({
9471
+ adminDepth: "admin_depth",
9472
+ adminLevel: "admin_level",
9473
+ class: "class",
9474
+ classTier: "class_tier",
9475
+ iso31661: "iso_3166_1",
9476
+ iso31662: "iso_3166_2",
9477
+ name: "name",
9478
+ nuts: "nuts",
9479
+ ref: "ref",
9480
+ wikidata: "wikidata"
9481
+ }),
9482
+ layers: Object.freeze({ areas: "boundary_area", labels: "boundary_label" }),
9483
+ /** Properties present on `boundary_area` polygons. */
9484
+ areaProperties: Object.freeze([
9485
+ "class",
9486
+ "class_tier",
9487
+ "admin_level",
9488
+ "admin_depth",
9489
+ "ref",
9490
+ "iso_3166_1"
9491
+ ]),
9492
+ maxzoom: 12,
9493
+ minzoom: 0,
9494
+ product: "boundaries-v1",
9495
+ schemaVersion: 1,
9496
+ tiers: tileflowBoundaryClassTiers
9497
+ });
9498
+ var tileflowBoundaryFamilies = Object.freeze({
9499
+ boundaries: Object.freeze({
9500
+ attribution: "\xA9 OpenStreetMap contributors",
9501
+ license: "ODbL-1.0",
9502
+ path: "/tiles/boundaries/tiles.json",
9503
+ sourceId: "tileflow-boundaries"
9504
+ })
9505
+ });
9506
+ function isTileflowBoundaryFamily(value) {
9507
+ return typeof value === "string" && Object.hasOwn(tileflowBoundaryFamilies, value);
9508
+ }
9509
+
9510
+ // src/boundary-areas/choropleth.ts
9511
+ var maxScaleStops = 64;
9512
+ var maxCategories = 256;
9513
+ var choroplethScale = Object.freeze({
9514
+ categories(categories) {
9515
+ const entries = Object.entries(categories);
9516
+ if (entries.length < 1 || entries.length > maxCategories) {
9517
+ throw new Error(`A category scale needs between 1 and ${maxCategories} categories.`);
9518
+ }
9519
+ for (const [category, color2] of entries) {
9520
+ if (!category) throw new Error("Category scale keys must not be empty.");
9521
+ assertColor2(color2);
9522
+ }
9523
+ return Object.freeze({ categories: Object.freeze({ ...categories }), kind: "categories" });
9524
+ },
9525
+ linear(stops) {
9526
+ if (stops.length < 2) throw new Error("A linear scale needs at least two stops.");
9527
+ return Object.freeze({ kind: "linear", stops: validateStops(stops) });
9528
+ },
9529
+ step(base, stops) {
9530
+ assertColor2(base);
9531
+ if (stops.length < 1) throw new Error("A step scale needs at least one stop.");
9532
+ return Object.freeze({ base, kind: "step", stops: validateStops(stops) });
9533
+ }
9534
+ });
9535
+ var tileflowBoundaryChoroplethMetadataKey = "tileflow:boundaryChoropleth";
9536
+ function validateStops(stops) {
9537
+ if (stops.length > maxScaleStops) {
9538
+ throw new Error(`A choropleth scale accepts at most ${maxScaleStops} stops.`);
9539
+ }
9540
+ let previous = -Infinity;
9541
+ for (const [value, color2] of stops) {
9542
+ if (!Number.isFinite(value) || value <= previous) {
9543
+ throw new Error("Choropleth scale stops must be finite and strictly increasing.");
9544
+ }
9545
+ assertColor2(color2);
9546
+ previous = value;
9547
+ }
9548
+ return Object.freeze(stops.map((stop) => Object.freeze([stop[0], stop[1]])));
9549
+ }
9550
+ function assertColor2(value) {
9551
+ if (typeof value !== "string" || !value.trim()) {
9552
+ throw new Error("Choropleth colors must be non-empty CSS color strings.");
9553
+ }
9554
+ }
9555
+
9556
+ // src/boundary-areas/source.ts
9557
+ function resolveBoundaryAreas(config, apiBaseUrl = "https://api.tileflow.dev") {
9558
+ if (config === void 0 || config === false) return void 0;
9559
+ if (!isTileflowBoundaryFamily(config.family)) {
9560
+ throw new Error(
9561
+ `Unknown Tileflow Boundaries family: ${String(config.family)}. Expected one of: ${Object.keys(
9562
+ tileflowBoundaryFamilies
9563
+ ).join(", ")}.`
9564
+ );
9565
+ }
9566
+ if (config.promoteId !== void 0 && config.promoteId !== "ref") {
9567
+ throw new Error('Tileflow Boundaries promoteId must be "ref" when set.');
9568
+ }
9569
+ const family = tileflowBoundaryFamilies[config.family];
9570
+ const url = validatePublicVectorUrl(
9571
+ config.url ?? new URL(family.path, normalizeApiBaseUrl3(apiBaseUrl)).toString()
9572
+ );
9573
+ const { areas, labels: labels2 } = tileflowBoundaryAreasV1Schema.layers;
9574
+ return Object.freeze({
9575
+ identity: Object.freeze({
9576
+ family: config.family,
9577
+ kind: "tileflow-boundaries",
9578
+ product: tileflowBoundaryAreasV1Schema.product,
9579
+ ...config.promoteId ? { promoteId: config.promoteId } : {},
9580
+ schemaVersion: tileflowBoundaryAreasV1Schema.schemaVersion,
9581
+ sourceId: family.sourceId,
9582
+ url
9583
+ }),
9584
+ source: Object.freeze({
9585
+ attribution: family.attribution,
9586
+ ...config.promoteId ? { promoteId: Object.freeze({ [areas]: config.promoteId, [labels2]: config.promoteId }) } : {},
9587
+ type: "vector",
9588
+ url
9589
+ }),
9590
+ sourceId: family.sourceId
9591
+ });
9592
+ }
9593
+ function normalizeApiBaseUrl3(value) {
9594
+ const url = new URL(value);
9595
+ if (url.username || url.password) {
9596
+ throw new Error("Tileflow API base URL must not contain user information.");
9597
+ }
9598
+ return url.toString();
9599
+ }
9600
+
9601
+ // src/boundary-areas/compile.ts
9602
+ function validateBoundaryChoroplethOverlays(mapId, overlays, boundaryAreas) {
9603
+ const entries = Object.entries(overlays ?? {});
9604
+ for (const [overlayId, overlay] of entries) {
9605
+ for (const layer of overlay.layers) {
9606
+ const metadata = layer.metadata?.[tileflowBoundaryChoroplethMetadataKey];
9607
+ if (!metadata) continue;
9608
+ const path = `map.${mapId}.overlays.${overlayId}`;
9609
+ if (!boundaryAreas || boundaryAreas.identity.family !== metadata.family) {
9610
+ throw new TileflowOverlayCompilationError(
9611
+ "TF_BOUNDARY_AREAS_REQUIRED",
9612
+ path,
9613
+ `Boundary choropleth "${overlayId}" needs the map to declare boundaryAreas with family "${metadata.family}".`,
9614
+ `Add boundaryAreas: {family: '${metadata.family}'} to the map.`
9615
+ );
9616
+ }
9617
+ if (overlay.source !== tileflowBoundaryFamilies[metadata.family].sourceId) {
9618
+ throw new TileflowOverlayCompilationError(
9619
+ "TF_BOUNDARY_CHOROPLETH_SOURCE",
9620
+ `${path}.source`,
9621
+ `Boundary choropleth "${overlayId}" must draw from ${tileflowBoundaryFamilies[metadata.family].sourceId}.`
9622
+ );
9623
+ }
9624
+ if (metadata.joinBy === "id" && boundaryAreas.identity.promoteId === "ref") {
9625
+ throw new TileflowOverlayCompilationError(
9626
+ "TF_BOUNDARY_CHOROPLETH_JOIN",
9627
+ path,
9628
+ `Boundary choropleth "${overlayId}" joins by id, but boundaryAreas promotes ref as the feature ID.`,
9629
+ "Join the values by ref, or remove promoteId from boundaryAreas."
9630
+ );
9631
+ }
9632
+ }
9633
+ }
9634
+ return overlays;
9635
+ }
9636
+
9176
9637
  // src/resolved-map-schema.ts
9177
9638
  import { Color as Color3 } from "@maplibre/maplibre-gl-style-spec";
9178
9639
  import { z as z5 } from "zod";
@@ -9310,7 +9771,7 @@ function caseValue(branches, fallback) {
9310
9771
  }
9311
9772
  function step(input, fallback, stops) {
9312
9773
  validateStopCount(stops.length, "expr.step");
9313
- validateStops(stops, "expr.step");
9774
+ validateStops2(stops, "expr.step");
9314
9775
  return make([
9315
9776
  "step",
9316
9777
  node(input),
@@ -9320,7 +9781,7 @@ function step(input, fallback, stops) {
9320
9781
  }
9321
9782
  function interpolate2(interpolation, input, stops) {
9322
9783
  validateStopCount(stops.length, "expr.interpolate");
9323
- validateStops(stops, "expr.interpolate");
9784
+ validateStops2(stops, "expr.interpolate");
9324
9785
  const method = interpolationMethod(interpolation);
9325
9786
  return make([
9326
9787
  "interpolate",
@@ -9347,7 +9808,7 @@ function interpolationMethod(interpolation) {
9347
9808
  }
9348
9809
  }
9349
9810
  }
9350
- function validateStops(stops, api) {
9811
+ function validateStops2(stops, api) {
9351
9812
  let previous = -Infinity;
9352
9813
  for (const [stop] of stops) {
9353
9814
  if (!Number.isFinite(stop) || stop <= previous) {
@@ -10042,11 +10503,11 @@ function physicalLayerIdForSemanticKey(semanticKey) {
10042
10503
  semanticKey
10043
10504
  );
10044
10505
  if (match3) return `tileflow-${match3[1]}-render-${match3[2].replaceAll(".", "-")}`;
10045
- match3 = /^roads\.classes\.([^.]+)\.(tunnel|surface|bridge)\.(shadow|casing|fill|hatch)$/u.exec(
10506
+ match3 = /^roads\.classes\.([^.]+)\.(tunnel|surface|bridge)\.(shadow|casing|fill|hatch|rampFill)$/u.exec(
10046
10507
  semanticKey
10047
10508
  );
10048
10509
  if (match3) return `tileflow-road-${match3[2]}-${match3[1]}-${match3[3]}`;
10049
- match3 = /^roads\.cohorts\.(tunnel|surface|bridge)\.([^.]+)\.(shadow|casing|fill)$/u.exec(
10510
+ match3 = /^roads\.cohorts\.(tunnel|surface|bridge)\.([^.]+)\.(shadow|casing|fill|rampFill)$/u.exec(
10050
10511
  semanticKey
10051
10512
  );
10052
10513
  if (match3) return `tileflow-road-${match3[1]}-highzoom-${match3[2]}-${match3[3]}`;
@@ -10062,6 +10523,8 @@ function physicalLayerIdForSemanticKey(semanticKey) {
10062
10523
  const base = `tileflow-${match3[1]}-${match3[2]}`;
10063
10524
  return match3[3] === "fill" ? base : `${base}-outline`;
10064
10525
  }
10526
+ match3 = /^land\.barriers\.(fence|hedge|wall)\.(area|shadow|line)$/u.exec(semanticKey);
10527
+ if (match3) return `tileflow-barrier-${match3[1]}${match3[2] === "line" ? "" : `-${match3[2]}`}`;
10065
10528
  match3 = /^land\.compatibility\.legacyPark\.(fill|outline)$/u.exec(semanticKey);
10066
10529
  if (match3) {
10067
10530
  return match3[1] === "fill" ? "tileflow-landcover-legacy-park" : "tileflow-landcover-legacy-park-outline";
@@ -10102,10 +10565,14 @@ function physicalLayerIdForSemanticKey(semanticKey) {
10102
10565
  if (match3) return `tileflow-label-road-cohort-${match3[1]}`;
10103
10566
  match3 = /^landforms\.classes\.([^.]+)$/u.exec(semanticKey);
10104
10567
  if (match3) return `tileflow-landform-${match3[1]}`;
10105
- match3 = /^water\.(bathymetryLandforms|bathymetryWaterNames)\.cohort(\d+)$/u.exec(semanticKey);
10568
+ match3 = /^water\.(bathymetryLandforms|bathymetryWaterNames)(?:\.placement(Point|Line|LineCenter))?(?:\.cohort(\d+))?$/u.exec(
10569
+ semanticKey
10570
+ );
10106
10571
  if (match3) {
10107
10572
  const base = match3[1] === "bathymetryLandforms" ? "landforms" : "water-names";
10108
- return `tileflow-bathymetry-${base}-cohort-${match3[2]}`;
10573
+ const placement = match3[2] ? `-placement-${match3[2] === "LineCenter" ? "line-center" : match3[2].toLowerCase()}` : "";
10574
+ const cohort = match3[3] === void 0 ? "" : `-cohort-${match3[3]}`;
10575
+ return `tileflow-bathymetry-${base}${placement}${cohort}`;
10109
10576
  }
10110
10577
  match3 = /^poi\.([A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*)$/u.exec(semanticKey);
10111
10578
  if (match3) return `tileflow-poi-${match3[1].replaceAll(".", "-")}`;
@@ -11624,6 +12091,12 @@ var numberValueSchema = z5.union([
11624
12091
  numberExpressionSchema,
11625
12092
  numberZoomValueSchema
11626
12093
  ]);
12094
+ var nonnegativeNumberLiteralSchema = terrainNumberValueSchema(z5.number().finite().min(0));
12095
+ var nonnegativeNumberValueSchema = z5.union([
12096
+ nonnegativeNumberLiteralSchema,
12097
+ numberExpressionSchema,
12098
+ zoomValueSchemaFor(nonnegativeNumberLiteralSchema)
12099
+ ]);
11627
12100
  var structuralNumberValueSchema = z5.union([
11628
12101
  z5.number().finite(),
11629
12102
  structuralExpressionSchema,
@@ -11974,6 +12447,13 @@ var symbolStyleSchema = z5.object({
11974
12447
  text: textStyleSchema.optional()
11975
12448
  }).strict();
11976
12449
  var markerSymbolStyleSchema = symbolStyleSchema.extend({ marker: circleStyleSchema.optional() }).strict();
12450
+ var bathymetryNameStyleSchema = symbolStyleSchema.extend({
12451
+ placementByGeometry: z5.object({
12452
+ point: z5.literal("point"),
12453
+ line: z5.enum(["line", "line-center", "point"]),
12454
+ polygon: z5.enum(["line", "line-center", "point"])
12455
+ }).strict().optional()
12456
+ }).strict();
11977
12457
  var poiCategoryStyleSchema = markerSymbolStyleSchema.omit({ priority: true, priorityOrder: true }).strict();
11978
12458
  var renderScalarSchema = z5.union([z5.boolean(), z5.number().finite(), z5.string()]);
11979
12459
  var renderTargetSchema = z5.string().regex(tileflowSemanticTargetPattern, "Expected a semantic target");
@@ -12165,9 +12645,19 @@ var roadTreatmentStyleSchema = z5.object({
12165
12645
  tunnel: roadTreatmentLayerStyleSchema.optional(),
12166
12646
  widthScale: themeNumberValueSchema.optional()
12167
12647
  }).strict();
12648
+ var barrierStyleSchema = z5.object({
12649
+ area: fillStyleSchema.optional(),
12650
+ shadow: lineStyleSchema.optional(),
12651
+ line: lineStyleSchema.optional()
12652
+ }).strict();
12168
12653
  var landModuleSchema = z5.object({
12169
12654
  type: z5.literal("land"),
12170
12655
  background: backgroundStyleSchema.optional(),
12656
+ barriers: z5.object({
12657
+ fence: barrierStyleSchema.optional(),
12658
+ hedge: barrierStyleSchema.optional(),
12659
+ wall: barrierStyleSchema.optional()
12660
+ }).strict().optional(),
12171
12661
  enabled: z5.boolean().optional(),
12172
12662
  globalLandcover: fillStyleSchema.optional(),
12173
12663
  landcover: z5.object({
@@ -12207,8 +12697,8 @@ var waterModuleSchema = z5.object({
12207
12697
  bathymetry: fillStyleSchema.optional(),
12208
12698
  bathymetryContours: lineStyleSchema.optional(),
12209
12699
  bathymetryLabels: symbolStyleSchema.optional(),
12210
- bathymetryLandforms: symbolStyleSchema.optional(),
12211
- bathymetryWaterNames: symbolStyleSchema.optional(),
12700
+ bathymetryLandforms: bathymetryNameStyleSchema.optional(),
12701
+ bathymetryWaterNames: bathymetryNameStyleSchema.optional(),
12212
12702
  bodies: areaStyleSchema.optional(),
12213
12703
  enabled: z5.boolean().optional(),
12214
12704
  intermittent: z5.object({ bodies: areaStyleSchema.optional(), waterways: lineStyleSchema.optional() }).strict().optional(),
@@ -12243,6 +12733,7 @@ var buildingsModuleSchema = z5.object({
12243
12733
  lighting: z5.enum(["style", "sun"]).optional(),
12244
12734
  colorVariation: boundedThemeNumberSchema(0, 1).optional(),
12245
12735
  edges: boundedThemeNumberSchema(0, 1).optional(),
12736
+ edgeRadius: z5.number().min(0).max(1).optional(),
12246
12737
  edgeColor: themeColorValueSchema.optional(),
12247
12738
  nightLight: boundedThemeNumberSchema(0, 2).optional(),
12248
12739
  occlusion: z5.boolean().optional(),
@@ -12369,6 +12860,9 @@ var roadsModuleSchema = z5.object({
12369
12860
  }).strict().optional(),
12370
12861
  oneWayMarkers: z5.boolean().optional(),
12371
12862
  outline: z5.enum(["none", "subtle", "strong"]).optional(),
12863
+ rampOrder: z5.enum(["below-roads", "by-class"]).describe(
12864
+ "Ramp fills draw below receiving roads by default. Use by-class to keep ramp fills in their road class order."
12865
+ ).optional(),
12372
12866
  restrictions: z5.object({
12373
12867
  access: roadTreatmentStyleSchema.optional(),
12374
12868
  bicycle: roadTreatmentStyleSchema.optional(),
@@ -12377,7 +12871,16 @@ var roadsModuleSchema = z5.object({
12377
12871
  toll: roadTreatmentStyleSchema.optional()
12378
12872
  }).strict().optional(),
12379
12873
  renderStack: renderStackSchema.optional(),
12380
- roundabouts: z5.object({ casing: circleStyleSchema.optional(), fill: circleStyleSchema.optional() }).strict().optional(),
12874
+ roundabouts: z5.object({
12875
+ borderWidth: nonnegativeNumberValueSchema.describe(
12876
+ "Extra casing width in pixels, split across both edges. Defaults to the effective road border width."
12877
+ ).optional(),
12878
+ casing: circleStyleSchema.optional(),
12879
+ fallbackWidth: nonnegativeNumberValueSchema.describe(
12880
+ "Road width in pixels used only when metric radii are absent. Defaults to the effective surface fill width."
12881
+ ).optional(),
12882
+ fill: circleStyleSchema.optional()
12883
+ }).strict().optional(),
12381
12884
  serviceTypes: z5.object({
12382
12885
  alley: roadTreatmentStyleSchema.optional(),
12383
12886
  crossover: roadTreatmentStyleSchema.optional(),
@@ -12425,6 +12928,9 @@ var labelsModuleSchema = z5.object({
12425
12928
  language: z5.string().trim().min(1).optional(),
12426
12929
  places: z5.enum(["none", "major", "all"]).optional(),
12427
12930
  roadClasses: z5.array(roadClassSchema).min(1).optional(),
12931
+ roadPlacement: z5.enum(["ground", "foreground"]).describe(
12932
+ "Street names draw in the foreground by default. Ground names draw below buildings, use map-aligned text by default, and yield collisions to foreground symbols. Shields and junction numbers stay in the foreground."
12933
+ ).optional(),
12428
12934
  renderStack: renderStackSchema.optional(),
12429
12935
  roads: z5.enum(["none", "highways", "major", "streets", "all"]).optional(),
12430
12936
  styles: z5.object({
@@ -12522,6 +13028,8 @@ var modulesSchema = z5.object({
12522
13028
  broadleafColors: z5.array(themeColorValueSchema).min(1).max(8).optional(),
12523
13029
  coniferColors: z5.array(themeColorValueSchema).min(1).max(8).optional(),
12524
13030
  palmColors: z5.array(themeColorValueSchema).min(1).max(8).optional(),
13031
+ hedges: z5.boolean().optional(),
13032
+ hedgeColors: z5.array(themeColorValueSchema).min(1).max(8).optional(),
12525
13033
  finish: z5.enum(["faceted", "smooth", "toon"]).optional(),
12526
13034
  edges: boundedThemeNumberSchema(0, 1).optional(),
12527
13035
  edgeColor: themeColorValueSchema.optional(),
@@ -12598,6 +13106,7 @@ var layerBindingsSchema = z5.object({
12598
13106
  roadShield: z5.string().trim().min(1).optional(),
12599
13107
  sidewalk: z5.string().trim().min(1).optional(),
12600
13108
  streetFurniture: z5.string().trim().min(1).optional(),
13109
+ trafficControl: z5.string().trim().min(1).optional(),
12601
13110
  tree: z5.string().trim().min(1).optional(),
12602
13111
  water: z5.string().trim().min(1),
12603
13112
  waterName: z5.string().trim().min(1),
@@ -12633,6 +13142,7 @@ var fieldBindingsSchema = z5.object({
12633
13142
  elevationFeet: z5.string().trim().min(1),
12634
13143
  expressway: z5.string().trim().min(1),
12635
13144
  foot: z5.string().trim().min(1),
13145
+ hedge: z5.string().trim().min(1).optional(),
12636
13146
  height: z5.string().trim().min(1),
12637
13147
  hasParts: z5.string().trim().min(1),
12638
13148
  hide3d: z5.string().trim().min(1),
@@ -12803,6 +13313,14 @@ var marineSchema = z5.union([
12803
13313
  nautical: z5.union([z5.literal(false), marineSourceSchema]).optional()
12804
13314
  }).strict()
12805
13315
  ]);
13316
+ var boundaryAreasSchema = z5.union([
13317
+ z5.literal(false),
13318
+ z5.object({
13319
+ family: z5.enum(["boundaries"]),
13320
+ promoteId: z5.literal("ref").optional(),
13321
+ url: publicVectorUrlSchema.optional()
13322
+ }).strict()
13323
+ ]);
12806
13324
  var directVectorDataSchema = z5.object({
12807
13325
  ...vectorDataShape,
12808
13326
  bounds: z5.tuple([
@@ -13077,10 +13595,32 @@ var terrainSchema = z5.union([
13077
13595
  encoding: z5.enum(["mapbox", "terrarium"]).optional(),
13078
13596
  exaggeration: z5.number().finite().min(0).max(10).optional(),
13079
13597
  hillshade: terrainHillshadeSchema.optional(),
13598
+ tiles: z5.array(
13599
+ z5.string().refine(
13600
+ isSafeTileflowDemUrlTemplate,
13601
+ "Expected an HTTP(S) DEM template containing {z}, {x}, and {y}"
13602
+ )
13603
+ ).min(1).max(8).optional(),
13604
+ tileSize: z5.union([z5.literal(256), z5.literal(512)]).optional(),
13605
+ minZoom: z5.number().int().min(0).max(24).optional(),
13606
+ maxZoom: z5.number().int().min(0).max(24).optional(),
13080
13607
  mode: z5.enum(["none", "hillshade", "3d"]).optional(),
13081
13608
  sourceId: z5.string().trim().min(1).optional(),
13082
13609
  url: z5.string().trim().min(1).optional()
13083
- }).strict()
13610
+ }).strict().superRefine((value, context) => {
13611
+ if (value.tiles && value.url)
13612
+ context.addIssue({
13613
+ code: "custom",
13614
+ message: "Terrain accepts tiles or url, not both",
13615
+ path: ["tiles"]
13616
+ });
13617
+ if ((value.minZoom ?? 0) > (value.maxZoom ?? 24))
13618
+ context.addIssue({
13619
+ code: "custom",
13620
+ message: "Terrain maxZoom must be at least minZoom",
13621
+ path: ["maxZoom"]
13622
+ });
13623
+ })
13084
13624
  ]);
13085
13625
  var atmosphereColorLiteralSchema = z5.string().refine((value) => Color3.parse(value)?.a === 1, "Expected an opaque color");
13086
13626
  var atmosphereColorSchema = z5.union([
@@ -13133,6 +13673,7 @@ var themesSchema = z5.record(tileflowThemeNameSchema, tileflowThemeSchema).super
13133
13673
  var systemThemesSchema = z5.object({ dark: tileflowThemeNameSchema, light: tileflowThemeNameSchema }).strict();
13134
13674
  var resolvedTileflowMapSchema = z5.object({
13135
13675
  atmosphere: atmosphereSchema.optional(),
13676
+ boundaryAreas: boundaryAreasSchema.optional(),
13136
13677
  data: dataSchema.optional(),
13137
13678
  defaultTheme: tileflowThemeNameSchema,
13138
13679
  fonts: fontDirectoriesSchema.optional(),
@@ -13557,7 +14098,10 @@ function resolveRaster(terrain, apiBaseUrl, encoding) {
13557
14098
  ...terrain.attribution ? { attribution: terrain.attribution } : {},
13558
14099
  encoding,
13559
14100
  type: "raster-dem",
13560
- url
14101
+ ...terrain.tiles ? { tiles: [...terrain.tiles] } : { url },
14102
+ ...terrain.minZoom === void 0 ? {} : { minzoom: terrain.minZoom },
14103
+ ...terrain.maxZoom === void 0 ? {} : { maxzoom: terrain.maxZoom },
14104
+ ...terrain.tileSize === void 0 ? {} : { tileSize: terrain.tileSize }
13561
14105
  }
13562
14106
  };
13563
14107
  }
@@ -13645,6 +14189,91 @@ var tileflowCompilationPlannerStages = Object.freeze([
13645
14189
  "lowering"
13646
14190
  ]);
13647
14191
 
14192
+ // src/cartography/design-decisions.ts
14193
+ function describeTileflowDesignDecisions(input) {
14194
+ const { map, modules, theme, buildings: buildings2 } = input;
14195
+ const decisions = input.colors.map(
14196
+ ({ role, source, value }) => ({
14197
+ path: `themes.${theme}.tokens.color.${role}`,
14198
+ source,
14199
+ value,
14200
+ reason: source === "theme" ? "The selected theme defines this role, including any authored token expression." : "The compiler derives this optional role from the selected theme color roles.",
14201
+ override: `Set tokens.color[${JSON.stringify(role)}] in defineTheme(); an explicit role replaces this derivation.`
14202
+ })
14203
+ );
14204
+ const policy = (domain, key, value, reason) => {
14205
+ const path = `modules.${domain}.${key}`;
14206
+ const authored = key.split(".").reduce(
14207
+ (node2, part) => node2 && typeof node2 === "object" ? node2[part] : void 0,
14208
+ map.modules?.[domain]
14209
+ );
14210
+ decisions.push({
14211
+ path,
14212
+ value,
14213
+ source: authored === void 0 ? "default" : "map",
14214
+ reason,
14215
+ override: `Set ${path} with ${domain}(...) or refine(...); reset() restores the SDK default. Render-stack refinements apply after module policies.`
14216
+ });
14217
+ };
14218
+ if (modules.roads.enabled !== false) {
14219
+ const roads2 = resolveRoads(modules.roads);
14220
+ policy(
14221
+ "roads",
14222
+ "rampOrder",
14223
+ roads2.rampOrder,
14224
+ "Below-roads draws ramp fills before receiving carriageways; by-class keeps each fill with its road class."
14225
+ );
14226
+ policy(
14227
+ "roads",
14228
+ "detail",
14229
+ roads2.detail,
14230
+ "Road detail selects the initial classes; explicit class settings and disabled classes refine that selection."
14231
+ );
14232
+ }
14233
+ if (modules.labels.enabled !== false) {
14234
+ const labels2 = resolveLabels(modules.labels);
14235
+ policy(
14236
+ "labels",
14237
+ "roadPlacement",
14238
+ labels2.roadPlacement,
14239
+ "Ground names draw below buildings and yield in collisions to foreground symbols; foreground names draw above buildings."
14240
+ );
14241
+ policy(
14242
+ "labels",
14243
+ "collisionPriority",
14244
+ labels2.collisionPriority,
14245
+ "Navigation prioritizes structural road classes within their drawing stack; it does not decouple collision priority from depth."
14246
+ );
14247
+ }
14248
+ if (buildings2) {
14249
+ policy(
14250
+ "buildings",
14251
+ "threeDimensional.edgeRadius",
14252
+ buildings2.edgeRadius,
14253
+ "Radius in metres rounds building corners; zero retains sharp corners."
14254
+ );
14255
+ policy(
14256
+ "buildings",
14257
+ "threeDimensional.lighting",
14258
+ buildings2.lighting,
14259
+ "Sun lighting shares scene illumination; style lighting uses the alternate building shading mode."
14260
+ );
14261
+ policy(
14262
+ "buildings",
14263
+ "threeDimensional.shadows",
14264
+ buildings2.shadows,
14265
+ "Cast shadows follow scene light; false disables them independently of contact occlusion."
14266
+ );
14267
+ policy(
14268
+ "buildings",
14269
+ "threeDimensional.occlusion",
14270
+ buildings2.occlusion,
14271
+ "Contact occlusion separates buildings from the ground and shades lower walls; false disables it independently."
14272
+ );
14273
+ }
14274
+ return decisions;
14275
+ }
14276
+
13648
14277
  // src/cartography/graph.ts
13649
14278
  function assembleTileflowLayerFamilies(families, extraConstraints = []) {
13650
14279
  const slotOrder = resolveSlotOrder(extraConstraints);
@@ -14127,7 +14756,7 @@ function diagnoseFractionalLayoutZoomSteps(layers) {
14127
14756
  function fractionalZoomSteps(value) {
14128
14757
  if (!Array.isArray(value)) return [];
14129
14758
  const stops = [];
14130
- if (value[0] === "step" && isZoom(value[1])) {
14759
+ if (value[0] === "step" && isZoom2(value[1])) {
14131
14760
  for (let index = 3; index < value.length; index += 2) {
14132
14761
  const stop = value[index];
14133
14762
  if (typeof stop === "number" && !Number.isInteger(stop)) stops.push(stop);
@@ -14136,7 +14765,7 @@ function fractionalZoomSteps(value) {
14136
14765
  for (const entry of value) stops.push(...fractionalZoomSteps(entry));
14137
14766
  return stops;
14138
14767
  }
14139
- function isZoom(value) {
14768
+ function isZoom2(value) {
14140
14769
  return Array.isArray(value) && value.length === 1 && value[0] === "zoom";
14141
14770
  }
14142
14771
 
@@ -14265,6 +14894,7 @@ function canMergeRoadLines(layers) {
14265
14894
  function mergeRoadLineGroup(key, layers) {
14266
14895
  const first = layers[0];
14267
14896
  const conditions = layers.map((layer) => asFilter(layer.selector));
14897
+ if (conditions.some(containsZoom)) return void 0;
14268
14898
  const paint = {};
14269
14899
  const paintKeys = new Set(layers.flatMap((layer) => Object.keys(layer.style.appearance ?? {})));
14270
14900
  for (const key2 of paintKeys) {
@@ -14911,7 +15541,7 @@ function sameLayerSource(left, right) {
14911
15541
  }
14912
15542
  function sameTopLevelProperties(left, right, additionallyIgnored = []) {
14913
15543
  const ignored = new Set(additionallyIgnored);
14914
- const select = (layer) => ({
15544
+ const select2 = (layer) => ({
14915
15545
  annotations: layer.annotations,
14916
15546
  feature: layer.feature,
14917
15547
  properties: layer.properties,
@@ -14920,7 +15550,7 @@ function sameTopLevelProperties(left, right, additionallyIgnored = []) {
14920
15550
  ),
14921
15551
  renderer: layer.renderer
14922
15552
  });
14923
- return allEqual([select(left), select(right)]);
15553
+ return allEqual([select2(left), select2(right)]);
14924
15554
  }
14925
15555
  function isOrderedRenderPass(layer) {
14926
15556
  return layer.origins.some(({ operations }) => operations.some(({ kind }) => kind === "pass"));
@@ -14987,6 +15617,7 @@ function tileflowBuildings3DDesignOf(module, context, layers, buildingModels, ti
14987
15617
  const config = isRecord8(module) ? module : {};
14988
15618
  if (config.enabled === false) return void 0;
14989
15619
  const style = config.threeDimensional ?? {};
15620
+ const defaults4 = tileflowDefaultBuildings3DDesign;
14990
15621
  let color2;
14991
15622
  let verticalGradient = true;
14992
15623
  for (const [id2, property] of colourSources) {
@@ -15009,15 +15640,16 @@ function tileflowBuildings3DDesignOf(module, context, layers, buildingModels, ti
15009
15640
  ...context.data.schema.layers.poi ? { places: context.data.schema.layers.poi } : {}
15010
15641
  },
15011
15642
  color: color2 ?? context.colors.buildings.generic,
15012
- lighting: style.lighting === "sun" ? "sun" : "style",
15643
+ lighting: style.lighting === "style" ? "style" : defaults4.lighting,
15013
15644
  colorVariation: number(style.colorVariation, 0),
15014
15645
  verticalGradient,
15015
15646
  edges: number(style.edges, 0),
15647
+ edgeRadius: number(style.edgeRadius, defaults4.edgeRadius),
15016
15648
  ...typeof edgeColor === "string" ? { edgeColor } : {},
15017
15649
  nightLight: number(style.nightLight, 0),
15018
15650
  windows: style.windows === true,
15019
- shadows: style.shadows === true,
15020
- occlusion: style.occlusion === true,
15651
+ shadows: typeof style.shadows === "boolean" ? style.shadows : defaults4.shadows,
15652
+ occlusion: typeof style.occlusion === "boolean" ? style.occlusion : defaults4.occlusion,
15021
15653
  ...placedModels(style.models, buildingModels),
15022
15654
  ...modelColours(style.modelColors),
15023
15655
  ...landmarksOf(style.landmarks, tileflowLandmarks)
@@ -15064,7 +15696,7 @@ function tileflowStreetLightsDesignOf(module, context) {
15064
15696
  const config = isRecord8(module) ? module : {};
15065
15697
  const layers = context.data.schema.layers;
15066
15698
  const style = config.streetLights ?? {};
15067
- if (config.enabled === false || style.enabled === false || !layers.streetFurniture)
15699
+ if (config.enabled === false || style.enabled !== true || !layers.streetFurniture)
15068
15700
  return void 0;
15069
15701
  const color2 = style.color === void 0 ? void 0 : toMapLibreStyleValue(style.color);
15070
15702
  const intensity = style.intensity === void 0 ? void 0 : toMapLibreStyleValue(style.intensity);
@@ -15137,6 +15769,10 @@ function compileSemanticStyleInternal(input, options, inspect, onReport) {
15137
15769
  const data = runCompilationPhase("data", () => resolveTileflowData(config.data, { apiBaseUrl }));
15138
15770
  const marine = runCompilationPhase("data", () => resolveMarine(config.marine, apiBaseUrl));
15139
15771
  const terrain = runCompilationPhase("data", () => resolveTerrain(config.terrain, apiBaseUrl));
15772
+ const boundaryAreas = runCompilationPhase(
15773
+ "data",
15774
+ () => resolveBoundaryAreas(config.boundaryAreas, apiBaseUrl)
15775
+ );
15140
15776
  const terrainSources = [terrain?.raster, terrain?.contours].filter(
15141
15777
  (source) => source !== void 0
15142
15778
  );
@@ -15177,11 +15813,17 @@ function compileSemanticStyleInternal(input, options, inspect, onReport) {
15177
15813
  }
15178
15814
  marineSourceIds.add(source.sourceId);
15179
15815
  }
15816
+ if (boundaryAreas && (boundaryAreas.sourceId === data.sourceId || terrainSourceIds.has(boundaryAreas.sourceId) || marineSourceIds.has(boundaryAreas.sourceId))) {
15817
+ throw new Error(
15818
+ `Boundaries source ID "${boundaryAreas.sourceId}" conflicts with another Tileflow source.`
15819
+ );
15820
+ }
15180
15821
  });
15181
15822
  const platformSourceIds = [
15182
15823
  data.sourceId,
15183
15824
  ...terrainSources.map(({ sourceId }) => sourceId),
15184
- ...marineSources.map(({ sourceId }) => sourceId)
15825
+ ...marineSources.map(({ sourceId }) => sourceId),
15826
+ ...boundaryAreas ? [boundaryAreas.sourceId] : []
15185
15827
  ];
15186
15828
  const hostedSources = runCompilationPhase(
15187
15829
  "data",
@@ -15209,7 +15851,14 @@ function compileSemanticStyleInternal(input, options, inspect, onReport) {
15209
15851
  "data",
15210
15852
  () => createSemanticDataView(semanticData)
15211
15853
  );
15212
- const colors = runCompilationPhase("theme", () => resolveThemeColors(selected.theme));
15854
+ const colorDecisions = [];
15855
+ const colors = runCompilationPhase(
15856
+ "theme",
15857
+ () => resolveThemeColors(
15858
+ selected.theme,
15859
+ onReport ? (decision) => colorDecisions.push(decision) : void 0
15860
+ )
15861
+ );
15213
15862
  const images = runCompilationPhase("theme", () => resolveThemeImages(selected.theme));
15214
15863
  const typography = resolvedTheme.typography;
15215
15864
  const context = {
@@ -15278,8 +15927,11 @@ function compileSemanticStyleInternal(input, options, inspect, onReport) {
15278
15927
  () => insertTileflowOverlays({
15279
15928
  basemapLayers,
15280
15929
  mapId: config.id,
15281
- overlays: config.overlays,
15282
- sourceIds: Object.keys(hostedSources.definitions)
15930
+ overlays: validateBoundaryChoroplethOverlays(config.id, config.overlays, boundaryAreas),
15931
+ sourceIds: [
15932
+ ...Object.keys(hostedSources.definitions),
15933
+ ...boundaryAreas ? [boundaryAreas.sourceId] : []
15934
+ ]
15283
15935
  })
15284
15936
  );
15285
15937
  const plannedLayers = loweredOverlays.layers;
@@ -15436,6 +16088,7 @@ function compileSemanticStyleInternal(input, options, inspect, onReport) {
15436
16088
  ...marine?.nautical ? { [marine.nautical.sourceId]: marine.nautical.source } : {},
15437
16089
  ...terrain?.raster ? { [terrain.raster.sourceId]: terrain.raster.source } : {},
15438
16090
  ...terrain?.contours ? { [terrain.contours.sourceId]: terrain.contours.source } : {},
16091
+ ...boundaryAreas ? { [boundaryAreas.sourceId]: boundaryAreas.source } : {},
15439
16092
  ...hostedSources.definitions
15440
16093
  };
15441
16094
  const sourceIdentities = {
@@ -15443,6 +16096,7 @@ function compileSemanticStyleInternal(input, options, inspect, onReport) {
15443
16096
  ...marine?.bathymetry?.vector ? { [marine.bathymetry.vector.sourceId]: marine.bathymetry.vector.identity } : {},
15444
16097
  ...marine?.bathymetry?.relief ? { [marine.bathymetry.relief.sourceId]: marine.bathymetry.relief.identity } : {},
15445
16098
  ...marine?.nautical ? { [marine.nautical.sourceId]: marine.nautical.identity } : {},
16099
+ ...boundaryAreas ? { [boundaryAreas.sourceId]: boundaryAreas.identity } : {},
15446
16100
  ...hostedSources.identities
15447
16101
  };
15448
16102
  const sourceRequirements = runCompilationPhase(
@@ -15527,6 +16181,15 @@ function compileSemanticStyleInternal(input, options, inspect, onReport) {
15527
16181
  };
15528
16182
  const report = {
15529
16183
  ...partialReport,
16184
+ ...onReport ? {
16185
+ decisions: describeTileflowDesignDecisions({
16186
+ map: input,
16187
+ modules,
16188
+ theme: selected.name,
16189
+ colors: colorDecisions,
16190
+ ...buildings3D ? { buildings: buildings3D } : {}
16191
+ })
16192
+ } : {},
15530
16193
  requirements: sourceRequirements
15531
16194
  };
15532
16195
  onReport?.(report);
@@ -16330,29 +16993,6 @@ function checkShotTiming(shot, context) {
16330
16993
  }
16331
16994
  }
16332
16995
 
16333
- // src/maps/operations.ts
16334
- var resetValue = deepFreeze({ $tileflow: "reset" });
16335
- var disableValue = deepFreeze({ op: "disable" });
16336
- function isTileflowReset(value) {
16337
- if (!isPlainRecord3(value)) return false;
16338
- return value.$tileflow === "reset" && Object.keys(value).length === 1;
16339
- }
16340
- function isTileflowModuleOperation(value) {
16341
- if (!isPlainRecord3(value) || typeof value.op !== "string") return false;
16342
- return value.op === "disable" || value.op === "refine";
16343
- }
16344
- function deepFreeze(value) {
16345
- if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
16346
- Object.freeze(value);
16347
- for (const child of Object.values(value)) deepFreeze(child);
16348
- return value;
16349
- }
16350
- function isPlainRecord3(value) {
16351
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
16352
- const prototype = Object.getPrototypeOf(value);
16353
- return prototype === Object.prototype || prototype === null;
16354
- }
16355
-
16356
16996
  // src/maps/resolve.ts
16357
16997
  var tileflowMapDefaultMaxDepth = 64;
16358
16998
  var TileflowMapResolutionError = class extends Error {
@@ -16369,6 +17009,7 @@ var TileflowMapResolutionError = class extends Error {
16369
17009
  };
16370
17010
  var tileflowMapMergeStrategies = {
16371
17011
  atmosphere: "atomic",
17012
+ boundaryAreas: "atomic",
16372
17013
  data: "atomic",
16373
17014
  defaultTheme: "atomic",
16374
17015
  extends: "lineage",