@tileflow/core 0.1.0-alpha.14 → 0.1.0-alpha.16

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/index.js CHANGED
@@ -115,7 +115,11 @@ var themePresets = {
115
115
  var defaultTypography = {
116
116
  font: "Noto Sans",
117
117
  fontFamily: "Noto Sans",
118
- weight: "regular"
118
+ weight: "regular",
119
+ places: { font: "Noto Sans", fontFamily: "Noto Sans", weight: "regular" },
120
+ roads: { font: "Noto Sans", fontFamily: "Noto Sans", weight: "regular" },
121
+ water: { font: "Noto Sans", fontFamily: "Noto Sans", weight: "regular" },
122
+ poi: { font: "Noto Sans", fontFamily: "Noto Sans", weight: "regular" }
119
123
  };
120
124
  var fontWeightLabels = {
121
125
  regular: "Regular",
@@ -219,6 +223,10 @@ function resolveColors(overrides = {}, defaults = defaultColors, moduleOverrides
219
223
  const buildingDefaults = {
220
224
  extrusion: mix(base.building, base.text, 0.08),
221
225
  fill: base.building,
226
+ highRise: base.building,
227
+ highRiseOutline: mix(base.building, base.text, 0.2),
228
+ lowRise: mix(base.building, base.land, 0.55),
229
+ lowRiseOutline: mix(base.building, base.land, 0.2),
222
230
  outline: mix(base.building, base.text, 0.2)
223
231
  };
224
232
  const boundaryDefaults = {
@@ -250,15 +258,35 @@ function resolveColors(overrides = {}, defaults = defaultColors, moduleOverrides
250
258
  };
251
259
  }
252
260
  function resolveTypography(overrides = {}, defaults = defaultTypography) {
253
- const font = overrides.font ?? overrides.fontFamily ?? defaults.font;
261
+ const base = resolveTypographyStyle(overrides, defaults);
262
+ return {
263
+ ...base,
264
+ places: resolveTypographyDomain(overrides.places, overrides, defaults.places, base),
265
+ roads: resolveTypographyDomain(overrides.roads, overrides, defaults.roads, base),
266
+ water: resolveTypographyDomain(overrides.water, overrides, defaults.water, base),
267
+ poi: resolveTypographyDomain(overrides.poi, overrides, defaults.poi, base)
268
+ };
269
+ }
270
+ function textFont(typography, domain) {
271
+ const style = domain ? typography[domain] : typography;
272
+ return [`${style.font} ${fontWeightLabels[style.weight]}`];
273
+ }
274
+ function resolveTypographyStyle(overrides, defaults) {
275
+ const font = overrides?.font ?? overrides?.fontFamily ?? defaults.font;
254
276
  return {
255
277
  font,
256
278
  fontFamily: font,
257
- weight: overrides.weight ?? defaults.weight
279
+ weight: overrides?.weight ?? defaults.weight
258
280
  };
259
281
  }
260
- function textFont(typography) {
261
- return [`${typography.font} ${fontWeightLabels[typography.weight]}`];
282
+ function resolveTypographyDomain(domainOverrides, globalOverrides, defaults, resolvedGlobal) {
283
+ const globalFontOverridden = Boolean(globalOverrides.font || globalOverrides.fontFamily);
284
+ const font = domainOverrides?.font ?? domainOverrides?.fontFamily ?? (globalFontOverridden ? resolvedGlobal.font : defaults.font);
285
+ return {
286
+ font,
287
+ fontFamily: font,
288
+ weight: domainOverrides?.weight ?? globalOverrides.weight ?? defaults.weight
289
+ };
262
290
  }
263
291
  function resolveLegacyTheme(theme) {
264
292
  const preset = themePresets[theme];
@@ -342,13 +370,17 @@ function isLegacyTheme(value) {
342
370
  function resolveThemeTypography(theme) {
343
371
  const font = theme.typography?.font ?? theme.typography?.fontFamily ?? theme.fonts?.labels ?? theme.fonts?.body;
344
372
  return {
373
+ ...theme.typography?.places ? { places: theme.typography.places } : {},
374
+ ...theme.typography?.poi ? { poi: theme.typography.poi } : {},
375
+ ...theme.typography?.roads ? { roads: theme.typography.roads } : {},
376
+ ...theme.typography?.water ? { water: theme.typography.water } : {},
345
377
  ...theme.typography?.weight ? { weight: theme.typography.weight } : {},
346
378
  ...font ? { font } : {}
347
379
  };
348
380
  }
349
381
  function hasTypographyConfig(theme) {
350
382
  return Boolean(
351
- theme.typography?.font || theme.typography?.fontFamily || theme.typography?.weight || theme.fonts?.labels || theme.fonts?.body
383
+ theme.typography?.font || theme.typography?.fontFamily || theme.typography?.weight || theme.typography?.places || theme.typography?.poi || theme.typography?.roads || theme.typography?.water || theme.fonts?.labels || theme.fonts?.body
352
384
  );
353
385
  }
354
386
  function normalizeColorAliases(colors) {
@@ -517,9 +549,21 @@ var moduleRoadDefaults = {
517
549
  detail: "streets",
518
550
  extras: { ferry: false, paths: false, rail: false },
519
551
  hierarchy: "clear",
552
+ oneWayMarkers: false,
520
553
  outline: "subtle",
521
554
  weight: "regular"
522
555
  };
556
+ var defaultRoadWidthScale = {
557
+ motorway: 1,
558
+ trunk: 1,
559
+ primary: 1,
560
+ secondary: 1,
561
+ tertiary: 1,
562
+ minor: 1,
563
+ service: 1,
564
+ track: 1,
565
+ path: 1
566
+ };
523
567
  var highwayRoadClasses = ["motorway", "trunk"];
524
568
  var arterialRoadClasses = ["primary", "secondary", "tertiary"];
525
569
  var majorRoadClasses = [...highwayRoadClasses, ...arterialRoadClasses];
@@ -543,8 +587,10 @@ function roads(options = {}) {
543
587
  ...options.detail ? { detail: options.detail } : {},
544
588
  ...options.extras ? { extras: { ...options.extras } } : {},
545
589
  ...options.hierarchy ? { hierarchy: options.hierarchy } : {},
590
+ ...options.oneWayMarkers !== void 0 ? { oneWayMarkers: options.oneWayMarkers } : {},
546
591
  ...options.outline ? { outline: options.outline } : {},
547
- ...options.weight ? { weight: options.weight } : {}
592
+ ...options.weight ? { weight: options.weight } : {},
593
+ ...options.widthScale ? { widthScale: { ...options.widthScale } } : {}
548
594
  };
549
595
  }
550
596
  function resolveRoads(legacyRoads, moduleConfig, fallback) {
@@ -558,8 +604,10 @@ function resolveRoads(legacyRoads, moduleConfig, fallback) {
558
604
  rail: moduleConfig?.extras?.rail ?? defaults.extras.rail
559
605
  },
560
606
  hierarchy: moduleConfig?.hierarchy ?? defaults.hierarchy,
607
+ oneWayMarkers: moduleConfig?.oneWayMarkers ?? defaults.oneWayMarkers,
561
608
  outline: moduleConfig?.outline ?? defaults.outline,
562
- weight: moduleConfig?.weight ?? defaults.weight
609
+ weight: moduleConfig?.weight ?? defaults.weight,
610
+ ...moduleConfig?.widthScale ? { widthScale: { ...defaultRoadWidthScale, ...moduleConfig.widthScale } } : {}
563
611
  };
564
612
  return {
565
613
  ...resolved,
@@ -603,7 +651,7 @@ function roadStyleMetrics(options) {
603
651
  weightScale
604
652
  };
605
653
  }
606
- function roadLayers(source, sourceLayer, colors, options) {
654
+ function roadLayers(source, sourceLayer, colors, options, typography) {
607
655
  if (options.detail === "none") {
608
656
  return [];
609
657
  }
@@ -647,7 +695,8 @@ function roadLayers(source, sourceLayer, colors, options) {
647
695
  [14, 2.2],
648
696
  [16, 4.8]
649
697
  ],
650
- metrics
698
+ metrics,
699
+ options.widthScale
651
700
  )
652
701
  }
653
702
  });
@@ -672,7 +721,8 @@ function roadLayers(source, sourceLayer, colors, options) {
672
721
  [14, 1],
673
722
  [16, 2.4]
674
723
  ],
675
- metrics
724
+ metrics,
725
+ options.widthScale
676
726
  )
677
727
  }
678
728
  });
@@ -693,7 +743,8 @@ function roadLayers(source, sourceLayer, colors, options) {
693
743
  [12, 1.8],
694
744
  [16, 5.4]
695
745
  ],
696
- metrics
746
+ metrics,
747
+ options.widthScale
697
748
  )
698
749
  }
699
750
  });
@@ -739,7 +790,7 @@ function roadLayers(source, sourceLayer, colors, options) {
739
790
  [14, 0.8],
740
791
  [16, 2.2]
741
792
  ],
742
- metrics.weightScale * metrics.minorWidthScale
793
+ metrics.weightScale * metrics.minorWidthScale * (options.widthScale?.path ?? 1)
743
794
  )
744
795
  }
745
796
  });
@@ -782,7 +833,8 @@ function roadLayers(source, sourceLayer, colors, options) {
782
833
  [14, 0.6],
783
834
  [16, 2.2]
784
835
  ],
785
- metrics.weightScale * metrics.minorWidthScale
836
+ metrics.weightScale * metrics.minorWidthScale,
837
+ options.widthScale
786
838
  )
787
839
  }
788
840
  });
@@ -803,7 +855,8 @@ function roadLayers(source, sourceLayer, colors, options) {
803
855
  [12, 0.8],
804
856
  [16, 3]
805
857
  ],
806
- metrics.weightScale * metrics.minorWidthScale
858
+ metrics.weightScale * metrics.minorWidthScale,
859
+ options.widthScale
807
860
  )
808
861
  }
809
862
  });
@@ -825,7 +878,8 @@ function roadLayers(source, sourceLayer, colors, options) {
825
878
  [12, 1.2],
826
879
  [16, 4]
827
880
  ],
828
- metrics.weightScale * metrics.majorWidthScale
881
+ metrics.weightScale * metrics.majorWidthScale,
882
+ options.widthScale
829
883
  )
830
884
  }
831
885
  },
@@ -845,11 +899,15 @@ function roadLayers(source, sourceLayer, colors, options) {
845
899
  [14, 1.6],
846
900
  [16, 4.4]
847
901
  ],
848
- metrics
902
+ metrics,
903
+ options.widthScale
849
904
  )
850
905
  }
851
906
  }
852
907
  );
908
+ if (options.oneWayMarkers) {
909
+ layers.push(oneWayMarkerLayer(source, sourceLayer, colors, typography));
910
+ }
853
911
  return layers;
854
912
  }
855
913
  function roadDefaultsFromLegacyMode(mode) {
@@ -858,6 +916,7 @@ function roadDefaultsFromLegacyMode(mode) {
858
916
  detail: "none",
859
917
  extras: { ferry: false, paths: false, rail: false },
860
918
  hierarchy: "clear",
919
+ oneWayMarkers: false,
861
920
  outline: "subtle",
862
921
  weight: "regular"
863
922
  };
@@ -867,6 +926,7 @@ function roadDefaultsFromLegacyMode(mode) {
867
926
  detail: "streets",
868
927
  extras: { ferry: false, paths: false, rail: false },
869
928
  hierarchy: "subtle",
929
+ oneWayMarkers: false,
870
930
  outline: "subtle",
871
931
  weight: "thin"
872
932
  };
@@ -876,6 +936,7 @@ function roadDefaultsFromLegacyMode(mode) {
876
936
  detail: "all",
877
937
  extras: { ferry: true, paths: true, rail: true },
878
938
  hierarchy: "clear",
939
+ oneWayMarkers: false,
879
940
  outline: "strong",
880
941
  weight: "regular"
881
942
  };
@@ -884,6 +945,7 @@ function roadDefaultsFromLegacyMode(mode) {
884
945
  detail: "streets",
885
946
  extras: { ferry: true, paths: true, rail: true },
886
947
  hierarchy: "clear",
948
+ oneWayMarkers: false,
887
949
  outline: "subtle",
888
950
  weight: "regular"
889
951
  };
@@ -898,33 +960,101 @@ function classFilter(classes) {
898
960
  return ["match", ["get", "class"], classes, true, false];
899
961
  }
900
962
  function roadOpacityExpression(majorOpacity, minorOpacity) {
963
+ return [
964
+ "step",
965
+ ["zoom"],
966
+ roadOpacityAtZoom(majorOpacity, minorOpacity, 0, 0),
967
+ 12,
968
+ roadOpacityAtZoom(majorOpacity, minorOpacity, minorOpacity, 0),
969
+ 14,
970
+ roadOpacityAtZoom(majorOpacity, minorOpacity, minorOpacity, minorOpacity)
971
+ ];
972
+ }
973
+ function roadOpacityAtZoom(majorOpacity, minorOpacity, streetOpacity, serviceOpacity) {
901
974
  return [
902
975
  "case",
903
976
  classFilter(majorRoadClasses),
904
977
  majorOpacity,
905
978
  classFilter(streetRoadClasses),
906
- ["step", ["zoom"], 0, 12, minorOpacity],
979
+ streetOpacity,
907
980
  classFilter(serviceRoadClasses),
908
- ["step", ["zoom"], 0, 14, minorOpacity],
981
+ serviceOpacity,
909
982
  minorOpacity
910
983
  ];
911
984
  }
912
- function roadWidthExpression(stops, metrics) {
985
+ function roadWidthExpression(stops, metrics, widthScale) {
913
986
  return [
914
- "case",
915
- classFilter(majorRoadClasses),
916
- zoomWidth(stops, metrics.weightScale * metrics.majorWidthScale),
917
- zoomWidth(stops, metrics.weightScale * metrics.minorWidthScale)
987
+ "interpolate",
988
+ ["linear"],
989
+ ["zoom"],
990
+ ...stops.flatMap(([zoom, width]) => [
991
+ zoom,
992
+ widthScale ? [
993
+ "*",
994
+ [
995
+ "case",
996
+ classFilter(majorRoadClasses),
997
+ width * metrics.weightScale * metrics.majorWidthScale,
998
+ width * metrics.weightScale * metrics.minorWidthScale
999
+ ],
1000
+ roadClassWidthScaleExpression(widthScale)
1001
+ ] : [
1002
+ "case",
1003
+ classFilter(majorRoadClasses),
1004
+ width * metrics.weightScale * metrics.majorWidthScale,
1005
+ width * metrics.weightScale * metrics.minorWidthScale
1006
+ ]
1007
+ ])
918
1008
  ];
919
1009
  }
920
- function zoomWidth(stops, scale) {
1010
+ function zoomWidth(stops, scale, widthScale) {
921
1011
  return [
922
1012
  "interpolate",
923
1013
  ["linear"],
924
1014
  ["zoom"],
925
- ...stops.flatMap(([zoom, width]) => [zoom, width * scale])
1015
+ ...stops.flatMap(([zoom, width]) => [
1016
+ zoom,
1017
+ widthScale ? ["*", width * scale, roadClassWidthScaleExpression(widthScale)] : width * scale
1018
+ ])
1019
+ ];
1020
+ }
1021
+ function roadClassWidthScaleExpression(widthScale) {
1022
+ return [
1023
+ "match",
1024
+ ["get", "class"],
1025
+ ...Object.entries(widthScale).flatMap(([roadClass, scale]) => [roadClass, scale]),
1026
+ 1
926
1027
  ];
927
1028
  }
1029
+ function oneWayMarkerLayer(source, sourceLayer, colors, typography) {
1030
+ return {
1031
+ id: "road-oneway-markers",
1032
+ type: "symbol",
1033
+ source,
1034
+ "source-layer": sourceLayer,
1035
+ minzoom: 15,
1036
+ filter: [
1037
+ "any",
1038
+ ["==", ["get", "oneway"], 1],
1039
+ ["==", ["get", "oneway"], "1"],
1040
+ ["==", ["get", "oneway"], true]
1041
+ ],
1042
+ layout: {
1043
+ "symbol-placement": "line",
1044
+ "symbol-spacing": 88,
1045
+ "text-field": "\u203A",
1046
+ ...typography ? { "text-font": textFont(typography, "roads") } : {},
1047
+ "text-keep-upright": false,
1048
+ "text-rotation-alignment": "map",
1049
+ "text-size": 15
1050
+ },
1051
+ paint: {
1052
+ "text-color": colors.labels.road,
1053
+ "text-halo-color": colors.labels.halo,
1054
+ "text-halo-width": 0.5
1055
+ }
1056
+ };
1057
+ }
928
1058
  function roadClassColorExpression(colors) {
929
1059
  return [
930
1060
  "match",
@@ -966,6 +1096,7 @@ function labels(options = {}) {
966
1096
  type: "labels",
967
1097
  ...options.language ? { language: options.language } : {},
968
1098
  ...options.places ? { places: options.places } : {},
1099
+ ...options.roadClasses ? { roadClasses: [...options.roadClasses] } : {},
969
1100
  ...options.roads ? { roads: options.roads } : {},
970
1101
  ...options.water ? { water: options.water } : {}
971
1102
  };
@@ -976,6 +1107,7 @@ function resolveLabels(legacyLabels, moduleConfig, fallback) {
976
1107
  const resolved = {
977
1108
  language: moduleConfig?.language ?? defaults.language,
978
1109
  places: moduleConfig?.places ?? defaults.places,
1110
+ ...moduleConfig?.roadClasses ? { roadClasses: moduleConfig.roadClasses } : {},
979
1111
  roads: moduleConfig?.roads ?? defaults.roads,
980
1112
  water: moduleConfig?.water ?? defaults.water
981
1113
  };
@@ -1017,7 +1149,7 @@ function placeLabelLayers(source, sourceLayer, colors, labelsConfig, typography)
1017
1149
  filter: labelsConfig.places === "major" ? ["all", ["has", "name"], classFilter2(majorPlaceClasses)] : ["has", "name"],
1018
1150
  layout: {
1019
1151
  "text-field": labelTextField(labelsConfig.language),
1020
- "text-font": textFont(typography),
1152
+ "text-font": textFont(typography, "places"),
1021
1153
  "text-size": ["interpolate", ["linear"], ["zoom"], 3, 10, 8, 13, 14, 17],
1022
1154
  "text-letter-spacing": 0,
1023
1155
  "text-padding": 4
@@ -1031,7 +1163,11 @@ function placeLabelLayers(source, sourceLayer, colors, labelsConfig, typography)
1031
1163
  ];
1032
1164
  }
1033
1165
  function roadLabelLayers(source, sourceLayer, colors, labelsConfig, roadsConfig, typography) {
1034
- const classes = visibleRoadLabelClasses(labelsConfig.roads, roadsConfig);
1166
+ const classes = visibleRoadLabelClasses(
1167
+ labelsConfig.roads,
1168
+ roadsConfig,
1169
+ labelsConfig.roadClasses
1170
+ );
1035
1171
  const majorClasses = classes.filter(isMajorRoadClass);
1036
1172
  const minorClasses = classes.filter((roadClass) => !isMajorRoadClass(roadClass));
1037
1173
  if (classes.length === 0) return [];
@@ -1039,7 +1175,7 @@ function roadLabelLayers(source, sourceLayer, colors, labelsConfig, roadsConfig,
1039
1175
  const sharedLayout = {
1040
1176
  "symbol-placement": "line",
1041
1177
  "text-field": labelTextField(labelsConfig.language),
1042
- "text-font": textFont(typography),
1178
+ "text-font": textFont(typography, "roads"),
1043
1179
  "text-rotation-alignment": "map",
1044
1180
  "text-size": ["interpolate", ["linear"], ["zoom"], 11, 11, 14, 13, 17, 15]
1045
1181
  };
@@ -1061,7 +1197,7 @@ function roadLabelLayers(source, sourceLayer, colors, labelsConfig, roadsConfig,
1061
1197
  "symbol-placement": "line",
1062
1198
  "symbol-spacing": 240,
1063
1199
  "text-field": ["get", "ref"],
1064
- "text-font": textFont(typography),
1200
+ "text-font": textFont(typography, "roads"),
1065
1201
  "text-rotation-alignment": "viewport",
1066
1202
  "text-size": 10
1067
1203
  },
@@ -1113,7 +1249,7 @@ function waterLabelLayers(source, waterNameSourceLayer, waterwaySourceLayer, col
1113
1249
  filter: ["all", ["==", ["geometry-type"], "Point"], waterFilter],
1114
1250
  layout: {
1115
1251
  "text-field": labelTextField(labelsConfig.language),
1116
- "text-font": textFont(typography),
1252
+ "text-font": textFont(typography, "water"),
1117
1253
  "text-size": ["interpolate", ["linear"], ["zoom"], 3, 11, 8, 14]
1118
1254
  },
1119
1255
  paint
@@ -1128,7 +1264,7 @@ function waterLabelLayers(source, waterNameSourceLayer, waterwaySourceLayer, col
1128
1264
  layout: {
1129
1265
  "symbol-placement": "line",
1130
1266
  "text-field": labelTextField(labelsConfig.language),
1131
- "text-font": textFont(typography),
1267
+ "text-font": textFont(typography, "water"),
1132
1268
  "text-size": 13
1133
1269
  },
1134
1270
  paint
@@ -1144,14 +1280,14 @@ function waterLabelLayers(source, waterNameSourceLayer, waterwaySourceLayer, col
1144
1280
  "symbol-placement": "line",
1145
1281
  "symbol-spacing": 320,
1146
1282
  "text-field": labelTextField(labelsConfig.language),
1147
- "text-font": textFont(typography),
1283
+ "text-font": textFont(typography, "water"),
1148
1284
  "text-size": 12
1149
1285
  },
1150
1286
  paint
1151
1287
  }
1152
1288
  ];
1153
1289
  }
1154
- function visibleRoadLabelClasses(detail, roadsConfig) {
1290
+ function visibleRoadLabelClasses(detail, roadsConfig, eligibleClasses) {
1155
1291
  if (detail === "none" || roadsConfig.detail === "none") return [];
1156
1292
  const visibleRoadClasses = [
1157
1293
  ...roadClassesForDetail(roadsConfig.detail),
@@ -1159,7 +1295,10 @@ function visibleRoadLabelClasses(detail, roadsConfig) {
1159
1295
  ];
1160
1296
  const requestedClasses = roadLabelClasses(detail);
1161
1297
  const visible = new Set(visibleRoadClasses);
1162
- return requestedClasses.filter((roadClass) => visible.has(roadClass));
1298
+ const eligible = eligibleClasses ? new Set(eligibleClasses) : void 0;
1299
+ return requestedClasses.filter(
1300
+ (roadClass) => visible.has(roadClass) && (!eligible || eligible.has(roadClass))
1301
+ );
1163
1302
  }
1164
1303
  function roadLabelClasses(detail) {
1165
1304
  return [...roadClassesForDetail(detail), ...detail === "all" ? ["path"] : []];
@@ -1206,20 +1345,33 @@ function poi(options = {}) {
1206
1345
  return {
1207
1346
  type: "poi",
1208
1347
  ...options.categories ? { categories: options.categories } : {},
1348
+ ...options.classMapping ? { classMapping: { ...options.classMapping } } : {},
1349
+ ...options.color ? { color: options.color } : {},
1350
+ ...options.density ? { density: options.density } : {},
1209
1351
  ...options.icons !== void 0 ? { icons: options.icons } : {},
1210
1352
  ...options.labels ? { labels: options.labels } : {},
1211
1353
  ...options.minZoom !== void 0 ? { minZoom: options.minZoom } : {},
1354
+ ...options.placement ? { placement: { ...options.placement } } : {},
1212
1355
  ...options.preset ? { preset: options.preset } : {}
1213
1356
  };
1214
1357
  }
1215
1358
  function resolvePoi(legacyPoi, moduleConfig, fallback) {
1216
1359
  const mode = moduleConfig?.preset ?? legacyPoi ?? fallback;
1360
+ const density = moduleConfig?.density;
1217
1361
  return {
1218
- categories: moduleConfig?.categories,
1362
+ ...moduleConfig?.categories ? { categories: moduleConfig.categories } : {},
1363
+ classMapping: resolvePoiClassMapping(moduleConfig?.classMapping),
1364
+ color: moduleConfig?.color ?? "uniform",
1365
+ ...density ? { density } : {},
1219
1366
  icons: moduleConfig?.icons ?? (mode === "none" ? false : "essential"),
1220
1367
  labels: moduleConfig?.labels ?? poiLabelsFromMode(mode),
1221
- minZoom: moduleConfig?.minZoom ?? defaultPoiMinZoom[mode],
1222
- mode
1368
+ minZoom: moduleConfig?.minZoom ?? defaultPoiMinZoom[mode] + (density === void 0 ? 0 : poiDensityZoomOffset(density)),
1369
+ mode,
1370
+ placement: {
1371
+ coupleIconAndLabel: moduleConfig?.placement?.coupleIconAndLabel ?? false,
1372
+ iconPadding: moduleConfig?.placement?.iconPadding ?? 2,
1373
+ textPadding: moduleConfig?.placement?.textPadding ?? 2
1374
+ }
1223
1375
  };
1224
1376
  }
1225
1377
  function poiLayers(source, sourceLayer, colors, poiConfig, typography, customIconSpriteId2, customIconMapping) {
@@ -1228,21 +1380,29 @@ function poiLayers(source, sourceLayer, colors, poiConfig, typography, customIco
1228
1380
  }
1229
1381
  const layout = {
1230
1382
  "text-field": labelTextField("auto"),
1231
- "text-font": textFont(typography),
1383
+ "text-font": textFont(typography, "poi"),
1232
1384
  "text-size": ["interpolate", ["linear"], ["zoom"], poiConfig.minZoom, 10, 17, 12],
1233
1385
  "text-letter-spacing": 0,
1234
- "text-padding": 2
1386
+ "text-padding": poiConfig.placement.textPadding
1235
1387
  };
1388
+ if (poiConfig.density) {
1389
+ layout["symbol-sort-key"] = poiPriorityExpression(poiConfig.classMapping);
1390
+ }
1236
1391
  if (poiConfig.labels === "none") {
1237
1392
  delete layout["text-field"];
1238
1393
  }
1239
1394
  if (poiConfig.icons) {
1240
- layout["icon-image"] = poiIconImageExpression(customIconSpriteId2, customIconMapping);
1241
- layout["icon-optional"] = true;
1242
- layout["icon-padding"] = 2;
1395
+ layout["icon-image"] = poiIconImageExpression(
1396
+ customIconSpriteId2,
1397
+ customIconMapping,
1398
+ poiConfig.classMapping
1399
+ );
1400
+ layout["icon-optional"] = !poiConfig.placement.coupleIconAndLabel;
1401
+ layout["icon-padding"] = poiConfig.placement.iconPadding;
1243
1402
  layout["icon-size"] = ["interpolate", ["linear"], ["zoom"], poiConfig.minZoom, 0.85, 17, 1];
1244
1403
  layout["text-anchor"] = "top";
1245
1404
  layout["text-offset"] = [0, 0.75];
1405
+ if (poiConfig.placement.coupleIconAndLabel) layout["text-optional"] = false;
1246
1406
  }
1247
1407
  return [
1248
1408
  {
@@ -1251,10 +1411,10 @@ function poiLayers(source, sourceLayer, colors, poiConfig, typography, customIco
1251
1411
  source,
1252
1412
  "source-layer": sourceLayer,
1253
1413
  minzoom: poiConfig.minZoom,
1254
- ...poiConfig.categories ? { filter: poiCategoryFilter(poiConfig.categories) } : {},
1414
+ ...poiFilter(poiConfig) ? { filter: poiFilter(poiConfig) } : {},
1255
1415
  layout,
1256
1416
  paint: {
1257
- "text-color": colors.poi.label,
1417
+ "text-color": poiConfig.color === "category" ? poiCategoryColorExpression(colors, poiConfig.classMapping) : colors.poi.label,
1258
1418
  "text-halo-color": colors.poi.halo ?? alpha(colors.background, 0.78),
1259
1419
  "text-halo-width": 1
1260
1420
  }
@@ -1267,26 +1427,112 @@ function poiLabelsFromMode(mode) {
1267
1427
  if (mode === "full") return "full";
1268
1428
  return "balanced";
1269
1429
  }
1270
- function poiCategoryFilter(categories) {
1271
- return ["in", "class", ...categories.flatMap((category) => poiCategoryClasses(category))];
1430
+ function poiCategoryFilter(categories, classMapping = poiCategoryClassNames) {
1431
+ const classes = categories.flatMap((category) => poiCategoryClasses(category, classMapping));
1432
+ return ["any", ["in", "class", ...classes], ["in", "subclass", ...classes]];
1272
1433
  }
1273
- function poiIconImageExpression(customIconSpriteId2, customIconMapping = {}) {
1274
- const matchParts = [];
1275
- for (const [className, iconName] of Object.entries(poiClassIconNames)) {
1276
- matchParts.push(
1277
- className,
1278
- poiIconImageFallback(iconName, customIconSpriteId2, customIconMapping)
1279
- );
1280
- }
1434
+ function poiIconImageExpression(customIconSpriteId2, customIconMapping = {}, classMapping = poiCategoryClassNames) {
1435
+ const matchParts = Object.keys(classMapping).flatMap((category) => [
1436
+ category,
1437
+ poiIconImageFallback(category, customIconSpriteId2, customIconMapping)
1438
+ ]);
1281
1439
  return [
1282
1440
  "match",
1283
- ["get", "class"],
1441
+ poiCategoryExpression("default", classMapping),
1284
1442
  ...matchParts,
1285
1443
  poiIconImageFallback("default", customIconSpriteId2, customIconMapping)
1286
1444
  ];
1287
1445
  }
1288
- function poiCategoryClasses(category) {
1289
- return poiCategoryClassNames[category] ?? [category];
1446
+ function poiCategoryExpression(fallback = "", classMapping = poiCategoryClassNames) {
1447
+ const categoryMatches = poiCategoryMatchParts(classMapping);
1448
+ return [
1449
+ "match",
1450
+ ["get", "subclass"],
1451
+ ...categoryMatches,
1452
+ fallbackCategoryExpression(fallback, classMapping)
1453
+ ];
1454
+ }
1455
+ function fallbackCategoryExpression(fallback, classMapping) {
1456
+ const categoryMatches = poiCategoryMatchParts(classMapping);
1457
+ return ["match", ["get", "class"], ...categoryMatches, fallback];
1458
+ }
1459
+ function poiCategoryMatchParts(classMapping) {
1460
+ return Object.entries(classMapping).flatMap(([category, classes]) => [classes, category]);
1461
+ }
1462
+ function poiCategoryClasses(category, classMapping = poiCategoryClassNames) {
1463
+ return [...classMapping[category] ?? [category]];
1464
+ }
1465
+ function poiPriorityExpression(classMapping = poiCategoryClassNames) {
1466
+ const categoryPenalty = [
1467
+ "match",
1468
+ poiCategoryExpression("default", classMapping),
1469
+ "transit",
1470
+ 0,
1471
+ "culture",
1472
+ 1,
1473
+ "health",
1474
+ 2,
1475
+ "education",
1476
+ 3,
1477
+ "lodging",
1478
+ 4,
1479
+ "food",
1480
+ 5,
1481
+ "coffee",
1482
+ 6,
1483
+ "services",
1484
+ 7,
1485
+ "shopping",
1486
+ 8,
1487
+ 9
1488
+ ];
1489
+ return ["+", ["*", categoryPenalty, 1e3], ["to-number", ["get", "rank"], 999]];
1490
+ }
1491
+ function poiDensityFilter(density, classMapping = poiCategoryClassNames) {
1492
+ if (density === "dense") return void 0;
1493
+ const cutoffs = poiDensityRankCutoffs[density];
1494
+ return [
1495
+ "any",
1496
+ ...Object.keys(classMapping).map((category) => [
1497
+ "all",
1498
+ poiCategoryFilter([category], classMapping),
1499
+ ["any", ["!has", "rank"], ["<=", "rank", cutoffs[category] ?? cutoffs.default]]
1500
+ ])
1501
+ ];
1502
+ }
1503
+ function poiCategoryColorExpression(colors, classMapping = poiCategoryClassNames) {
1504
+ return [
1505
+ "match",
1506
+ poiCategoryExpression("default", classMapping),
1507
+ ...Object.keys(classMapping).flatMap((category) => [
1508
+ category,
1509
+ colors.poi[category] ?? colors.poi.label
1510
+ ]),
1511
+ colors.poi.label
1512
+ ];
1513
+ }
1514
+ function resolvePoiClassMapping(overrides) {
1515
+ const categories = /* @__PURE__ */ new Set([
1516
+ ...Object.keys(poiCategoryClassNames),
1517
+ ...Object.keys(overrides ?? {})
1518
+ ]);
1519
+ return Object.fromEntries(
1520
+ [...categories].map((category) => [
1521
+ category,
1522
+ [.../* @__PURE__ */ new Set([...poiCategoryClassNames[category] ?? [], ...overrides?.[category] ?? []])]
1523
+ ])
1524
+ );
1525
+ }
1526
+ function poiDensityZoomOffset(density) {
1527
+ if (density === "sparse") return 1;
1528
+ if (density === "dense") return -1;
1529
+ return 0;
1530
+ }
1531
+ function poiFilter(poiConfig) {
1532
+ const categoryFilter = poiConfig.categories ? poiCategoryFilter(poiConfig.categories, poiConfig.classMapping) : void 0;
1533
+ const densityFilter = poiConfig.density ? poiDensityFilter(poiConfig.density, poiConfig.classMapping) : void 0;
1534
+ if (categoryFilter && densityFilter) return ["all", categoryFilter, densityFilter];
1535
+ return categoryFilter ?? densityFilter;
1290
1536
  }
1291
1537
  function poiIconImageFallback(iconName, customIconSpriteId2, customIconMapping) {
1292
1538
  const osmBrightIcon = `${osmBrightIconNameForSemanticIcon(iconName)}_11`;
@@ -1305,55 +1551,50 @@ function osmBrightIconNameForSemanticIcon(iconName) {
1305
1551
  }
1306
1552
  var poiCategoryClassNames = {
1307
1553
  coffee: ["cafe"],
1308
- culture: ["museum", "gallery", "theatre", "cinema", "library", "attraction"],
1554
+ culture: [
1555
+ "museum",
1556
+ "gallery",
1557
+ "art_gallery",
1558
+ "theatre",
1559
+ "cinema",
1560
+ "library",
1561
+ "attraction",
1562
+ "garden",
1563
+ "monument"
1564
+ ],
1309
1565
  education: ["school", "college", "university"],
1310
1566
  food: ["restaurant", "fast_food", "bar", "pub", "biergarten"],
1311
1567
  health: ["hospital", "pharmacy", "doctors", "dentist"],
1312
- lodging: ["hotel", "motel", "hostel", "guest_house"],
1568
+ lodging: ["lodging", "hotel", "motel", "hostel", "guest_house"],
1313
1569
  services: ["bank", "post", "toilet", "police", "fire_station", "town_hall"],
1314
1570
  shopping: ["shop", "mall", "supermarket", "marketplace"],
1315
1571
  transit: ["railway", "station", "bus", "bus_stop", "subway", "tram"]
1316
1572
  };
1317
- var poiClassIconNames = {
1318
- attraction: "culture",
1319
- bank: "services",
1320
- bar: "food",
1321
- biergarten: "food",
1322
- bus: "transit",
1323
- bus_stop: "transit",
1324
- cafe: "coffee",
1325
- cinema: "culture",
1326
- college: "education",
1327
- dentist: "health",
1328
- doctors: "health",
1329
- fast_food: "food",
1330
- fire_station: "services",
1331
- gallery: "culture",
1332
- guest_house: "lodging",
1333
- hospital: "health",
1334
- hostel: "lodging",
1335
- hotel: "lodging",
1336
- library: "culture",
1337
- mall: "shopping",
1338
- marketplace: "shopping",
1339
- motel: "lodging",
1340
- museum: "culture",
1341
- pharmacy: "health",
1342
- police: "services",
1343
- post: "services",
1344
- pub: "food",
1345
- railway: "transit",
1346
- restaurant: "food",
1347
- school: "education",
1348
- shop: "shopping",
1349
- station: "transit",
1350
- subway: "transit",
1351
- supermarket: "shopping",
1352
- theatre: "culture",
1353
- toilet: "services",
1354
- town_hall: "services",
1355
- tram: "transit",
1356
- university: "education"
1573
+ var poiDensityRankCutoffs = {
1574
+ sparse: {
1575
+ coffee: 4,
1576
+ culture: 10,
1577
+ education: 8,
1578
+ food: 6,
1579
+ health: 8,
1580
+ lodging: 8,
1581
+ services: 6,
1582
+ shopping: 4,
1583
+ transit: 14,
1584
+ default: 6
1585
+ },
1586
+ balanced: {
1587
+ coffee: 10,
1588
+ culture: 18,
1589
+ education: 14,
1590
+ food: 14,
1591
+ health: 14,
1592
+ lodging: 16,
1593
+ services: 12,
1594
+ shopping: 10,
1595
+ transit: 24,
1596
+ default: 12
1597
+ }
1357
1598
  };
1358
1599
  var semanticToOsmBrightIconName = {
1359
1600
  coffee: "cafe",
@@ -1378,7 +1619,8 @@ function openMapTilesLayers(source, sourceLayers, colors, options) {
1378
1619
  "background-color": colors.background
1379
1620
  }
1380
1621
  },
1381
- fillLayer("landuse", source, sourceLayers.landuse, colors.landuse.residential, 0.42),
1622
+ semanticLanduseLayer(source, sourceLayers.landuse, colors),
1623
+ semanticLandcoverLayer(source, sourceLayers.landcover, colors),
1382
1624
  fillLayer(
1383
1625
  "parks",
1384
1626
  source,
@@ -1387,8 +1629,15 @@ function openMapTilesLayers(source, sourceLayers, colors, options) {
1387
1629
  parkOpacity(options.density)
1388
1630
  ),
1389
1631
  fillLayer("water", source, sourceLayers.water, colors.hydro.water, 1),
1390
- ...buildingLayers(source, sourceLayers.building, colors, options.density, options.buildings),
1391
- ...roadLayers(source, sourceLayers.road, colors, options.roads),
1632
+ ...buildingLayers(
1633
+ source,
1634
+ sourceLayers.building,
1635
+ colors,
1636
+ options.density,
1637
+ options.buildings,
1638
+ options.buildingStyle
1639
+ ),
1640
+ ...roadLayers(source, sourceLayers.road, colors, options.roads, options.typography),
1392
1641
  ...boundaryLayers(source, sourceLayers.boundary, colors),
1393
1642
  ...labelLayers(source, sourceLayers, colors, options.labels, options.roads, options.typography),
1394
1643
  ...poiLayers(
@@ -1402,6 +1651,61 @@ function openMapTilesLayers(source, sourceLayers, colors, options) {
1402
1651
  )
1403
1652
  ];
1404
1653
  }
1654
+ function semanticLanduseLayer(source, sourceLayer, colors) {
1655
+ return {
1656
+ id: "landuse",
1657
+ type: "fill",
1658
+ source,
1659
+ "source-layer": sourceLayer,
1660
+ paint: {
1661
+ "fill-color": [
1662
+ "match",
1663
+ ["get", "class"],
1664
+ ["commercial", "retail"],
1665
+ colors.landuse.commercial,
1666
+ ["industrial", "railway"],
1667
+ colors.landuse.industrial,
1668
+ ["school", "university", "hospital", "civic"],
1669
+ colors.landuse.civic,
1670
+ "cemetery",
1671
+ colors.landuse.cemetery,
1672
+ colors.landuse.residential
1673
+ ],
1674
+ "fill-opacity": 0.62
1675
+ }
1676
+ };
1677
+ }
1678
+ function semanticLandcoverLayer(source, sourceLayer, colors) {
1679
+ return {
1680
+ id: "landcover",
1681
+ type: "fill",
1682
+ source,
1683
+ "source-layer": sourceLayer,
1684
+ filter: [
1685
+ "match",
1686
+ ["get", "class"],
1687
+ ["grass", "wood", "forest", "scrub", "farmland", "sand", "beach", "ice", "glacier"],
1688
+ true,
1689
+ false
1690
+ ],
1691
+ paint: {
1692
+ "fill-color": [
1693
+ "match",
1694
+ ["get", "class"],
1695
+ ["wood", "forest"],
1696
+ colors.landcover.wood,
1697
+ ["scrub", "farmland"],
1698
+ colors.landcover.protected,
1699
+ ["sand", "beach"],
1700
+ colors.landcover.sand,
1701
+ ["ice", "glacier"],
1702
+ colors.landcover.ice,
1703
+ colors.landcover.grass
1704
+ ],
1705
+ "fill-opacity": 0.88
1706
+ }
1707
+ };
1708
+ }
1405
1709
  function fillLayer(id, source, sourceLayer, color, opacity) {
1406
1710
  return {
1407
1711
  id,
@@ -1414,7 +1718,7 @@ function fillLayer(id, source, sourceLayer, color, opacity) {
1414
1718
  }
1415
1719
  };
1416
1720
  }
1417
- function buildingLayers(source, sourceLayer, colors, density, buildings) {
1721
+ function buildingLayers(source, sourceLayer, colors, density, buildings, buildingStyle) {
1418
1722
  if (buildings === "hidden") {
1419
1723
  return [];
1420
1724
  }
@@ -1481,8 +1785,12 @@ function buildingLayers(source, sourceLayer, colors, density, buildings) {
1481
1785
  "source-layer": sourceLayer,
1482
1786
  minzoom: 13,
1483
1787
  paint: {
1484
- "fill-color": colors.buildings.fill,
1485
- "fill-opacity": density === "dense" ? 0.62 : 0.5
1788
+ "fill-color": buildingStyle ? buildingHeightColorExpression(
1789
+ buildingStyle.heightThreshold ?? 12,
1790
+ colors.buildings.lowRise,
1791
+ colors.buildings.highRise
1792
+ ) : colors.buildings.fill,
1793
+ "fill-opacity": buildingStyle?.fillOpacity ?? (density === "dense" ? 0.62 : 0.5)
1486
1794
  }
1487
1795
  },
1488
1796
  {
@@ -1492,13 +1800,33 @@ function buildingLayers(source, sourceLayer, colors, density, buildings) {
1492
1800
  "source-layer": sourceLayer,
1493
1801
  minzoom: 15,
1494
1802
  paint: {
1495
- "line-color": colors.buildings.outline,
1496
- "line-opacity": 0.24,
1497
- "line-width": 0.6
1803
+ "line-color": buildingStyle ? buildingHeightColorExpression(
1804
+ buildingStyle.heightThreshold ?? 12,
1805
+ colors.buildings.lowRiseOutline,
1806
+ colors.buildings.highRiseOutline
1807
+ ) : colors.buildings.outline,
1808
+ "line-opacity": buildingStyle?.outlineOpacity ?? 0.24,
1809
+ "line-width": buildingStyle?.outlineWidth ?? 0.6
1498
1810
  }
1499
1811
  }
1500
1812
  ];
1501
1813
  }
1814
+ function buildingHeightColorExpression(threshold, lowRise, highRise) {
1815
+ return [
1816
+ "case",
1817
+ [
1818
+ ">=",
1819
+ [
1820
+ "to-number",
1821
+ ["coalesce", ["get", "render_height"], ["get", "height"], ["get", "levels"]],
1822
+ 0
1823
+ ],
1824
+ threshold
1825
+ ],
1826
+ highRise,
1827
+ lowRise
1828
+ ];
1829
+ }
1502
1830
  function boundaryLayers(source, sourceLayer, colors) {
1503
1831
  return [
1504
1832
  {
@@ -1548,12 +1876,15 @@ function applyLayerOverride(layers, layerId, override) {
1548
1876
  return insertLayer(layers, layer, override.before);
1549
1877
  }
1550
1878
  const previousLayer = layers[index];
1879
+ const layout = mergeRecords(previousLayer.layout, override.layout);
1880
+ const paint = mergeRecords(previousLayer.paint, override.paint);
1881
+ const metadata = mergeRecords(previousLayer.metadata, override.metadata);
1551
1882
  const patchedLayer = stripControlFields({
1552
1883
  ...previousLayer,
1553
1884
  ...override,
1554
- layout: mergeRecords(previousLayer.layout, override.layout),
1555
- paint: mergeRecords(previousLayer.paint, override.paint),
1556
- metadata: mergeRecords(previousLayer.metadata, override.metadata)
1885
+ ...layout ? { layout } : {},
1886
+ ...paint ? { paint } : {},
1887
+ ...metadata ? { metadata } : {}
1557
1888
  });
1558
1889
  const nextLayers = [...layers];
1559
1890
  nextLayers[index] = patchedLayer;
@@ -1580,28 +1911,206 @@ function mergeRecords(previous, next) {
1580
1911
  }
1581
1912
  function stripControlFields(layer) {
1582
1913
  const { before, ...styleLayer } = layer;
1583
- return styleLayer;
1914
+ return Object.fromEntries(Object.entries(styleLayer).filter(([, value]) => value !== void 0));
1584
1915
  }
1585
1916
  function isRecord2(value) {
1586
1917
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
1587
1918
  }
1588
1919
 
1589
1920
  // src/schema.ts
1921
+ import { z as z2 } from "zod";
1922
+
1923
+ // src/capture-scene.ts
1590
1924
  import { z } from "zod";
1925
+ var tileflowCaptureSceneSchemaVersion = 1;
1926
+ var tileflowCaptureSceneLimits = Object.freeze({
1927
+ applicationPathLength: 2048,
1928
+ boundsPadding: { maximum: 1024, minimum: 0 },
1929
+ captureIdLength: 64,
1930
+ maximumPhysicalPixels: 16777216,
1931
+ pitch: { maximum: 85, minimum: 0 },
1932
+ selectorLength: 256,
1933
+ viewport: { maximum: 4096, minimum: 64 },
1934
+ zoom: { maximum: 24, minimum: 0 }
1935
+ });
1936
+ var finiteLongitudeSchema = z.number().finite().min(-180).max(180);
1937
+ var finiteLatitudeSchema = z.number().finite().min(-90).max(90);
1938
+ var bearingSchema = z.number().finite().min(-180).max(180).optional();
1939
+ var pitchSchema = z.number().finite().min(tileflowCaptureSceneLimits.pitch.minimum).max(tileflowCaptureSceneLimits.pitch.maximum).optional();
1940
+ var tileflowCaptureCenterCameraSchema = z.object({
1941
+ type: z.literal("center"),
1942
+ center: z.tuple([finiteLongitudeSchema, finiteLatitudeSchema]),
1943
+ zoom: z.number().finite().min(tileflowCaptureSceneLimits.zoom.minimum).max(tileflowCaptureSceneLimits.zoom.maximum),
1944
+ bearing: bearingSchema,
1945
+ pitch: pitchSchema
1946
+ }).strict();
1947
+ var tileflowCaptureBoundsCameraSchema = z.object({
1948
+ type: z.literal("bounds"),
1949
+ bounds: z.tuple([
1950
+ finiteLongitudeSchema,
1951
+ finiteLatitudeSchema,
1952
+ finiteLongitudeSchema,
1953
+ finiteLatitudeSchema
1954
+ ]),
1955
+ padding: z.number().finite().min(tileflowCaptureSceneLimits.boundsPadding.minimum).max(tileflowCaptureSceneLimits.boundsPadding.maximum).optional(),
1956
+ bearing: bearingSchema,
1957
+ pitch: pitchSchema
1958
+ }).strict().superRefine((camera, context) => {
1959
+ const [west, south, east, north] = camera.bounds;
1960
+ if (south >= north) {
1961
+ context.addIssue({
1962
+ code: "custom",
1963
+ message: "Expected south latitude to be less than north latitude",
1964
+ path: ["bounds"]
1965
+ });
1966
+ }
1967
+ if (west === east) {
1968
+ context.addIssue({
1969
+ code: "custom",
1970
+ message: "Expected west and east longitude to define a non-empty span",
1971
+ path: ["bounds"]
1972
+ });
1973
+ }
1974
+ });
1975
+ var tileflowCaptureCameraSchema = z.discriminatedUnion("type", [
1976
+ tileflowCaptureCenterCameraSchema,
1977
+ tileflowCaptureBoundsCameraSchema
1978
+ ]);
1979
+ var tileflowCaptureViewportSchema = z.object({
1980
+ width: z.number().int().min(tileflowCaptureSceneLimits.viewport.minimum).max(tileflowCaptureSceneLimits.viewport.maximum),
1981
+ height: z.number().int().min(tileflowCaptureSceneLimits.viewport.minimum).max(tileflowCaptureSceneLimits.viewport.maximum),
1982
+ dpr: z.union([z.literal(1), z.literal(2)]).optional()
1983
+ }).strict().superRefine((viewport, context) => {
1984
+ const dpr = viewport.dpr ?? 1;
1985
+ const physicalPixels = viewport.width * viewport.height * dpr * dpr;
1986
+ if (physicalPixels > tileflowCaptureSceneLimits.maximumPhysicalPixels) {
1987
+ context.addIssue({
1988
+ code: "custom",
1989
+ message: `Expected at most ${tileflowCaptureSceneLimits.maximumPhysicalPixels} physical pixels`,
1990
+ path: ["dpr"]
1991
+ });
1992
+ }
1993
+ });
1994
+ var tileflowCaptureIdSchema = z.string().trim().min(1).max(tileflowCaptureSceneLimits.captureIdLength).regex(/^[A-Za-z0-9_-]+$/, {
1995
+ message: "Expected letters, numbers, underscores, or hyphens"
1996
+ }).refine((name) => name !== "__proto__", {
1997
+ message: "Expected an identifier that cannot mutate an object prototype"
1998
+ });
1999
+ var tileflowCaptureSceneNameSchema = tileflowCaptureIdSchema.refine(
2000
+ (name) => !/^(?:AUX|CON|NUL|PRN|COM[1-9]|LPT[1-9])$/i.test(name),
2001
+ "Expected a portable scene name that is not a reserved filename or prototype key"
2002
+ );
2003
+ var applicationPathSchema = z.string().min(1).max(tileflowCaptureSceneLimits.applicationPathLength).refine(isPortableApplicationPath, {
2004
+ message: "Expected a root-relative application path without an origin, credentials, fragment, backslash, or control character"
2005
+ });
2006
+ var tileflowCaptureMapTargetSchema = z.object({ kind: z.literal("map") }).strict();
2007
+ var tileflowCaptureApplicationTargetSchema = z.object({
2008
+ kind: z.literal("application"),
2009
+ path: applicationPathSchema,
2010
+ captureId: tileflowCaptureIdSchema.optional(),
2011
+ selector: z.string().trim().min(1).max(tileflowCaptureSceneLimits.selectorLength).optional(),
2012
+ frame: z.enum(["map", "viewport"]).optional()
2013
+ }).strict().superRefine((target, context) => {
2014
+ if (target.captureId && target.selector) {
2015
+ context.addIssue({
2016
+ code: "custom",
2017
+ message: "Expected either captureId or selector, not both",
2018
+ path: ["selector"]
2019
+ });
2020
+ }
2021
+ });
2022
+ var tileflowCaptureTargetSchema = z.discriminatedUnion("kind", [
2023
+ tileflowCaptureMapTargetSchema,
2024
+ tileflowCaptureApplicationTargetSchema
2025
+ ]);
2026
+ var tileflowCaptureSceneSchema = z.object({
2027
+ map: tileflowCaptureIdSchema,
2028
+ camera: tileflowCaptureCameraSchema,
2029
+ viewport: tileflowCaptureViewportSchema,
2030
+ target: tileflowCaptureTargetSchema.optional()
2031
+ }).strict();
2032
+ function normalizeTileflowCaptureScene(input) {
2033
+ const scene = tileflowCaptureSceneSchema.parse(input);
2034
+ const camera = scene.camera.type === "center" ? {
2035
+ type: scene.camera.type,
2036
+ center: scene.camera.center,
2037
+ zoom: scene.camera.zoom,
2038
+ bearing: scene.camera.bearing ?? 0,
2039
+ pitch: scene.camera.pitch ?? 0
2040
+ } : {
2041
+ type: scene.camera.type,
2042
+ bounds: scene.camera.bounds,
2043
+ padding: scene.camera.padding ?? 0,
2044
+ bearing: scene.camera.bearing ?? 0,
2045
+ pitch: scene.camera.pitch ?? 0
2046
+ };
2047
+ const target = scene.target ?? { kind: "map" };
2048
+ return {
2049
+ map: scene.map,
2050
+ camera,
2051
+ viewport: { ...scene.viewport, dpr: scene.viewport.dpr ?? 1 },
2052
+ target: target.kind === "map" ? target : {
2053
+ kind: target.kind,
2054
+ path: target.path,
2055
+ ...target.captureId ? { captureId: target.captureId } : {},
2056
+ ...target.selector ? { selector: target.selector } : {},
2057
+ frame: target.frame ?? "map"
2058
+ }
2059
+ };
2060
+ }
2061
+ function normalizeTileflowCaptureId(value) {
2062
+ return value === void 0 ? void 0 : tileflowCaptureIdSchema.parse(value);
2063
+ }
2064
+ function isPortableApplicationPath(value) {
2065
+ if (value !== value.trim() || !value.startsWith("/") || value.startsWith("//") || value.includes("#") || value.includes("\\") || hasControlCharacter(value)) {
2066
+ return false;
2067
+ }
2068
+ try {
2069
+ const base = new URL("http://tileflow.local");
2070
+ const resolved = new URL(value, base);
2071
+ return resolved.origin === base.origin && resolved.username === "" && resolved.password === "" && resolved.pathname.startsWith("/");
2072
+ } catch {
2073
+ return false;
2074
+ }
2075
+ }
2076
+ function hasControlCharacter(value) {
2077
+ for (let index = 0; index < value.length; index += 1) {
2078
+ const code = value.charCodeAt(index);
2079
+ if (code <= 31 || code === 127) return true;
2080
+ }
2081
+ return false;
2082
+ }
2083
+
2084
+ // src/schema.ts
1591
2085
  var legacyThemeNames = /* @__PURE__ */ new Set(["standard", "light", "dark", "minimal", "osm-bright-2"]);
1592
- var identifierSchema = z.string().trim().regex(/^[A-Za-z0-9_-]+$/, {
2086
+ var prototypeSafeInputSchema = z2.unknown().superRefine((value, context) => {
2087
+ const unsafe = findUnsafeObjectStructure(value);
2088
+ if (unsafe) {
2089
+ context.addIssue({
2090
+ code: "custom",
2091
+ message: unsafe.reason === "key" ? "Expected an object without prototype-mutating keys" : "Expected plain configuration objects without inherited properties",
2092
+ path: unsafe.path
2093
+ });
2094
+ }
2095
+ });
2096
+ var identifierSchema = z2.string().trim().regex(/^[A-Za-z0-9_-]+$/, {
1593
2097
  message: "Expected letters, numbers, underscores, or hyphens"
2098
+ }).refine((value) => value !== "__proto__", {
2099
+ message: "Expected an identifier that cannot mutate an object prototype"
2100
+ });
2101
+ var sourceVersionSchema = z2.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/, {
2102
+ message: "Expected a portable source version"
1594
2103
  });
1595
- var hexColorSchema = z.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
2104
+ var hexColorSchema = z2.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
1596
2105
  message: "Expected a hex color like #F6F7F3 or #F6F7F3FF"
1597
2106
  });
1598
2107
  function colorGroupSchema(keys) {
1599
2108
  const shape = Object.fromEntries(
1600
2109
  keys.map((key) => [key, hexColorSchema.optional()])
1601
2110
  );
1602
- return z.object(shape).strict();
2111
+ return z2.object(shape).strict();
1603
2112
  }
1604
- var colorConfigSchema = z.object({
2113
+ var colorConfigSchema = z2.object({
1605
2114
  accent: hexColorSchema.optional(),
1606
2115
  background: hexColorSchema.optional(),
1607
2116
  boundary: hexColorSchema.optional(),
@@ -1619,7 +2128,15 @@ var colorConfigSchema = z.object({
1619
2128
  textMuted: hexColorSchema.optional(),
1620
2129
  water: hexColorSchema.optional(),
1621
2130
  boundaries: colorGroupSchema(["admin", "disputed", "major", "maritime"]).optional(),
1622
- buildings: colorGroupSchema(["extrusion", "fill", "outline"]).optional(),
2131
+ buildings: colorGroupSchema([
2132
+ "extrusion",
2133
+ "fill",
2134
+ "highRise",
2135
+ "highRiseOutline",
2136
+ "lowRise",
2137
+ "lowRiseOutline",
2138
+ "outline"
2139
+ ]).optional(),
1623
2140
  hydro: colorGroupSchema(["ferry", "label", "water", "waterway"]).optional(),
1624
2141
  labels: colorGroupSchema([
1625
2142
  "country",
@@ -1668,9 +2185,17 @@ var colorConfigSchema = z.object({
1668
2185
  "tunnel"
1669
2186
  ]).optional()
1670
2187
  }).strict();
1671
- var themeModulesSchema = z.object({
2188
+ var themeModulesSchema = z2.object({
1672
2189
  boundaries: colorGroupSchema(["admin", "disputed", "major", "maritime"]).optional(),
1673
- buildings: colorGroupSchema(["extrusion", "fill", "outline"]).optional(),
2190
+ buildings: colorGroupSchema([
2191
+ "extrusion",
2192
+ "fill",
2193
+ "highRise",
2194
+ "highRiseOutline",
2195
+ "lowRise",
2196
+ "lowRiseOutline",
2197
+ "outline"
2198
+ ]).optional(),
1674
2199
  hydro: colorGroupSchema(["ferry", "label", "water", "waterway"]).optional(),
1675
2200
  labels: colorGroupSchema([
1676
2201
  "country",
@@ -1719,170 +2244,228 @@ var themeModulesSchema = z.object({
1719
2244
  "tunnel"
1720
2245
  ]).optional()
1721
2246
  }).strict();
1722
- var styleLayerOverrideSchema = z.object({
1723
- before: z.string().min(1).optional(),
1724
- filter: z.unknown().optional(),
1725
- layout: z.record(z.string(), z.unknown()).optional(),
1726
- maxzoom: z.number().finite().min(0).max(24).optional(),
1727
- metadata: z.record(z.string(), z.unknown()).optional(),
1728
- minzoom: z.number().finite().min(0).max(24).optional(),
1729
- paint: z.record(z.string(), z.unknown()).optional(),
1730
- source: z.string().min(1).optional(),
1731
- "source-layer": z.string().min(1).optional(),
1732
- type: z.string().min(1).optional()
1733
- }).catchall(z.unknown());
1734
- var typographySchema = z.object({
1735
- font: z.string().trim().min(1).optional(),
1736
- fontFamily: z.string().trim().min(1).optional(),
1737
- weight: z.enum(["regular", "medium", "semibold", "bold"]).optional()
2247
+ var styleLayerOverrideSchema = z2.object({
2248
+ before: z2.string().min(1).optional(),
2249
+ filter: z2.unknown().optional(),
2250
+ layout: z2.record(z2.string(), z2.unknown()).optional(),
2251
+ maxzoom: z2.number().finite().min(0).max(24).optional(),
2252
+ metadata: z2.record(z2.string(), z2.unknown()).optional(),
2253
+ minzoom: z2.number().finite().min(0).max(24).optional(),
2254
+ paint: z2.record(z2.string(), z2.unknown()).optional(),
2255
+ source: z2.string().min(1).optional(),
2256
+ "source-layer": z2.string().min(1).optional(),
2257
+ type: z2.string().min(1).optional()
2258
+ }).catchall(z2.unknown());
2259
+ var typographyStyleSchema = z2.object({
2260
+ font: z2.string().trim().min(1).optional(),
2261
+ fontFamily: z2.string().trim().min(1).optional(),
2262
+ weight: z2.enum(["regular", "medium", "semibold", "bold"]).optional()
1738
2263
  }).strict();
1739
- var fontsSchema = z.object({
1740
- body: z.string().trim().min(1).optional(),
1741
- labels: z.string().trim().min(1).optional()
2264
+ var typographySchema = typographyStyleSchema.extend({
2265
+ places: typographyStyleSchema.optional(),
2266
+ poi: typographyStyleSchema.optional(),
2267
+ roads: typographyStyleSchema.optional(),
2268
+ water: typographyStyleSchema.optional()
2269
+ });
2270
+ var fontsSchema = z2.object({
2271
+ body: z2.string().trim().min(1).optional(),
2272
+ labels: z2.string().trim().min(1).optional()
1742
2273
  }).strict();
1743
- var tileflowThemeSchema = z.object({
2274
+ var tileflowThemeSchema = z2.object({
1744
2275
  colors: colorConfigSchema.optional(),
1745
- extends: z.string().trim().min(1).optional(),
2276
+ extends: z2.string().trim().min(1).optional(),
1746
2277
  fonts: fontsSchema.optional(),
1747
- layers: z.record(z.string(), styleLayerOverrideSchema).optional(),
1748
- mode: z.enum(["light", "dark"]).optional(),
2278
+ layers: z2.record(z2.string(), styleLayerOverrideSchema).optional(),
2279
+ mode: z2.enum(["light", "dark"]).optional(),
1749
2280
  modules: themeModulesSchema.optional(),
1750
2281
  typography: typographySchema.optional()
1751
2282
  }).strict();
1752
2283
  var themeConfigSchema = tileflowThemeSchema;
1753
- var sourceLayersSchema = z.object({
1754
- landuse: z.string().optional(),
1755
- park: z.string().optional(),
1756
- water: z.string().optional(),
1757
- waterName: z.string().optional(),
1758
- waterway: z.string().optional(),
1759
- road: z.string().optional(),
1760
- roadName: z.string().optional(),
1761
- building: z.string().optional(),
1762
- boundary: z.string().optional(),
1763
- place: z.string().optional(),
1764
- poi: z.string().optional()
2284
+ var sourceLayersSchema = z2.object({
2285
+ landcover: z2.string().optional(),
2286
+ landuse: z2.string().optional(),
2287
+ park: z2.string().optional(),
2288
+ water: z2.string().optional(),
2289
+ waterName: z2.string().optional(),
2290
+ waterway: z2.string().optional(),
2291
+ road: z2.string().optional(),
2292
+ roadName: z2.string().optional(),
2293
+ building: z2.string().optional(),
2294
+ boundary: z2.string().optional(),
2295
+ place: z2.string().optional(),
2296
+ poi: z2.string().optional()
1765
2297
  }).strict();
1766
- var basemapSchema = z.object({
1767
- type: z.literal("osm"),
1768
- attribution: z.string().optional(),
1769
- glyphs: z.string().min(1).optional(),
1770
- sourceId: z.string().min(1).optional(),
2298
+ var basemapSchema = z2.object({
2299
+ type: z2.literal("osm"),
2300
+ attribution: z2.string().optional(),
2301
+ glyphs: z2.string().min(1).optional(),
2302
+ sourceId: z2.string().min(1).optional(),
1771
2303
  sourceLayers: sourceLayersSchema.optional(),
1772
- sprite: z.string().min(1).optional(),
1773
- tileset: z.string().trim().min(1).optional(),
1774
- url: z.url().optional()
2304
+ sprite: z2.string().min(1).optional(),
2305
+ tileset: z2.string().trim().min(1).optional(),
2306
+ url: z2.url().optional(),
2307
+ version: sourceVersionSchema.optional()
1775
2308
  }).strict();
1776
- var roadDetailSchema = z.enum(["none", "highways", "major", "streets", "all"]);
1777
- var roadsModuleSchema = z.object({
1778
- type: z.literal("roads"),
2309
+ var roadDetailSchema = z2.enum(["none", "highways", "major", "streets", "all"]);
2310
+ var roadClassSchema = z2.enum([
2311
+ "motorway",
2312
+ "trunk",
2313
+ "primary",
2314
+ "secondary",
2315
+ "tertiary",
2316
+ "minor",
2317
+ "service",
2318
+ "track",
2319
+ "path"
2320
+ ]);
2321
+ var roadsModuleSchema = z2.object({
2322
+ type: z2.literal("roads"),
1779
2323
  detail: roadDetailSchema.optional(),
1780
- extras: z.object({
1781
- ferry: z.boolean().optional(),
1782
- paths: z.boolean().optional(),
1783
- rail: z.boolean().optional()
2324
+ extras: z2.object({
2325
+ ferry: z2.boolean().optional(),
2326
+ paths: z2.boolean().optional(),
2327
+ rail: z2.boolean().optional()
1784
2328
  }).strict().optional(),
1785
- hierarchy: z.enum(["subtle", "clear", "strong"]).optional(),
1786
- outline: z.enum(["none", "subtle", "strong"]).optional(),
1787
- weight: z.enum(["thin", "regular", "bold"]).optional()
2329
+ hierarchy: z2.enum(["subtle", "clear", "strong"]).optional(),
2330
+ oneWayMarkers: z2.boolean().optional(),
2331
+ outline: z2.enum(["none", "subtle", "strong"]).optional(),
2332
+ weight: z2.enum(["thin", "regular", "bold"]).optional(),
2333
+ widthScale: z2.partialRecord(roadClassSchema, z2.number().finite().min(0.1).max(8)).optional()
1788
2334
  }).strict();
1789
- var labelDetailSchema = z.enum(["none", "major", "all"]);
1790
- var labelsModuleSchema = z.object({
1791
- type: z.literal("labels"),
1792
- language: z.string().trim().min(1).optional(),
2335
+ var labelDetailSchema = z2.enum(["none", "major", "all"]);
2336
+ var labelsModuleSchema = z2.object({
2337
+ type: z2.literal("labels"),
2338
+ language: z2.string().trim().min(1).optional(),
1793
2339
  places: labelDetailSchema.optional(),
2340
+ roadClasses: z2.array(roadClassSchema).min(1).max(9).optional(),
1794
2341
  roads: roadDetailSchema.optional(),
1795
2342
  water: labelDetailSchema.optional()
1796
2343
  }).strict();
1797
- var poiModuleSchema = z.object({
1798
- type: z.literal("poi"),
1799
- categories: z.array(identifierSchema).optional(),
1800
- icons: z.union([z.boolean(), z.enum(["essential", "full"])]).optional(),
1801
- labels: z.enum(["none", "minimal", "balanced", "full"]).optional(),
1802
- minZoom: z.number().finite().min(0).max(24).optional(),
1803
- preset: z.enum(["none", "minimal", "balanced", "full"]).optional()
2344
+ var poiModuleSchema = z2.object({
2345
+ type: z2.literal("poi"),
2346
+ categories: z2.array(identifierSchema).optional(),
2347
+ classMapping: z2.record(identifierSchema, z2.array(identifierSchema).min(1).max(64)).optional(),
2348
+ color: z2.enum(["uniform", "category"]).optional(),
2349
+ density: z2.enum(["sparse", "balanced", "dense"]).optional(),
2350
+ icons: z2.union([z2.boolean(), z2.enum(["essential", "full"])]).optional(),
2351
+ labels: z2.enum(["none", "minimal", "balanced", "full"]).optional(),
2352
+ minZoom: z2.number().finite().min(0).max(24).optional(),
2353
+ placement: z2.object({
2354
+ coupleIconAndLabel: z2.boolean().optional(),
2355
+ iconPadding: z2.number().finite().min(0).max(64).optional(),
2356
+ textPadding: z2.number().finite().min(0).max(64).optional()
2357
+ }).strict().optional(),
2358
+ preset: z2.enum(["none", "minimal", "balanced", "full"]).optional()
1804
2359
  }).strict();
1805
- var styleOverrideModuleSchema = z.object({
1806
- type: z.literal("styleOverride"),
1807
- layers: z.record(z.string(), styleLayerOverrideSchema).optional(),
1808
- removeLayers: z.array(z.string().min(1)).optional()
2360
+ var styleOverrideModuleSchema = z2.object({
2361
+ type: z2.literal("styleOverride"),
2362
+ layers: z2.record(z2.string(), styleLayerOverrideSchema).optional(),
2363
+ removeLayers: z2.array(z2.string().min(1)).optional()
1809
2364
  }).strict();
1810
- var moduleSchema = z.discriminatedUnion("type", [
2365
+ var moduleSchema = z2.discriminatedUnion("type", [
1811
2366
  labelsModuleSchema,
1812
2367
  poiModuleSchema,
1813
2368
  roadsModuleSchema,
1814
2369
  styleOverrideModuleSchema
1815
2370
  ]);
1816
- var iconSetSchema = z.union([
1817
- z.string().trim().min(1),
1818
- z.object({
1819
- extends: z.string().trim().min(1).optional(),
1820
- mapping: z.record(identifierSchema, z.string().trim().min(1)).optional(),
1821
- source: z.string().trim().min(1).optional(),
1822
- sprite: z.string().trim().min(1).optional()
2371
+ var iconSetSchema = z2.union([
2372
+ z2.string().trim().min(1),
2373
+ z2.object({
2374
+ extends: z2.string().trim().min(1).optional(),
2375
+ mapping: z2.record(identifierSchema, z2.string().trim().min(1)).optional(),
2376
+ source: z2.string().trim().min(1).optional(),
2377
+ sprite: z2.string().trim().min(1).optional()
1823
2378
  }).strict()
1824
2379
  ]);
1825
- var viewSchema = z.object({
1826
- bearing: z.number().finite().min(-180).max(180).optional(),
1827
- center: z.tuple([z.number().finite(), z.number().finite()]).refine(([lng, lat]) => lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90, {
2380
+ var viewSchema = z2.object({
2381
+ bearing: z2.number().finite().min(-180).max(180).optional(),
2382
+ center: z2.tuple([z2.number().finite(), z2.number().finite()]).refine(([lng, lat]) => lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90, {
1828
2383
  message: "Expected center as [lng, lat] within world bounds"
1829
2384
  }).optional(),
1830
- zoom: z.number().finite().min(0).max(24).optional()
2385
+ zoom: z2.number().finite().min(0).max(24).optional()
1831
2386
  }).strict();
1832
- var terrainConfigSchema = z.object({
1833
- attribution: z.string().optional(),
1834
- encoding: z.enum(["mapbox", "terrarium"]).optional(),
1835
- exaggeration: z.number().min(0).max(10).optional(),
1836
- mode: z.enum(["none", "hillshade", "3d"]).optional(),
1837
- sourceId: z.string().min(1).optional(),
1838
- tileset: z.string().trim().min(1).optional(),
1839
- url: z.url().optional()
2387
+ var buildingStyleSchema = z2.object({
2388
+ fillOpacity: z2.number().finite().min(0).max(1).optional(),
2389
+ heightThreshold: z2.number().finite().min(0).max(1e3).optional(),
2390
+ outlineOpacity: z2.number().finite().min(0).max(1).optional(),
2391
+ outlineWidth: z2.number().finite().min(0).max(16).optional()
1840
2392
  }).strict();
1841
- var tilesSchema = z.object({
1842
- sourceId: z.string().min(1).optional(),
1843
- tileset: z.string().trim().min(1).optional(),
1844
- url: z.url().optional(),
1845
- attribution: z.string().optional(),
1846
- sourceLayers: sourceLayersSchema.optional()
2393
+ var terrainConfigSchema = z2.object({
2394
+ attribution: z2.string().optional(),
2395
+ encoding: z2.enum(["mapbox", "terrarium"]).optional(),
2396
+ exaggeration: z2.number().min(0).max(10).optional(),
2397
+ mode: z2.enum(["none", "hillshade", "3d"]).optional(),
2398
+ sourceId: z2.string().min(1).optional(),
2399
+ tileset: z2.string().trim().min(1).optional(),
2400
+ url: z2.url().optional()
2401
+ }).strict();
2402
+ var tilesSchema = z2.object({
2403
+ sourceId: z2.string().min(1).optional(),
2404
+ tileset: z2.string().trim().min(1).optional(),
2405
+ url: z2.url().optional(),
2406
+ attribution: z2.string().optional(),
2407
+ sourceLayers: sourceLayersSchema.optional(),
2408
+ version: sourceVersionSchema.optional()
1847
2409
  }).strict();
1848
- var tileflowMapSchema = z.object({
2410
+ var tileflowMapSchema = z2.object({
1849
2411
  basemap: basemapSchema.optional(),
1850
- name: z.string().min(1).optional(),
1851
- tileset: z.string().trim().min(1).optional(),
1852
- theme: z.union([z.string().trim().min(1), themeConfigSchema]).optional(),
1853
- modules: z.array(moduleSchema).optional(),
1854
- layers: z.record(z.string(), styleLayerOverrideSchema).optional(),
2412
+ name: z2.string().min(1).optional(),
2413
+ tileset: z2.string().trim().min(1).optional(),
2414
+ theme: z2.union([z2.string().trim().min(1), themeConfigSchema]).optional(),
2415
+ modules: z2.array(moduleSchema).optional(),
2416
+ layers: z2.record(z2.string(), styleLayerOverrideSchema).optional(),
1855
2417
  colors: colorConfigSchema.optional(),
1856
2418
  typography: typographySchema.optional(),
1857
- density: z.enum(["clean", "balanced", "dense"]).optional(),
1858
- roads: z.enum(["hidden", "soft", "standard", "detailed"]).optional(),
1859
- labels: z.enum(["none", "essential", "balanced", "full"]).optional(),
1860
- poi: z.enum(["none", "minimal", "balanced", "full"]).optional(),
1861
- buildings: z.enum(["hidden", "flat", "3d"]).optional(),
1862
- terrain: z.union([z.enum(["none", "hillshade", "3d"]), terrainConfigSchema]).optional(),
1863
- renderer: z.enum(["auto", "osm-bright", "generated"]).optional(),
1864
- glyphs: z.string().min(1).optional(),
2419
+ density: z2.enum(["clean", "balanced", "dense"]).optional(),
2420
+ roads: z2.enum(["hidden", "soft", "standard", "detailed"]).optional(),
2421
+ labels: z2.enum(["none", "essential", "balanced", "full"]).optional(),
2422
+ poi: z2.enum(["none", "minimal", "balanced", "full"]).optional(),
2423
+ buildings: z2.enum(["hidden", "flat", "3d"]).optional(),
2424
+ buildingStyle: buildingStyleSchema.optional(),
2425
+ terrain: z2.union([z2.enum(["none", "hillshade", "3d"]), terrainConfigSchema]).optional(),
2426
+ renderer: z2.enum(["auto", "osm-bright", "generated"]).optional(),
2427
+ glyphs: z2.string().min(1).optional(),
1865
2428
  icons: iconSetSchema.optional(),
1866
- sprite: z.string().min(1).optional(),
2429
+ sprite: z2.string().min(1).optional(),
1867
2430
  tiles: tilesSchema.optional(),
1868
2431
  view: viewSchema.optional(),
1869
- allowedOrigins: z.array(z.string().trim().min(1).max(253)).max(20).optional()
2432
+ allowedOrigins: z2.array(z2.string().trim().min(1).max(253)).max(20).optional()
1870
2433
  }).strict();
1871
- var configSchema = tileflowMapSchema;
1872
- var tilesetConfigSchema = z.object({
1873
- id: z.string().trim().min(1).optional(),
1874
- name: z.string().trim().min(1).optional(),
1875
- attribution: z.string().optional(),
1876
- sourceLayers: sourceLayersSchema.optional()
1877
- }).strict();
1878
- var tileflowProjectSchema = z.object({
1879
- icons: z.record(identifierSchema, iconSetSchema).optional(),
1880
- themes: z.record(identifierSchema, themeConfigSchema).optional(),
1881
- tilesets: z.record(identifierSchema, tilesetConfigSchema).optional(),
1882
- maps: z.record(identifierSchema, configSchema).refine((maps) => {
1883
- return Object.keys(maps).length > 0;
1884
- }, "Expected at least one map")
2434
+ var configSchema = prototypeSafeInputSchema.pipe(tileflowMapSchema);
2435
+ var tilesetConfigSchema = z2.object({
2436
+ id: z2.string().trim().min(1).optional(),
2437
+ name: z2.string().trim().min(1).optional(),
2438
+ attribution: z2.string().optional(),
2439
+ sourceLayers: sourceLayersSchema.optional(),
2440
+ version: sourceVersionSchema.optional()
1885
2441
  }).strict();
2442
+ var captureScenesSchema = z2.record(tileflowCaptureSceneNameSchema, tileflowCaptureSceneSchema).superRefine((scenes, context) => {
2443
+ const namesByCaseFold = /* @__PURE__ */ new Map();
2444
+ for (const name of Object.keys(scenes).sort()) {
2445
+ const folded = name.toLowerCase();
2446
+ const existing = namesByCaseFold.get(folded);
2447
+ if (existing !== void 0) {
2448
+ context.addIssue({
2449
+ code: "custom",
2450
+ message: `Expected a portable scene name distinct from "${existing}" on case-insensitive filesystems`,
2451
+ path: [name]
2452
+ });
2453
+ } else {
2454
+ namesByCaseFold.set(folded, name);
2455
+ }
2456
+ }
2457
+ });
2458
+ var tileflowProjectSchema = prototypeSafeInputSchema.pipe(
2459
+ z2.object({
2460
+ icons: z2.record(identifierSchema, iconSetSchema).optional(),
2461
+ scenes: captureScenesSchema.optional(),
2462
+ themes: z2.record(identifierSchema, themeConfigSchema).optional(),
2463
+ tilesets: z2.record(identifierSchema, tilesetConfigSchema).optional(),
2464
+ maps: z2.record(identifierSchema, configSchema).refine((maps) => {
2465
+ return Object.keys(maps).length > 0;
2466
+ }, "Expected at least one map")
2467
+ }).strict()
2468
+ );
1886
2469
  var projectConfigSchema = tileflowProjectSchema;
1887
2470
  function parseTileflowMap(input) {
1888
2471
  const parsed = configSchema.parse(input);
@@ -1939,17 +2522,54 @@ function validateConfig(input) {
1939
2522
  var validateTileflowConfig = validateConfig;
1940
2523
  function isProjectConfigLike(input) {
1941
2524
  return Boolean(
1942
- input && typeof input === "object" && "maps" in input && typeof input.maps === "object"
2525
+ input && typeof input === "object" && Object.hasOwn(input, "maps") && typeof input.maps === "object"
1943
2526
  );
1944
2527
  }
2528
+ function findUnsafeObjectStructure(input) {
2529
+ if (!input || typeof input !== "object") return void 0;
2530
+ const pending = [{ path: [], value: input }];
2531
+ const visited = /* @__PURE__ */ new WeakSet();
2532
+ while (pending.length > 0) {
2533
+ const current = pending.pop();
2534
+ if (!current || visited.has(current.value)) continue;
2535
+ visited.add(current.value);
2536
+ if (!Array.isArray(current.value)) {
2537
+ const prototype = Object.getPrototypeOf(current.value);
2538
+ if (prototype !== Object.prototype && prototype !== null) {
2539
+ return { path: current.path, reason: "prototype" };
2540
+ }
2541
+ }
2542
+ for (const key of Object.keys(current.value)) {
2543
+ const path = [...current.path, Array.isArray(current.value) ? Number(key) : key];
2544
+ if (key === "__proto__") return { path, reason: "key" };
2545
+ const descriptor = Object.getOwnPropertyDescriptor(current.value, key);
2546
+ if (descriptor && "value" in descriptor && descriptor.value && typeof descriptor.value === "object") {
2547
+ pending.push({ path, value: descriptor.value });
2548
+ }
2549
+ }
2550
+ }
2551
+ return void 0;
2552
+ }
1945
2553
  function validateProjectThemeReferences(project) {
1946
2554
  const themeNames = new Set(Object.keys(project.themes ?? {}));
1947
2555
  const themeMessages = validateThemeExtends(project.themes, themeNames);
1948
2556
  const iconMessages = validateProjectIconReferences(project);
2557
+ const sceneMessages = validateProjectSceneReferences(project);
1949
2558
  const mapMessages = Object.entries(project.maps).flatMap(
1950
2559
  ([mapName, mapConfig]) => validateMapThemeReference(mapConfig, themeNames, `maps.${mapName}.theme`)
1951
2560
  );
1952
- return [...themeMessages, ...iconMessages, ...mapMessages];
2561
+ return [...themeMessages, ...iconMessages, ...sceneMessages, ...mapMessages];
2562
+ }
2563
+ function validateProjectSceneReferences(project) {
2564
+ return Object.entries(project.scenes ?? {}).flatMap(
2565
+ ([sceneName, scene]) => Object.hasOwn(project.maps, scene.map) ? [] : [
2566
+ {
2567
+ level: "error",
2568
+ path: `scenes.${sceneName}.map`,
2569
+ message: `Unknown Tileflow map "${scene.map}"`
2570
+ }
2571
+ ]
2572
+ );
1953
2573
  }
1954
2574
  function validateMapThemeReference(config, themeNames, path) {
1955
2575
  if (!config.theme) {
@@ -2019,7 +2639,7 @@ function validateProjectIconReferences(project) {
2019
2639
  const iconSets = project.icons ?? {};
2020
2640
  for (const [iconSetName, iconSet] of Object.entries(iconSets)) {
2021
2641
  const extendedName = iconSetReference(iconSet);
2022
- if (extendedName && !iconSets[extendedName]) {
2642
+ if (extendedName && !Object.hasOwn(iconSets, extendedName)) {
2023
2643
  messages.push({
2024
2644
  level: "error",
2025
2645
  path: iconSetExtendsPath(`icons.${iconSetName}`, iconSet),
@@ -2041,7 +2661,7 @@ function validateProjectIconReferences(project) {
2041
2661
  if (!isIconSetObject(mapConfig.icons) || !mapConfig.icons.extends) {
2042
2662
  continue;
2043
2663
  }
2044
- if (!iconSets[mapConfig.icons.extends]) {
2664
+ if (!Object.hasOwn(iconSets, mapConfig.icons.extends)) {
2045
2665
  messages.push({
2046
2666
  level: "error",
2047
2667
  path: `maps.${mapName}.icons.extends`,
@@ -2055,8 +2675,8 @@ function findIconSetCycle(iconSetName, iconSets, path) {
2055
2675
  if (path.includes(iconSetName)) {
2056
2676
  return [...path, iconSetName];
2057
2677
  }
2058
- const nextIconSetName = iconSetReference(iconSets?.[iconSetName]);
2059
- if (!nextIconSetName || !iconSets?.[nextIconSetName]) {
2678
+ const nextIconSetName = Object.hasOwn(iconSets ?? {}, iconSetName) ? iconSetReference(iconSets?.[iconSetName]) : void 0;
2679
+ if (!nextIconSetName || !Object.hasOwn(iconSets ?? {}, nextIconSetName)) {
2060
2680
  return void 0;
2061
2681
  }
2062
2682
  return findIconSetCycle(nextIconSetName, iconSets, [...path, iconSetName]);
@@ -2140,6 +2760,7 @@ function applyOsmBrightControls(layers, options) {
2140
2760
  return applyPoiLayerControls(
2141
2761
  layer,
2142
2762
  options.poi,
2763
+ options.colors,
2143
2764
  options.customIconSpriteId,
2144
2765
  options.customIconMapping
2145
2766
  );
@@ -2166,7 +2787,8 @@ function applyRoadLayerControls(layer, roads2) {
2166
2787
  const opacityScale = isCasing ? metrics.outlineOpacity : group === "highways" || group === "major" ? metrics.majorOpacity : group === "ferry" ? metrics.majorOpacity * 0.8 : group === "paths" ? metrics.minorOpacity * 0.77 : group === "rail" ? metrics.minorOpacity * 0.65 : metrics.minorOpacity;
2167
2788
  if (type === "line") {
2168
2789
  paint["line-opacity"] = scaleStyleValue(paint["line-opacity"], opacityScale, 1);
2169
- paint["line-width"] = scaleStyleValue(paint["line-width"], widthScale, 1);
2790
+ const scaledWidth = scaleStyleValue(paint["line-width"], widthScale, 1);
2791
+ paint["line-width"] = roads2.widthScale ? multiplyStyleValue(scaledWidth, roadClassWidthScaleExpression(roads2.widthScale)) : scaledWidth;
2170
2792
  nextLayer.paint = paint;
2171
2793
  }
2172
2794
  if (type === "fill") {
@@ -2187,6 +2809,22 @@ function applyRoadLayerControls(layer, roads2) {
2187
2809
  }
2188
2810
  return nextLayer;
2189
2811
  }
2812
+ function multiplyStyleValue(value, multiplier) {
2813
+ return ["*", legacyCameraFunctionExpression(value), multiplier];
2814
+ }
2815
+ function legacyCameraFunctionExpression(value) {
2816
+ if (!isRecord3(value) || !Array.isArray(value.stops)) return value;
2817
+ const stops = value.stops.filter(
2818
+ (stop) => Array.isArray(stop) && stop.length >= 2 && typeof stop[0] === "number"
2819
+ );
2820
+ if (stops.length === 0) return typeof value.default === "number" ? value.default : 1;
2821
+ return [
2822
+ "interpolate",
2823
+ ["exponential", typeof value.base === "number" ? value.base : 1],
2824
+ ["zoom"],
2825
+ ...stops.flatMap(([zoom, output]) => [zoom, output])
2826
+ ];
2827
+ }
2190
2828
  function isRoadLayerVisible(group, roads2) {
2191
2829
  if (roads2.detail === "none") return false;
2192
2830
  if (group === "paths") return roads2.extras.paths;
@@ -2246,7 +2884,9 @@ function applyLabelLayerControls(layer, labels2, roads2) {
2246
2884
  layout["text-field"] = labelTextField(labels2.language);
2247
2885
  }
2248
2886
  if (group === "roads") {
2249
- const allowedClasses = (roads2 ? visibleRoadLabelClasses(detail, roads2) : roadLabelClasses(detail)).filter((roadClass) => roadLabelLayerClasses(id).includes(roadClass));
2887
+ const allowedClasses = (roads2 ? visibleRoadLabelClasses(detail, roads2, labels2.roadClasses) : roadLabelClasses(detail).filter(
2888
+ (roadClass) => !labels2.roadClasses || labels2.roadClasses.includes(roadClass)
2889
+ )).filter((roadClass) => roadLabelLayerClasses(id).includes(roadClass));
2250
2890
  if (allowedClasses.length === 0) {
2251
2891
  return setLayerVisibility(layer, "none");
2252
2892
  }
@@ -2262,15 +2902,24 @@ function applyLabelLayerControls(layer, labels2, roads2) {
2262
2902
  nextLayer.layout = layout;
2263
2903
  return nextLayer;
2264
2904
  }
2265
- function applyPoiLayerControls(layer, poi2, customIconSpriteId2, customIconMapping) {
2905
+ function applyPoiLayerControls(layer, poi2, colors, customIconSpriteId2, customIconMapping) {
2266
2906
  if (poi2.mode === "none") {
2267
2907
  return setLayerVisibility(layer, "none");
2268
2908
  }
2269
2909
  const nextLayer = setLayerVisibility(layer, "visible");
2270
2910
  const layout = isRecord3(nextLayer.layout) ? { ...nextLayer.layout } : {};
2911
+ const paint = isRecord3(nextLayer.paint) ? { ...nextLayer.paint } : {};
2271
2912
  nextLayer.minzoom = Math.max(numberValue(nextLayer.minzoom) ?? 0, poi2.minZoom);
2272
2913
  if (poi2.categories && sourceLayerValue(nextLayer) === "poi") {
2273
- nextLayer.filter = combineFilters(nextLayer.filter, poiCategoryFilter(poi2.categories));
2914
+ nextLayer.filter = combineFilters(
2915
+ nextLayer.filter,
2916
+ poiCategoryFilter(poi2.categories, poi2.classMapping)
2917
+ );
2918
+ }
2919
+ if (poi2.density && sourceLayerValue(nextLayer) === "poi") {
2920
+ const densityFilter = poiDensityFilter(poi2.density, poi2.classMapping);
2921
+ if (densityFilter) nextLayer.filter = combineFilters(nextLayer.filter, densityFilter);
2922
+ layout["symbol-sort-key"] = poiPriorityExpression(poi2.classMapping);
2274
2923
  }
2275
2924
  if (poi2.labels === "none") {
2276
2925
  delete layout["text-field"];
@@ -2280,10 +2929,21 @@ function applyPoiLayerControls(layer, poi2, customIconSpriteId2, customIconMappi
2280
2929
  if (poi2.icons === false) {
2281
2930
  delete layout["icon-image"];
2282
2931
  } else if ("icon-image" in layout || customIconSpriteId2) {
2283
- layout["icon-image"] = poiIconImageExpression(customIconSpriteId2, customIconMapping);
2284
- layout["icon-optional"] = true;
2932
+ layout["icon-image"] = poiIconImageExpression(
2933
+ customIconSpriteId2,
2934
+ customIconMapping,
2935
+ poi2.classMapping
2936
+ );
2937
+ layout["icon-optional"] = !poi2.placement.coupleIconAndLabel;
2938
+ layout["icon-padding"] = poi2.placement.iconPadding;
2939
+ }
2940
+ layout["text-padding"] = poi2.placement.textPadding;
2941
+ if (poi2.placement.coupleIconAndLabel) layout["text-optional"] = false;
2942
+ if (poi2.color === "category" && "text-color" in paint) {
2943
+ paint["text-color"] = poiCategoryColorExpression(colors, poi2.classMapping);
2285
2944
  }
2286
2945
  nextLayer.layout = layout;
2946
+ nextLayer.paint = paint;
2287
2947
  return nextLayer;
2288
2948
  }
2289
2949
  function labelGroupDetail(labels2, group) {
@@ -4909,8 +5569,8 @@ var style_default = {
4909
5569
  };
4910
5570
 
4911
5571
  // src/templates/osm-bright/theme.ts
4912
- function applyOsmBrightTheme(layers, colors, theme, typography) {
4913
- if (!theme.custom && theme.name === "light") {
5572
+ function applyOsmBrightTheme(layers, colors, theme, typography, typographyOverridden = theme.typographyOverridden) {
5573
+ if (!theme.custom && theme.name === "light" && !typographyOverridden) {
4914
5574
  return layers;
4915
5575
  }
4916
5576
  return layers.map((layer) => {
@@ -4950,8 +5610,8 @@ function applyOsmBrightTheme(layers, colors, theme, typography) {
4950
5610
  paint["text-halo-color"] = colors.labels.halo;
4951
5611
  paintChanged = true;
4952
5612
  }
4953
- if (theme.typographyOverridden && "text-font" in layout) {
4954
- layout["text-font"] = textFont(typography);
5613
+ if (typographyOverridden && "text-font" in layout) {
5614
+ layout["text-font"] = textFont(typography, typographyDomain(id, sourceLayer));
4955
5615
  layoutChanged = true;
4956
5616
  }
4957
5617
  }
@@ -4964,6 +5624,16 @@ function applyOsmBrightTheme(layers, colors, theme, typography) {
4964
5624
  return nextLayer;
4965
5625
  });
4966
5626
  }
5627
+ function typographyDomain(id, sourceLayer) {
5628
+ if (sourceLayer === "poi" || id.startsWith("poi-")) return "poi";
5629
+ if (sourceLayer === "transportation_name" || id.includes("highway-name") || id.includes("highway-shield")) {
5630
+ return "roads";
5631
+ }
5632
+ if (sourceLayer === "water_name" || sourceLayer === "waterway" || id.includes("water-name")) {
5633
+ return "water";
5634
+ }
5635
+ return "places";
5636
+ }
4967
5637
  function osmBrightFillColor(id, sourceLayer, colors, theme) {
4968
5638
  if (sourceLayer === "water" || id.includes("water")) {
4969
5639
  return hasBaseColor(theme, ["water"]) || hasNestedModuleColor(theme, "hydro", "water") ? colors.hydro.water : void 0;
@@ -5183,16 +5853,22 @@ function createOsmBrightStyle(options) {
5183
5853
  layers,
5184
5854
  options.colors,
5185
5855
  options.theme,
5186
- options.typography
5856
+ options.typography,
5857
+ options.typographyOverridden
5187
5858
  );
5188
5859
  const controlledLayers = applyOsmBrightControls(themedLayers, {
5860
+ colors: options.colors,
5189
5861
  customIconMapping: options.customIconMapping,
5190
5862
  customIconSpriteId: options.customIconSpriteId,
5191
5863
  labels: options.labelsModule,
5192
5864
  poi: options.poiModule,
5193
5865
  roads: options.roadsModule
5194
5866
  });
5195
- const resolvedLayers = options.terrain ? withTerrainLayers(controlledLayers, options.terrain) : controlledLayers;
5867
+ const semanticLayers = options.roadsModule?.oneWayMarkers ? insertBeforeRoadLabels(
5868
+ controlledLayers,
5869
+ oneWayMarkerLayer(options.sourceId, "transportation", options.colors, options.typography)
5870
+ ) : controlledLayers;
5871
+ const resolvedLayers = options.terrain ? withTerrainLayers(semanticLayers, options.terrain) : semanticLayers;
5196
5872
  return {
5197
5873
  ...template,
5198
5874
  name: options.name,
@@ -5222,6 +5898,13 @@ function createOsmBrightStyle(options) {
5222
5898
  }
5223
5899
  };
5224
5900
  }
5901
+ function insertBeforeRoadLabels(layers, layer) {
5902
+ const index = layers.findIndex(
5903
+ (candidate) => candidate.type === "symbol" && candidate["source-layer"] === "transportation_name"
5904
+ );
5905
+ if (index < 0) return [...layers, layer];
5906
+ return [...layers.slice(0, index), layer, ...layers.slice(index)];
5907
+ }
5225
5908
  function cloneJson(value) {
5226
5909
  return JSON.parse(JSON.stringify(value));
5227
5910
  }
@@ -5236,7 +5919,9 @@ function remapOsmBrightSource(layer, sourceId) {
5236
5919
  }
5237
5920
 
5238
5921
  // src/compiler.ts
5922
+ var tileflowHostedNotoSansWeights = ["regular", "bold"];
5239
5923
  var defaultSourceLayers = {
5924
+ landcover: "landcover",
5240
5925
  landuse: "landuse",
5241
5926
  park: "park",
5242
5927
  water: "water",
@@ -5302,6 +5987,15 @@ function createStyle(input = {}, options = {}) {
5302
5987
  theme.preset.roads ?? defaultConfig.roads
5303
5988
  );
5304
5989
  const { mode: resolvedRoadMode, ...resolvedRoadModule } = resolvedRoads;
5990
+ const resolvedRoadModuleMetadata = {
5991
+ detail: resolvedRoadModule.detail,
5992
+ extras: resolvedRoadModule.extras,
5993
+ hierarchy: resolvedRoadModule.hierarchy,
5994
+ outline: resolvedRoadModule.outline,
5995
+ weight: resolvedRoadModule.weight,
5996
+ ...roadModule?.oneWayMarkers !== void 0 ? { oneWayMarkers: resolvedRoadModule.oneWayMarkers } : {},
5997
+ ...roadModule?.widthScale ? { widthScale: resolvedRoadModule.widthScale } : {}
5998
+ };
5305
5999
  const resolvedLabels = resolveLabels(
5306
6000
  config.labels,
5307
6001
  labelsModule,
@@ -5309,14 +6003,18 @@ function createStyle(input = {}, options = {}) {
5309
6003
  );
5310
6004
  const { mode: resolvedLabelsMode, ...resolvedLabelsModule } = resolvedLabels;
5311
6005
  const resolvedPoi = resolvePoi(config.poi, poiModule, theme.preset.poi ?? defaultConfig.poi);
6006
+ const { mode: poi2, ...resolvedPoiModule } = resolvedPoi;
5312
6007
  const labels2 = resolvedLabelsMode;
5313
- const poi2 = resolvedPoi.mode;
5314
6008
  const buildings = config.buildings ?? "flat";
5315
6009
  const colors = resolveColors(config.colors, theme.colors, {}, theme.modules);
5316
6010
  const sourceId = config.tiles?.sourceId ?? basemap.sourceId ?? "tileflow";
5317
6011
  const tileBaseUrl = normalizeBaseUrl(options.tileBaseUrl);
5318
6012
  const tileset = options.tileset ?? config.tileset ?? config.tiles?.tileset ?? basemap.tileset ?? defaultTileset;
5319
- const tilesUrl = config.tiles?.url ?? basemap.url ?? `${tileBaseUrl}/tiles/${tileset}/tiles.json`;
6013
+ const tilesetVersion = config.tiles?.version ?? basemap.version;
6014
+ const tilesUrl = withArchiveVersion(
6015
+ config.tiles?.url ?? basemap.url ?? `${tileBaseUrl}/tiles/${tileset}/tiles.json`,
6016
+ tilesetVersion
6017
+ );
5320
6018
  const typography = resolveTypography(config.typography, theme.typography);
5321
6019
  const terrain = resolveTerrain(config.terrain, tileBaseUrl);
5322
6020
  const rendererPreference = config.renderer ?? "auto";
@@ -5324,10 +6022,11 @@ function createStyle(input = {}, options = {}) {
5324
6022
  const useOsmBrightTemplate = rendererPreference === "osm-bright" || rendererPreference === "auto" && canUseOsmBrightTemplate;
5325
6023
  if (rendererPreference === "osm-bright" && !canUseOsmBrightTemplate) {
5326
6024
  throw new Error(
5327
- 'Tileflow renderer "osm-bright" is not compatible with this config. Use renderer "auto" or "generated", or remove map-level color overrides, typography, density, legacy roads/buildings, custom source layers, and unsupported modules.'
6025
+ 'Tileflow renderer "osm-bright" is not compatible with this config. Use renderer "auto" or "generated", or remove map-level color overrides, density, legacy roads/buildings, custom source layers, and unsupported modules.'
5328
6026
  );
5329
6027
  }
5330
6028
  const glyphs = config.glyphs ?? basemap.glyphs ?? `${tileBaseUrl}/fonts/{fontstack}/{range}.pbf`;
6029
+ validateHostedTypography(typography, glyphs, tileBaseUrl);
5331
6030
  const iconSet = resolveIconSet(config.icons, options.iconSets);
5332
6031
  const iconSprite = iconSet?.sprite;
5333
6032
  const basemapSprite = basemap.sprite ?? (basemap.type === "osm" ? `${tileBaseUrl}/sprites/osm-bright/sprite` : void 0);
@@ -5340,32 +6039,38 @@ function createStyle(input = {}, options = {}) {
5340
6039
  "tileflow:themeMode": theme.mode,
5341
6040
  "tileflow:density": density,
5342
6041
  "tileflow:roads": resolvedRoadMode,
5343
- "tileflow:roadsModule": roadModule ? resolvedRoadModule : void 0,
6042
+ ...roadModule ? { "tileflow:roadsModule": resolvedRoadModuleMetadata } : {},
5344
6043
  "tileflow:labels": labels2,
5345
- "tileflow:labelsModule": labelsModule ? resolvedLabelsModule : void 0,
6044
+ ...labelsModule ? { "tileflow:labelsModule": resolvedLabelsModule } : {},
5346
6045
  "tileflow:poi": poi2,
5347
- "tileflow:poiModule": poiModule,
5348
- "tileflow:icons": iconSet ? {
5349
- customSpriteId: customIconSpriteName,
5350
- mapping: iconSet.mapping,
5351
- sprite: iconSprite
5352
- } : void 0,
6046
+ ...poiModule ? { "tileflow:poiModule": resolvedPoiModule } : {},
6047
+ ...iconSet ? {
6048
+ "tileflow:icons": {
6049
+ ...customIconSpriteName ? { customSpriteId: customIconSpriteName } : {},
6050
+ ...iconSet.mapping ? { mapping: iconSet.mapping } : {},
6051
+ ...iconSprite ? { sprite: iconSprite } : {}
6052
+ }
6053
+ } : {},
5353
6054
  "tileflow:buildings": buildings,
6055
+ ...config.buildingStyle ? { "tileflow:buildingStyle": config.buildingStyle } : {},
5354
6056
  "tileflow:modules": config.modules?.map((moduleConfig) => moduleConfig.type) ?? [],
5355
6057
  "tileflow:themeModules": Object.keys(theme.moduleOverrides),
5356
- "tileflow:terrain": terrain ? {
5357
- encoding: terrain.source.encoding,
5358
- mode: terrain.mode,
5359
- sourceId: terrain.sourceId
5360
- } : void 0,
6058
+ ...terrain ? {
6059
+ "tileflow:terrain": {
6060
+ encoding: terrain.source.encoding,
6061
+ mode: terrain.mode,
6062
+ sourceId: terrain.sourceId
6063
+ }
6064
+ } : {},
5361
6065
  "tileflow:renderer": useOsmBrightTemplate ? "osm-bright" : "generated",
5362
6066
  "tileflow:rendererPreference": rendererPreference,
5363
- "tileflow:typography": typography,
6067
+ "tileflow:typography": typographyMetadata(typography),
5364
6068
  "tileflow:colors": colors,
5365
6069
  "tileflow:tileBaseUrl": tileBaseUrl,
5366
6070
  "tileflow:tileset": tileset,
6071
+ ...tilesetVersion ? { "tileflow:tilesetVersion": tilesetVersion } : {},
5367
6072
  "tileflow:tiles": "tileflow",
5368
- "tileflow:view": config.view
6073
+ ...config.view ? { "tileflow:view": config.view } : {}
5369
6074
  };
5370
6075
  if (useOsmBrightTemplate) {
5371
6076
  const style2 = createOsmBrightStyle({
@@ -5380,6 +6085,7 @@ function createStyle(input = {}, options = {}) {
5380
6085
  theme,
5381
6086
  tilesUrl,
5382
6087
  typography,
6088
+ typographyOverridden: Boolean(config.typography) || theme.typographyOverridden,
5383
6089
  labelsModule: resolvedLabels,
5384
6090
  poi: poi2,
5385
6091
  poiModule: resolvedPoi,
@@ -5402,6 +6108,7 @@ function createStyle(input = {}, options = {}) {
5402
6108
  );
5403
6109
  const layers = openMapTilesLayers(sourceId, sourceLayers, colors, {
5404
6110
  buildings,
6111
+ buildingStyle: config.buildingStyle,
5405
6112
  density,
5406
6113
  labels: resolvedLabels,
5407
6114
  poi: resolvedPoi,
@@ -5439,7 +6146,7 @@ function createStyle(input = {}, options = {}) {
5439
6146
  return style;
5440
6147
  }
5441
6148
  function createStyleFromProject(projectConfig, mapName, options = {}) {
5442
- const config = projectConfig.maps[mapName];
6149
+ const config = Object.hasOwn(projectConfig.maps, mapName) ? projectConfig.maps[mapName] : void 0;
5443
6150
  if (!config) {
5444
6151
  throw new Error(`Unknown Tileflow map: ${mapName}`);
5445
6152
  }
@@ -5458,11 +6165,13 @@ function createStyleFromProject(projectConfig, mapName, options = {}) {
5458
6165
  ...baseBasemap,
5459
6166
  tileset: resolvedTileset,
5460
6167
  attribution: baseBasemap.attribution ?? tilesetConfig?.attribution,
6168
+ version: baseBasemap.version ?? tilesetConfig?.version,
5461
6169
  ...resolvedBasemapSourceLayers ? { sourceLayers: resolvedBasemapSourceLayers } : {}
5462
6170
  },
5463
6171
  tiles: {
5464
6172
  ...tileOptions,
5465
6173
  tileset: resolvedTileset,
6174
+ version: tileOptions.version ?? baseBasemap.version ?? tilesetConfig?.version,
5466
6175
  ...resolvedTileSourceLayers ? { sourceLayers: resolvedTileSourceLayers } : {}
5467
6176
  }
5468
6177
  },
@@ -5501,7 +6210,7 @@ function resolveSourceLayers(mapLayers, tilesetLayers) {
5501
6210
  }
5502
6211
  function isOsmBrightTemplateCompatible(config, basemap, theme) {
5503
6212
  const templateTheme = theme.custom || theme.name === "light" || theme.name === "dark";
5504
- return basemap.type === "osm" && templateTheme && !hasEffectiveColorOverrides(config.colors) && !config.typography && !config.density && !config.roads && !config.buildings && !config.modules?.some(
6213
+ return basemap.type === "osm" && templateTheme && !hasEffectiveColorOverrides(config.colors) && !config.density && !config.roads && !config.buildings && !config.buildingStyle && !config.modules?.some(
5505
6214
  (moduleConfig) => moduleConfig.type !== "styleOverride" && moduleConfig.type !== "labels" && moduleConfig.type !== "poi" && moduleConfig.type !== "roads"
5506
6215
  ) && !basemap.sourceLayers && !config.tiles?.sourceLayers;
5507
6216
  }
@@ -5519,6 +6228,23 @@ function hasEffectiveColorOverrides(colors) {
5519
6228
  return typeof value === "string";
5520
6229
  });
5521
6230
  }
6231
+ function typographyMetadata(typography) {
6232
+ const base = {
6233
+ font: typography.font,
6234
+ fontFamily: typography.fontFamily,
6235
+ weight: typography.weight
6236
+ };
6237
+ return {
6238
+ ...base,
6239
+ ...sameTypographyStyle(typography.places, base) ? {} : { places: typography.places },
6240
+ ...sameTypographyStyle(typography.roads, base) ? {} : { roads: typography.roads },
6241
+ ...sameTypographyStyle(typography.water, base) ? {} : { water: typography.water },
6242
+ ...sameTypographyStyle(typography.poi, base) ? {} : { poi: typography.poi }
6243
+ };
6244
+ }
6245
+ function sameTypographyStyle(left, right) {
6246
+ return left.font === right.font && left.fontFamily === right.fontFamily && left.weight === right.weight;
6247
+ }
5522
6248
  function isRecord5(value) {
5523
6249
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
5524
6250
  }
@@ -5531,6 +6257,12 @@ function normalizeStyleBaseUrl(value) {
5531
6257
  function normalizeUrl(value) {
5532
6258
  return value.replace(/\/+$/, "");
5533
6259
  }
6260
+ function withArchiveVersion(value, version) {
6261
+ if (!version) return value;
6262
+ const url = new URL(value);
6263
+ url.searchParams.set("archiveVersion", version);
6264
+ return url.toString();
6265
+ }
5534
6266
  function resolveIconSet(icons, iconSets, path = []) {
5535
6267
  if (!icons) {
5536
6268
  return void 0;
@@ -5573,12 +6305,455 @@ function resolveSprite(basemapSprite, iconSprite) {
5573
6305
  function isSpriteReference(value) {
5574
6306
  return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") || value.startsWith("data:");
5575
6307
  }
6308
+ function validateHostedTypography(typography, glyphs, tileBaseUrl) {
6309
+ const hostedGlyphs = `${tileBaseUrl}/fonts/{fontstack}/{range}.pbf`;
6310
+ if (glyphs !== hostedGlyphs) return;
6311
+ for (const [domain, style] of [
6312
+ ["default", typography],
6313
+ ["places", typography.places],
6314
+ ["roads", typography.roads],
6315
+ ["water", typography.water],
6316
+ ["poi", typography.poi]
6317
+ ]) {
6318
+ if (style.font === "Noto Sans" && !tileflowHostedNotoSansWeights.includes(style.weight)) {
6319
+ throw new Error(
6320
+ `Tileflow-hosted Noto Sans does not provide the ${style.weight} weight for ${domain} typography. Use regular or bold, or configure a custom glyphs URL that provides this font stack.`
6321
+ );
6322
+ }
6323
+ }
6324
+ }
5576
6325
 
5577
6326
  // src/config.ts
5578
6327
  function defineTileflow(config) {
5579
6328
  return config;
5580
6329
  }
5581
6330
 
6331
+ // src/icon-package.ts
6332
+ import { z as z3 } from "zod";
6333
+ var tileflowIconPackageFileNames = [
6334
+ "sprite.json",
6335
+ "sprite.png",
6336
+ "sprite@2x.json",
6337
+ "sprite@2x.png"
6338
+ ];
6339
+ var tileflowHostedIconIdPattern = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
6340
+ var tileflowIconPackageLimits = {
6341
+ decodeConcurrency: 4,
6342
+ maxAtlasDimension: 2048,
6343
+ maxDecodedPixelsPerIcon: 4194304,
6344
+ maxGeneratedFileBytes: 4 * 1024 * 1024,
6345
+ maxGeneratedPackageBytes: 8 * 1024 * 1024,
6346
+ maxIconCount: 256,
6347
+ maxSourceBytes: 32 * 1024 * 1024,
6348
+ maxSourceFileBytes: 1024 * 1024
6349
+ };
6350
+ var tileflowHostedAlphaCompatibility = {
6351
+ iconPackages: {
6352
+ maxGeneratedBytesPerProject: 64 * 1024 * 1024,
6353
+ maxRetainedPerProject: 24
6354
+ },
6355
+ maxMapsPerDeploy: 1
6356
+ };
6357
+ var sha256Schema = z3.string().regex(/^[a-f0-9]{64}$/);
6358
+ var tileflowIconPackageContentHashSchema = sha256Schema;
6359
+ var tileflowHostedIconIdSchema = z3.string().regex(tileflowHostedIconIdPattern);
6360
+ var tileflowRenderedIconManifestEntrySchema = z3.object({
6361
+ name: z3.string().min(1).max(256),
6362
+ pixelSha256: z3.object({
6363
+ oneX: sha256Schema,
6364
+ twoX: sha256Schema
6365
+ }).strict()
6366
+ }).strict();
6367
+ var tileflowIconPackageLabelSchema = z3.string().superRefine((value, context) => {
6368
+ if (value !== value.trim() || value.length === 0) {
6369
+ context.addIssue({
6370
+ code: "custom",
6371
+ message: "Icon package labels must be non-empty and have no surrounding whitespace"
6372
+ });
6373
+ }
6374
+ if (Array.from(value).length > 64) {
6375
+ context.addIssue({
6376
+ code: "custom",
6377
+ message: "Icon package labels must contain at most 64 Unicode characters"
6378
+ });
6379
+ }
6380
+ if (!isWellFormedUnicode(value) || containsControlCharacter(value)) {
6381
+ context.addIssue({
6382
+ code: "custom",
6383
+ message: "Icon package labels must contain printable, well-formed Unicode"
6384
+ });
6385
+ }
6386
+ });
6387
+ var spriteDimensionsSchema = z3.object({
6388
+ height: z3.number().int().positive().max(tileflowIconPackageLimits.maxAtlasDimension),
6389
+ pixelRatio: z3.union([z3.literal(1), z3.literal(2)]),
6390
+ width: z3.number().int().positive().max(tileflowIconPackageLimits.maxAtlasDimension)
6391
+ }).strict();
6392
+ function iconPackageFileSchema(name, contentType) {
6393
+ return z3.object({
6394
+ byteLength: z3.number().int().nonnegative().max(tileflowIconPackageLimits.maxGeneratedFileBytes),
6395
+ contentType: z3.literal(contentType),
6396
+ name: z3.literal(name),
6397
+ sha256: sha256Schema
6398
+ }).strict();
6399
+ }
6400
+ var tileflowIconPackageManifestSchema = z3.object({
6401
+ files: z3.tuple([
6402
+ iconPackageFileSchema("sprite.json", "application/json"),
6403
+ iconPackageFileSchema("sprite.png", "image/png"),
6404
+ iconPackageFileSchema("sprite@2x.json", "application/json"),
6405
+ iconPackageFileSchema("sprite@2x.png", "image/png")
6406
+ ]),
6407
+ format: z3.literal("tileflow-icon-package-v1"),
6408
+ iconNames: z3.array(z3.string().min(1).max(256)).min(1).max(tileflowIconPackageLimits.maxIconCount),
6409
+ renderedIcons: z3.array(tileflowRenderedIconManifestEntrySchema).min(1).max(tileflowIconPackageLimits.maxIconCount),
6410
+ sprites: z3.object({
6411
+ oneX: spriteDimensionsSchema.extend({ pixelRatio: z3.literal(1) }),
6412
+ twoX: spriteDimensionsSchema.extend({ pixelRatio: z3.literal(2) })
6413
+ }).strict()
6414
+ }).strict().superRefine((manifest, context) => {
6415
+ const sortedNames = [...manifest.iconNames].sort(compareCodeUnits);
6416
+ if (sortedNames.some((name, index) => name !== manifest.iconNames[index]) || new Set(manifest.iconNames).size !== manifest.iconNames.length) {
6417
+ context.addIssue({
6418
+ code: "custom",
6419
+ message: "Icon names must be unique and sorted by Unicode code unit",
6420
+ path: ["iconNames"]
6421
+ });
6422
+ }
6423
+ if (manifest.renderedIcons.length !== manifest.iconNames.length || manifest.renderedIcons.some((entry, index) => entry.name !== manifest.iconNames[index])) {
6424
+ context.addIssue({
6425
+ code: "custom",
6426
+ message: "Rendered icon entries must exactly match iconNames in sorted order",
6427
+ path: ["renderedIcons"]
6428
+ });
6429
+ }
6430
+ const totalBytes = manifest.files.reduce((total, file) => total + file.byteLength, 0);
6431
+ if (totalBytes > tileflowIconPackageLimits.maxGeneratedPackageBytes) {
6432
+ context.addIssue({
6433
+ code: "custom",
6434
+ message: `Generated icon package exceeds ${tileflowIconPackageLimits.maxGeneratedPackageBytes} bytes`,
6435
+ path: ["files"]
6436
+ });
6437
+ }
6438
+ if (manifest.sprites.twoX.width !== manifest.sprites.oneX.width * 2 || manifest.sprites.twoX.height !== manifest.sprites.oneX.height * 2) {
6439
+ context.addIssue({
6440
+ code: "custom",
6441
+ message: "The @2x sprite dimensions must be exactly twice the 1x dimensions",
6442
+ path: ["sprites", "twoX"]
6443
+ });
6444
+ }
6445
+ });
6446
+ function compareCodeUnits(left, right) {
6447
+ if (left < right) {
6448
+ return -1;
6449
+ }
6450
+ if (left > right) {
6451
+ return 1;
6452
+ }
6453
+ return 0;
6454
+ }
6455
+ function serializeCanonicalJson(value) {
6456
+ return JSON.stringify(sortJsonValue(value));
6457
+ }
6458
+ function serializeTileflowIconPackageManifest(input) {
6459
+ const manifest = tileflowIconPackageManifestSchema.parse(input);
6460
+ return serializeCanonicalJson(manifest);
6461
+ }
6462
+ async function sha256Hex(input) {
6463
+ const bytes = typeof input === "string" ? new TextEncoder().encode(input) : input;
6464
+ const digestInput = new Uint8Array(bytes.byteLength);
6465
+ digestInput.set(bytes);
6466
+ const digest = await crypto.subtle.digest("SHA-256", digestInput.buffer);
6467
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
6468
+ }
6469
+ async function hashTileflowIconPackageManifest(input) {
6470
+ return sha256Hex(serializeTileflowIconPackageManifest(input));
6471
+ }
6472
+ async function hashTileflowRenderedIconPixels(input) {
6473
+ if (!Number.isSafeInteger(input.width) || !Number.isSafeInteger(input.height) || input.width <= 0 || input.height <= 0 || input.width > tileflowIconPackageLimits.maxAtlasDimension || input.height > tileflowIconPackageLimits.maxAtlasDimension) {
6474
+ throw new Error("Rendered icon dimensions are invalid");
6475
+ }
6476
+ if (input.pixelRatio !== 1 && input.pixelRatio !== 2) {
6477
+ throw new Error("Rendered icon pixel ratio must be 1 or 2");
6478
+ }
6479
+ const pixelCount = input.width * input.height;
6480
+ const expectedBytes = pixelCount * 4;
6481
+ if (pixelCount > tileflowIconPackageLimits.maxDecodedPixelsPerIcon || input.rgba.byteLength !== expectedBytes) {
6482
+ throw new Error(`Rendered icon RGBA must contain exactly ${expectedBytes} bytes`);
6483
+ }
6484
+ const normalized = new Uint8Array(input.rgba);
6485
+ for (let offset = 0; offset < normalized.byteLength; offset += 4) {
6486
+ if (normalized[offset + 3] === 0) {
6487
+ normalized[offset] = 0;
6488
+ normalized[offset + 1] = 0;
6489
+ normalized[offset + 2] = 0;
6490
+ }
6491
+ }
6492
+ const domain = new TextEncoder().encode("tileflow-rendered-icon-pixels-v1\0");
6493
+ const frame = new Uint8Array(domain.byteLength + 9 + normalized.byteLength);
6494
+ frame.set(domain);
6495
+ frame[domain.byteLength] = input.pixelRatio;
6496
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
6497
+ view.setUint32(domain.byteLength + 1, input.width);
6498
+ view.setUint32(domain.byteLength + 5, input.height);
6499
+ frame.set(normalized, domain.byteLength + 9);
6500
+ return sha256Hex(frame);
6501
+ }
6502
+ function sortJsonValue(value) {
6503
+ if (Array.isArray(value)) {
6504
+ return value.map(sortJsonValue);
6505
+ }
6506
+ if (value !== null && typeof value === "object") {
6507
+ return Object.fromEntries(
6508
+ Object.entries(value).sort(([left], [right]) => compareCodeUnits(left, right)).map(([key, child]) => [key, sortJsonValue(child)])
6509
+ );
6510
+ }
6511
+ return value;
6512
+ }
6513
+ function isWellFormedUnicode(value) {
6514
+ for (let index = 0; index < value.length; index += 1) {
6515
+ const code = value.charCodeAt(index);
6516
+ if (code >= 55296 && code <= 56319) {
6517
+ const next = value.charCodeAt(index + 1);
6518
+ if (next < 56320 || next > 57343) {
6519
+ return false;
6520
+ }
6521
+ index += 1;
6522
+ continue;
6523
+ }
6524
+ if (code >= 56320 && code <= 57343) {
6525
+ return false;
6526
+ }
6527
+ }
6528
+ return true;
6529
+ }
6530
+ function containsControlCharacter(value) {
6531
+ return Array.from(value).some((character) => {
6532
+ const codePoint = character.codePointAt(0) ?? 0;
6533
+ return codePoint < 32 || codePoint >= 127 && codePoint <= 159;
6534
+ });
6535
+ }
6536
+
6537
+ // src/icon-diff.ts
6538
+ function diffTileflowIconPackageManifests(before, after) {
6539
+ const beforeIcons = new Map(
6540
+ (before?.renderedIcons ?? []).map((entry) => [entry.name, entry.pixelSha256])
6541
+ );
6542
+ const afterIcons = new Map(
6543
+ (after?.renderedIcons ?? []).map((entry) => [entry.name, entry.pixelSha256])
6544
+ );
6545
+ const names = [.../* @__PURE__ */ new Set([...beforeIcons.keys(), ...afterIcons.keys()])].sort(compareCodeUnits);
6546
+ const added = [];
6547
+ const modified = [];
6548
+ const removed = [];
6549
+ let unchangedCount = 0;
6550
+ for (const name of names) {
6551
+ const previous = beforeIcons.get(name);
6552
+ const proposed = afterIcons.get(name);
6553
+ if (!previous) {
6554
+ added.push(name);
6555
+ } else if (!proposed) {
6556
+ removed.push(name);
6557
+ } else if (previous.oneX !== proposed.oneX || previous.twoX !== proposed.twoX) {
6558
+ modified.push(name);
6559
+ } else {
6560
+ unchangedCount += 1;
6561
+ }
6562
+ }
6563
+ return {
6564
+ added,
6565
+ afterBytes: packageBytes(after),
6566
+ beforeBytes: packageBytes(before),
6567
+ modified,
6568
+ removed,
6569
+ unchangedCount
6570
+ };
6571
+ }
6572
+ function diffTileflowIconMappings(before, after) {
6573
+ const previous = before ?? {};
6574
+ const proposed = after ?? {};
6575
+ const keys = [.../* @__PURE__ */ new Set([...Object.keys(previous), ...Object.keys(proposed)])].sort(
6576
+ compareCodeUnits
6577
+ );
6578
+ const result = { added: [], changed: [], removed: [] };
6579
+ for (const key of keys) {
6580
+ const beforeTarget = previous[key];
6581
+ const afterTarget = proposed[key];
6582
+ if (beforeTarget === void 0 && afterTarget !== void 0) {
6583
+ result.added.push({ after: afterTarget, key });
6584
+ } else if (beforeTarget !== void 0 && afterTarget === void 0) {
6585
+ result.removed.push({ before: beforeTarget, key });
6586
+ } else if (beforeTarget !== afterTarget) {
6587
+ result.changed.push({ after: afterTarget, before: beforeTarget, key });
6588
+ }
6589
+ }
6590
+ return result;
6591
+ }
6592
+ function inspectTileflowIconReferences(project, mapName, availableIconNames) {
6593
+ const map = Object.hasOwn(project.maps, mapName) ? project.maps[mapName] : void 0;
6594
+ if (!map) {
6595
+ throw new Error(`Unknown Tileflow map: ${mapName}`);
6596
+ }
6597
+ const available = new Set(availableIconNames);
6598
+ const dangling = [];
6599
+ const unanalyzable = [];
6600
+ const mapping = resolveMappingReferences(project, mapName, map.icons);
6601
+ for (const reference of mapping.values()) {
6602
+ if (!available.has(reference.iconName)) {
6603
+ dangling.push({ ...reference, kind: "mapping" });
6604
+ }
6605
+ }
6606
+ const layers = [];
6607
+ collectThemeLayers(project, mapName, map.theme, layers);
6608
+ if (map.layers) {
6609
+ layers.push({ layers: map.layers, path: `maps.${mapName}.layers` });
6610
+ }
6611
+ for (const [index, moduleConfig] of (map.modules ?? []).entries()) {
6612
+ if (moduleConfig.type === "styleOverride" && moduleConfig.layers) {
6613
+ layers.push({
6614
+ layers: moduleConfig.layers,
6615
+ path: `maps.${mapName}.modules.${index}.layers`
6616
+ });
6617
+ }
6618
+ }
6619
+ for (const layerGroup of layers) {
6620
+ for (const [layerName, layer] of Object.entries(layerGroup.layers).sort(
6621
+ ([left], [right]) => compareCodeUnits(left, right)
6622
+ )) {
6623
+ if (!layer.layout || !Object.prototype.hasOwnProperty.call(layer.layout, "icon-image")) {
6624
+ continue;
6625
+ }
6626
+ const path = `${layerGroup.path}.${layerName}.layout.icon-image`;
6627
+ const inspection = inspectIconImageValue(layer.layout["icon-image"]);
6628
+ for (const iconName of inspection.managedIconNames) {
6629
+ if (!available.has(iconName)) {
6630
+ dangling.push({ iconName, kind: "style-override-literal", path });
6631
+ }
6632
+ }
6633
+ if (!inspection.complete) {
6634
+ unanalyzable.push({ kind: "style-override-expression", path });
6635
+ }
6636
+ }
6637
+ }
6638
+ const uniqueDangling = uniqueBy(
6639
+ dangling,
6640
+ (item) => `${item.kind}\0${item.path}\0${item.iconName}`
6641
+ ).sort(compareReferences);
6642
+ const uniqueUnanalyzable = uniqueBy(unanalyzable, (item) => `${item.kind}\0${item.path}`).sort(
6643
+ (left, right) => compareCodeUnits(left.path, right.path)
6644
+ );
6645
+ return {
6646
+ analysisComplete: uniqueUnanalyzable.length === 0,
6647
+ dangling: uniqueDangling,
6648
+ unanalyzable: uniqueUnanalyzable
6649
+ };
6650
+ }
6651
+ function resolveTileflowIconMapping(project, mapName) {
6652
+ const map = Object.hasOwn(project.maps, mapName) ? project.maps[mapName] : void 0;
6653
+ if (!map) {
6654
+ throw new Error(`Unknown Tileflow map: ${mapName}`);
6655
+ }
6656
+ return Object.fromEntries(
6657
+ [...resolveMappingReferences(project, mapName, map.icons).entries()].sort(([left], [right]) => compareCodeUnits(left, right)).map(([key, reference]) => [key, reference.iconName])
6658
+ );
6659
+ }
6660
+ function packageBytes(manifest) {
6661
+ return manifest?.files.reduce((total, file) => total + file.byteLength, 0) ?? 0;
6662
+ }
6663
+ function resolveMappingReferences(project, mapName, icons) {
6664
+ const result = /* @__PURE__ */ new Map();
6665
+ function apply(value, path, rootPath) {
6666
+ if (!value) {
6667
+ return;
6668
+ }
6669
+ if (typeof value === "string") {
6670
+ const referenced = Object.hasOwn(project.icons ?? {}, value) ? project.icons?.[value] : void 0;
6671
+ if (!referenced) {
6672
+ return;
6673
+ }
6674
+ if (rootPath.includes(value)) {
6675
+ throw new Error(`Circular Tileflow icon set extends: ${[...rootPath, value].join(" -> ")}`);
6676
+ }
6677
+ apply(referenced, `icons.${value}`, [...rootPath, value]);
6678
+ return;
6679
+ }
6680
+ if (value.extends) {
6681
+ apply(value.extends, path, rootPath);
6682
+ }
6683
+ for (const [key, iconName] of Object.entries(value.mapping ?? {}).sort(
6684
+ ([left], [right]) => compareCodeUnits(left, right)
6685
+ )) {
6686
+ result.set(key, { iconName, path: `${path}.mapping.${key}` });
6687
+ }
6688
+ }
6689
+ apply(icons, `maps.${mapName}.icons`, []);
6690
+ return result;
6691
+ }
6692
+ function collectThemeLayers(project, mapName, theme, result) {
6693
+ function apply(value, path, rootPath) {
6694
+ if (!value) {
6695
+ return;
6696
+ }
6697
+ if (typeof value === "string") {
6698
+ const custom = Object.hasOwn(project.themes ?? {}, value) ? project.themes?.[value] : void 0;
6699
+ if (!custom) {
6700
+ return;
6701
+ }
6702
+ if (rootPath.includes(value)) {
6703
+ throw new Error(`Circular Tileflow theme extends: ${[...rootPath, value].join(" -> ")}`);
6704
+ }
6705
+ apply(custom, `themes.${value}`, [...rootPath, value]);
6706
+ return;
6707
+ }
6708
+ if (value.extends) {
6709
+ apply(value.extends, path, rootPath);
6710
+ }
6711
+ if (value.layers) {
6712
+ result.push({ layers: value.layers, path: `${path}.layers` });
6713
+ }
6714
+ }
6715
+ apply(theme, `maps.${mapName}.theme`, []);
6716
+ }
6717
+ function inspectIconImageValue(value) {
6718
+ if (typeof value === "string") {
6719
+ const managed = managedIconName(value);
6720
+ return { complete: true, managedIconNames: managed ? [managed] : [] };
6721
+ }
6722
+ if (!Array.isArray(value)) {
6723
+ return { complete: false, managedIconNames: [] };
6724
+ }
6725
+ const managedIconNames = /* @__PURE__ */ new Set();
6726
+ collectManagedImageLiterals(value, managedIconNames);
6727
+ const isSingleLiteralImage = value.length === 2 && (value[0] === "image" || value[0] === "literal") && typeof value[1] === "string";
6728
+ return {
6729
+ complete: isSingleLiteralImage,
6730
+ managedIconNames: [...managedIconNames].sort(compareCodeUnits)
6731
+ };
6732
+ }
6733
+ function collectManagedImageLiterals(value, result) {
6734
+ if (!Array.isArray(value)) {
6735
+ return;
6736
+ }
6737
+ if ((value[0] === "image" || value[0] === "literal") && typeof value[1] === "string") {
6738
+ const managed = managedIconName(value[1]);
6739
+ if (managed) {
6740
+ result.add(managed);
6741
+ }
6742
+ }
6743
+ for (const child of value) {
6744
+ collectManagedImageLiterals(child, result);
6745
+ }
6746
+ }
6747
+ function managedIconName(value) {
6748
+ return value.startsWith("tileflow:") && value.length > "tileflow:".length ? value.slice("tileflow:".length) : null;
6749
+ }
6750
+ function uniqueBy(values, key) {
6751
+ return [...new Map(values.map((value) => [key(value), value])).values()];
6752
+ }
6753
+ function compareReferences(left, right) {
6754
+ return compareCodeUnits(left.path, right.path) || compareCodeUnits(left.kind, right.kind) || compareCodeUnits(left.iconName, right.iconName);
6755
+ }
6756
+
5582
6757
  // src/runtime.ts
5583
6758
  var defaultTileflowManifestUrl = "/tileflow/manifest.json";
5584
6759
  var defaultTileflowStyleBaseUrl = "http://localhost:3333";
@@ -5857,6 +7032,7 @@ function getTileflowLocationOrigin() {
5857
7032
  return window.location.origin;
5858
7033
  }
5859
7034
  export {
7035
+ compareCodeUnits,
5860
7036
  configSchema,
5861
7037
  createManifest,
5862
7038
  createStyle,
@@ -5865,14 +7041,21 @@ export {
5865
7041
  defaultTileflowManifestUrl,
5866
7042
  defaultTileflowStyleBaseUrl,
5867
7043
  defineTileflow,
7044
+ diffTileflowIconMappings,
7045
+ diffTileflowIconPackageManifests,
5868
7046
  getDefaultColors2 as getDefaultColors,
5869
7047
  getDefaultTileflowStyleBaseUrl,
5870
7048
  getTileflowAnalyticsApiUrl,
7049
+ hashTileflowIconPackageManifest,
7050
+ hashTileflowRenderedIconPixels,
5871
7051
  inferTileflowAnalyticsFromStyleUrl,
7052
+ inspectTileflowIconReferences,
5872
7053
  isTileflowLocalDevHost,
5873
7054
  labels,
5874
7055
  loadTileflowManifest,
5875
7056
  mergeTileflowAnalytics,
7057
+ normalizeTileflowCaptureId,
7058
+ normalizeTileflowCaptureScene,
5876
7059
  normalizeTileflowStaticImageSize,
5877
7060
  normalizeTileflowUrl,
5878
7061
  osm,
@@ -5880,17 +7063,43 @@ export {
5880
7063
  parseTileflowProject,
5881
7064
  poi,
5882
7065
  resolveTileflowAnalyticsRequestUrl,
7066
+ resolveTileflowIconMapping,
5883
7067
  resolveTileflowManifestMap,
5884
7068
  resolveTileflowMapMode,
5885
7069
  resolveTileflowRuntimeStyle,
5886
7070
  resolveTileflowStaticImageUrl,
5887
7071
  resolveTileflowStyleUrl,
5888
7072
  roads,
7073
+ serializeCanonicalJson,
7074
+ serializeTileflowIconPackageManifest,
7075
+ sha256Hex,
5889
7076
  shouldLoadTileflowManifest,
5890
7077
  startTileflowSession,
5891
7078
  styleOverride,
7079
+ tileflowCaptureApplicationTargetSchema,
7080
+ tileflowCaptureBoundsCameraSchema,
7081
+ tileflowCaptureCameraSchema,
7082
+ tileflowCaptureCenterCameraSchema,
7083
+ tileflowCaptureIdSchema,
7084
+ tileflowCaptureMapTargetSchema,
7085
+ tileflowCaptureSceneLimits,
7086
+ tileflowCaptureSceneNameSchema,
7087
+ tileflowCaptureSceneSchema,
7088
+ tileflowCaptureSceneSchemaVersion,
7089
+ tileflowCaptureTargetSchema,
7090
+ tileflowCaptureViewportSchema,
7091
+ tileflowHostedAlphaCompatibility,
7092
+ tileflowHostedIconIdPattern,
7093
+ tileflowHostedIconIdSchema,
7094
+ tileflowHostedNotoSansWeights,
7095
+ tileflowIconPackageContentHashSchema,
7096
+ tileflowIconPackageFileNames,
7097
+ tileflowIconPackageLabelSchema,
7098
+ tileflowIconPackageLimits,
7099
+ tileflowIconPackageManifestSchema,
5892
7100
  tileflowMapSchema,
5893
7101
  tileflowProjectSchema,
7102
+ tileflowRenderedIconManifestEntrySchema,
5894
7103
  tileflowThemeSchema,
5895
7104
  validateConfig,
5896
7105
  validateTileflowConfig