@tileflow/core 0.1.0-alpha.7 → 0.1.0-alpha.8

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
@@ -1,203 +1,13 @@
1
1
  // src/basemaps/osm.ts
2
+ var defaultOsmBrightSprite = "https://openmaptiles.github.io/osm-bright-gl-style/sprite";
2
3
  function osm(options = {}) {
3
4
  return {
4
5
  type: "osm",
6
+ sprite: defaultOsmBrightSprite,
5
7
  ...options
6
8
  };
7
9
  }
8
10
 
9
- // src/modules/roads/index.ts
10
- function roads(options = {}) {
11
- return {
12
- type: "roads",
13
- preset: options.preset ?? "modern",
14
- ...options.density ? { density: options.density } : {}
15
- };
16
- }
17
- function resolveRoads(legacyRoads, moduleConfig, fallback) {
18
- if (!moduleConfig) {
19
- return legacyRoads ?? fallback;
20
- }
21
- if (moduleConfig.preset === "minimal" || moduleConfig.density === "clean") {
22
- return "soft";
23
- }
24
- if (moduleConfig.density === "dense") {
25
- return "detailed";
26
- }
27
- return "standard";
28
- }
29
- function roadLayers(source, sourceLayer, colors, roadsMode) {
30
- if (roadsMode === "hidden") {
31
- return [];
32
- }
33
- const opacity = roadsMode === "soft" ? 0.72 : roadsMode === "standard" ? 0.84 : 0.96;
34
- const casingOpacity = roadsMode === "detailed" ? 0.3 : 0.16;
35
- const majorRoadFilter = classFilter(["motorway", "trunk", "primary", "secondary"]);
36
- const ferryFilter = classFilter(["ferry"]);
37
- const pathFilter = classFilter(["path", "track"]);
38
- const railFilter = classFilter(["rail", "transit"]);
39
- const minorRoadFilter = [
40
- "all",
41
- ["!", majorRoadFilter],
42
- ["!", ferryFilter],
43
- ["!", pathFilter],
44
- ["!", railFilter]
45
- ];
46
- const nonTunnelRoadFilter = ["!=", ["get", "brunnel"], "tunnel"];
47
- const roadClassColor = roadClassColorExpression(colors);
48
- return [
49
- {
50
- id: "roads-tunnels-casing",
51
- type: "line",
52
- source,
53
- "source-layer": sourceLayer,
54
- minzoom: 10,
55
- filter: ["==", ["get", "brunnel"], "tunnel"],
56
- paint: {
57
- "line-color": colors.roads.tunnel,
58
- "line-dasharray": [0.6, 0.8],
59
- "line-opacity": 0.28,
60
- "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.5, 14, 1.7, 16, 3.8]
61
- }
62
- },
63
- {
64
- id: "roads-tunnels",
65
- type: "line",
66
- source,
67
- "source-layer": sourceLayer,
68
- minzoom: 10,
69
- filter: ["==", ["get", "brunnel"], "tunnel"],
70
- paint: {
71
- "line-color": roadClassColor,
72
- "line-dasharray": [0.6, 0.8],
73
- "line-opacity": 0.2,
74
- "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.25, 14, 1, 16, 2.4]
75
- }
76
- },
77
- {
78
- id: "roads-casing",
79
- type: "line",
80
- source,
81
- "source-layer": sourceLayer,
82
- minzoom: 6,
83
- filter: nonTunnelRoadFilter,
84
- paint: {
85
- "line-color": colors.roads.casing,
86
- "line-opacity": casingOpacity,
87
- "line-width": ["interpolate", ["linear"], ["zoom"], 6, 0.4, 12, 1.2, 16, 4]
88
- }
89
- },
90
- {
91
- id: "roads-ferry",
92
- type: "line",
93
- source,
94
- "source-layer": sourceLayer,
95
- minzoom: 5,
96
- filter: ["all", nonTunnelRoadFilter, ferryFilter],
97
- paint: {
98
- "line-color": colors.roads.ferry,
99
- "line-dasharray": [1.2, 1.1],
100
- "line-opacity": opacity * 0.8,
101
- "line-width": ["interpolate", ["linear"], ["zoom"], 5, 0.25, 12, 0.8, 16, 2.6]
102
- }
103
- },
104
- {
105
- id: "roads-paths",
106
- type: "line",
107
- source,
108
- "source-layer": sourceLayer,
109
- minzoom: 10,
110
- filter: ["all", nonTunnelRoadFilter, pathFilter],
111
- paint: {
112
- "line-color": colors.roads.path,
113
- "line-dasharray": [0.7, 0.7],
114
- "line-opacity": opacity * 0.68,
115
- "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.25, 14, 0.8, 16, 2.2]
116
- }
117
- },
118
- {
119
- id: "roads-rail",
120
- type: "line",
121
- source,
122
- "source-layer": sourceLayer,
123
- minzoom: 7,
124
- filter: ["all", nonTunnelRoadFilter, railFilter],
125
- paint: {
126
- "line-color": colors.roads.rail,
127
- "line-opacity": opacity * 0.58,
128
- "line-width": ["interpolate", ["linear"], ["zoom"], 7, 0.35, 12, 0.9, 16, 2.4]
129
- }
130
- },
131
- {
132
- id: "roads-minor",
133
- type: "line",
134
- source,
135
- "source-layer": sourceLayer,
136
- minzoom: 6,
137
- filter: ["all", nonTunnelRoadFilter, minorRoadFilter],
138
- paint: {
139
- "line-color": colors.roads.minor,
140
- "line-opacity": opacity * 0.88,
141
- "line-width": ["interpolate", ["linear"], ["zoom"], 6, 0.2, 12, 0.8, 16, 3]
142
- }
143
- },
144
- {
145
- id: "roads-major",
146
- type: "line",
147
- source,
148
- "source-layer": sourceLayer,
149
- minzoom: 5,
150
- filter: ["all", nonTunnelRoadFilter, majorRoadFilter],
151
- paint: {
152
- "line-color": roadClassColor,
153
- "line-opacity": opacity,
154
- "line-width": ["interpolate", ["linear"], ["zoom"], 5, 0.35, 12, 1.2, 16, 4]
155
- }
156
- },
157
- {
158
- id: "roads-bridges",
159
- type: "line",
160
- source,
161
- "source-layer": sourceLayer,
162
- minzoom: 10,
163
- filter: ["==", ["get", "brunnel"], "bridge"],
164
- paint: {
165
- "line-color": colors.roads.bridge,
166
- "line-opacity": opacity,
167
- "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.45, 14, 1.6, 16, 4.4]
168
- }
169
- }
170
- ];
171
- }
172
- function classFilter(classes) {
173
- return ["match", ["get", "class"], classes, true, false];
174
- }
175
- function roadClassColorExpression(colors) {
176
- return [
177
- "match",
178
- ["get", "class"],
179
- "motorway",
180
- colors.roads.motorway,
181
- "trunk",
182
- colors.roads.trunk,
183
- "primary",
184
- colors.roads.primary,
185
- "secondary",
186
- colors.roads.secondary,
187
- "rail",
188
- colors.roads.rail,
189
- "transit",
190
- colors.roads.rail,
191
- "ferry",
192
- colors.roads.ferry,
193
- "path",
194
- colors.roads.path,
195
- "track",
196
- colors.roads.path,
197
- colors.roads.minor
198
- ];
199
- }
200
-
201
11
  // src/themes/index.ts
202
12
  var defaultColors = {
203
13
  background: "#F6F7F3",
@@ -366,12 +176,27 @@ function resolveColors(overrides = {}, defaults = defaultColors, moduleOverrides
366
176
  country: base.text,
367
177
  halo: base.textHalo,
368
178
  muted: base.textMuted,
179
+ neighborhood: base.textMuted,
369
180
  poi: base.textMuted,
370
181
  primary: base.text,
371
182
  road: base.textMuted,
372
183
  settlement: base.text,
373
184
  water: mix(base.water, base.text, 0.42)
374
185
  };
186
+ const poiDefaults = {
187
+ coffee: mix(base.textMuted, base.roadMajor, 0.24),
188
+ culture: mix(base.textMuted, base.text, 0.18),
189
+ education: mix(base.textMuted, base.water, 0.18),
190
+ food: mix(base.textMuted, base.roadMajor, 0.34),
191
+ halo: labelDefaults.halo,
192
+ health: mix(base.textMuted, "#D45E5E", 0.22),
193
+ icon: labelDefaults.poi,
194
+ label: labelDefaults.poi,
195
+ lodging: mix(base.textMuted, base.water, 0.24),
196
+ services: labelDefaults.poi,
197
+ shopping: mix(base.textMuted, base.park, 0.2),
198
+ transit: labelDefaults.road
199
+ };
375
200
  const landuseDefaults = {
376
201
  cemetery: mix(base.park, base.land, 0.35),
377
202
  civic: mix(base.land, base.textMuted, 0.08),
@@ -422,6 +247,7 @@ function resolveColors(overrides = {}, defaults = defaultColors, moduleOverrides
422
247
  landuse: mapColorRecord(
423
248
  resolveGroup(landuseDefaults, defaultModules.landuse, overrideModules.landuse)
424
249
  ),
250
+ poi: mapColorRecord(resolveGroup(poiDefaults, defaultModules.poi, overrideModules.poi)),
425
251
  roads: mapColorRecord(resolveGroup(roadDefaults, defaultModules.roads, overrideModules.roads))
426
252
  };
427
253
  }
@@ -575,6 +401,7 @@ function stripLegacyModuleColors(colors) {
575
401
  labels: _labels,
576
402
  landcover: _landcover,
577
403
  landuse: _landuse,
404
+ poi: _poi,
578
405
  roads: _roads,
579
406
  ...baseColors
580
407
  } = colors;
@@ -591,6 +418,7 @@ function extractLegacyModuleColors(colors) {
591
418
  ...colors.labels ? { labels: colors.labels } : {},
592
419
  ...colors.landcover ? { landcover: colors.landcover } : {},
593
420
  ...colors.landuse ? { landuse: colors.landuse } : {},
421
+ ...colors.poi ? { poi: colors.poi } : {},
594
422
  ...colors.roads ? { roads: colors.roads } : {}
595
423
  };
596
424
  }
@@ -602,6 +430,7 @@ function mergeThemeModules(defaults = {}, overrides = {}) {
602
430
  ...mergeThemeModuleGroup("labels", defaults, overrides),
603
431
  ...mergeThemeModuleGroup("landcover", defaults, overrides),
604
432
  ...mergeThemeModuleGroup("landuse", defaults, overrides),
433
+ ...mergeThemeModuleGroup("poi", defaults, overrides),
605
434
  ...mergeThemeModuleGroup("roads", defaults, overrides)
606
435
  };
607
436
  }
@@ -685,6 +514,437 @@ function rgbToHex(r, g, b) {
685
514
  return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
686
515
  }
687
516
 
517
+ // src/modules/labels/index.ts
518
+ function labels(options = {}) {
519
+ return {
520
+ type: "labels",
521
+ ...options.language ? { language: options.language } : {},
522
+ ...options.places ? { places: options.places } : {},
523
+ ...options.preset ? { preset: options.preset } : {},
524
+ ...options.roads ? { roads: options.roads } : {},
525
+ ...options.water ? { water: options.water } : {}
526
+ };
527
+ }
528
+ function resolveLabels(legacyLabels, moduleConfig, fallback) {
529
+ const mode = moduleConfig?.preset ?? legacyLabels ?? fallback;
530
+ const density = labelDensityFromMode(mode);
531
+ return {
532
+ language: moduleConfig?.language ?? "auto",
533
+ mode,
534
+ places: moduleConfig?.places ?? density,
535
+ roads: moduleConfig?.roads ?? density,
536
+ water: moduleConfig?.water ?? density
537
+ };
538
+ }
539
+ function labelLayers(source, sourceLayer, colors, labelsConfig, typography) {
540
+ if (labelsConfig.mode === "none" || labelsConfig.places === "none") {
541
+ return [];
542
+ }
543
+ const maxRank = labelsConfig.places === "minimal" ? 3 : labelsConfig.places === "balanced" ? 7 : 12;
544
+ return [
545
+ {
546
+ id: "place-labels",
547
+ type: "symbol",
548
+ source,
549
+ "source-layer": sourceLayer,
550
+ minzoom: 3,
551
+ filter: ["<=", ["coalesce", ["get", "rank"], 10], maxRank],
552
+ layout: {
553
+ "text-field": labelTextField(labelsConfig.language),
554
+ "text-font": textFont(typography),
555
+ "text-size": ["interpolate", ["linear"], ["zoom"], 3, 10, 8, 13, 14, 17],
556
+ "text-letter-spacing": 0,
557
+ "text-padding": 4
558
+ },
559
+ paint: {
560
+ "text-color": colors.labels.primary,
561
+ "text-halo-color": colors.labels.halo,
562
+ "text-halo-width": 1.2
563
+ }
564
+ }
565
+ ];
566
+ }
567
+ function labelDensityFromMode(mode) {
568
+ if (mode === "none") return "none";
569
+ if (mode === "essential") return "minimal";
570
+ if (mode === "full") return "detailed";
571
+ return "balanced";
572
+ }
573
+ function labelTextField(language) {
574
+ if (language === "local") {
575
+ return ["coalesce", ["get", "name"], ["get", "name:latin"], ["get", "name:en"]];
576
+ }
577
+ if (language === "auto") {
578
+ return ["coalesce", ["get", "name:latin"], ["get", "name"], ["get", "name:en"]];
579
+ }
580
+ return ["coalesce", ["get", `name:${language}`], ["get", "name:latin"], ["get", "name"]];
581
+ }
582
+
583
+ // src/modules/poi/index.ts
584
+ var defaultPoiMinZoom = {
585
+ none: 24,
586
+ minimal: 15,
587
+ balanced: 13,
588
+ full: 11
589
+ };
590
+ function poi(options = {}) {
591
+ return {
592
+ type: "poi",
593
+ ...options.categories ? { categories: options.categories } : {},
594
+ ...options.icons !== void 0 ? { icons: options.icons } : {},
595
+ ...options.labels ? { labels: options.labels } : {},
596
+ ...options.minZoom !== void 0 ? { minZoom: options.minZoom } : {},
597
+ ...options.preset ? { preset: options.preset } : {}
598
+ };
599
+ }
600
+ function resolvePoi(legacyPoi, moduleConfig, fallback) {
601
+ const mode = moduleConfig?.preset ?? legacyPoi ?? fallback;
602
+ return {
603
+ categories: moduleConfig?.categories,
604
+ icons: moduleConfig?.icons ?? (mode === "none" ? false : "essential"),
605
+ labels: moduleConfig?.labels ?? poiLabelsFromMode(mode),
606
+ minZoom: moduleConfig?.minZoom ?? defaultPoiMinZoom[mode],
607
+ mode
608
+ };
609
+ }
610
+ function poiLayers(source, sourceLayer, colors, poiConfig, typography, customIconSpriteId2, customIconMapping) {
611
+ if (poiConfig.mode === "none") {
612
+ return [];
613
+ }
614
+ const layout = {
615
+ "text-field": labelTextField("auto"),
616
+ "text-font": textFont(typography),
617
+ "text-size": ["interpolate", ["linear"], ["zoom"], poiConfig.minZoom, 10, 17, 12],
618
+ "text-letter-spacing": 0,
619
+ "text-padding": 2
620
+ };
621
+ if (poiConfig.labels === "none") {
622
+ delete layout["text-field"];
623
+ }
624
+ if (poiConfig.icons) {
625
+ layout["icon-image"] = poiIconImageExpression(customIconSpriteId2, customIconMapping);
626
+ layout["icon-optional"] = true;
627
+ layout["icon-padding"] = 2;
628
+ layout["icon-size"] = ["interpolate", ["linear"], ["zoom"], poiConfig.minZoom, 0.85, 17, 1];
629
+ layout["text-anchor"] = "top";
630
+ layout["text-offset"] = [0, 0.75];
631
+ }
632
+ return [
633
+ {
634
+ id: "poi-labels",
635
+ type: "symbol",
636
+ source,
637
+ "source-layer": sourceLayer,
638
+ minzoom: poiConfig.minZoom,
639
+ ...poiConfig.categories ? { filter: poiCategoryFilter(poiConfig.categories) } : {},
640
+ layout,
641
+ paint: {
642
+ "text-color": colors.poi.label,
643
+ "text-halo-color": colors.poi.halo ?? alpha(colors.background, 0.78),
644
+ "text-halo-width": 1
645
+ }
646
+ }
647
+ ];
648
+ }
649
+ function poiLabelsFromMode(mode) {
650
+ if (mode === "none") return "none";
651
+ if (mode === "minimal") return "minimal";
652
+ if (mode === "full") return "full";
653
+ return "balanced";
654
+ }
655
+ function poiCategoryFilter(categories) {
656
+ return ["in", "class", ...categories.flatMap((category) => poiCategoryClasses(category))];
657
+ }
658
+ function poiIconImageExpression(customIconSpriteId2, customIconMapping = {}) {
659
+ const matchParts = [];
660
+ for (const [className, iconName] of Object.entries(poiClassIconNames)) {
661
+ matchParts.push(
662
+ className,
663
+ poiIconImageFallback(iconName, customIconSpriteId2, customIconMapping)
664
+ );
665
+ }
666
+ return [
667
+ "match",
668
+ ["get", "class"],
669
+ ...matchParts,
670
+ poiIconImageFallback("default", customIconSpriteId2, customIconMapping)
671
+ ];
672
+ }
673
+ function poiCategoryClasses(category) {
674
+ return poiCategoryClassNames[category] ?? [category];
675
+ }
676
+ function poiIconImageFallback(iconName, customIconSpriteId2, customIconMapping) {
677
+ const osmBrightIcon = `${osmBrightIconNameForSemanticIcon(iconName)}_11`;
678
+ const customIconName = customIconMapping[iconName] ?? iconName;
679
+ if (!customIconSpriteId2) {
680
+ return ["coalesce", ["image", customIconName], ["image", osmBrightIcon]];
681
+ }
682
+ return [
683
+ "coalesce",
684
+ ["image", `${customIconSpriteId2}:${customIconName}`],
685
+ ["image", osmBrightIcon]
686
+ ];
687
+ }
688
+ function osmBrightIconNameForSemanticIcon(iconName) {
689
+ return semanticToOsmBrightIconName[iconName] ?? "marker";
690
+ }
691
+ var poiCategoryClassNames = {
692
+ coffee: ["cafe"],
693
+ culture: ["museum", "gallery", "theatre", "cinema", "library", "attraction"],
694
+ education: ["school", "college", "university"],
695
+ food: ["restaurant", "fast_food", "bar", "pub", "biergarten"],
696
+ health: ["hospital", "pharmacy", "doctors", "dentist"],
697
+ lodging: ["hotel", "motel", "hostel", "guest_house"],
698
+ services: ["bank", "post", "toilet", "police", "fire_station", "town_hall"],
699
+ shopping: ["shop", "mall", "supermarket", "marketplace"],
700
+ transit: ["railway", "station", "bus", "bus_stop", "subway", "tram"]
701
+ };
702
+ var poiClassIconNames = {
703
+ attraction: "culture",
704
+ bank: "services",
705
+ bar: "food",
706
+ biergarten: "food",
707
+ bus: "transit",
708
+ bus_stop: "transit",
709
+ cafe: "coffee",
710
+ cinema: "culture",
711
+ college: "education",
712
+ dentist: "health",
713
+ doctors: "health",
714
+ fast_food: "food",
715
+ fire_station: "services",
716
+ gallery: "culture",
717
+ guest_house: "lodging",
718
+ hospital: "health",
719
+ hostel: "lodging",
720
+ hotel: "lodging",
721
+ library: "culture",
722
+ mall: "shopping",
723
+ marketplace: "shopping",
724
+ motel: "lodging",
725
+ museum: "culture",
726
+ pharmacy: "health",
727
+ police: "services",
728
+ post: "services",
729
+ pub: "food",
730
+ railway: "transit",
731
+ restaurant: "food",
732
+ school: "education",
733
+ shop: "shopping",
734
+ station: "transit",
735
+ subway: "transit",
736
+ supermarket: "shopping",
737
+ theatre: "culture",
738
+ toilet: "services",
739
+ town_hall: "services",
740
+ tram: "transit",
741
+ university: "education"
742
+ };
743
+ var semanticToOsmBrightIconName = {
744
+ coffee: "cafe",
745
+ culture: "museum",
746
+ default: "marker",
747
+ education: "school",
748
+ food: "restaurant",
749
+ health: "hospital",
750
+ lodging: "lodging",
751
+ services: "marker",
752
+ shopping: "shop",
753
+ transit: "railway"
754
+ };
755
+
756
+ // src/modules/roads/index.ts
757
+ function roads(options = {}) {
758
+ return {
759
+ type: "roads",
760
+ preset: options.preset ?? "modern",
761
+ ...options.density ? { density: options.density } : {}
762
+ };
763
+ }
764
+ function resolveRoads(legacyRoads, moduleConfig, fallback) {
765
+ if (!moduleConfig) {
766
+ return legacyRoads ?? fallback;
767
+ }
768
+ if (moduleConfig.preset === "minimal" || moduleConfig.density === "clean") {
769
+ return "soft";
770
+ }
771
+ if (moduleConfig.density === "dense") {
772
+ return "detailed";
773
+ }
774
+ return "standard";
775
+ }
776
+ function roadLayers(source, sourceLayer, colors, roadsMode) {
777
+ if (roadsMode === "hidden") {
778
+ return [];
779
+ }
780
+ const opacity = roadsMode === "soft" ? 0.72 : roadsMode === "standard" ? 0.84 : 0.96;
781
+ const casingOpacity = roadsMode === "detailed" ? 0.3 : 0.16;
782
+ const majorRoadFilter = classFilter(["motorway", "trunk", "primary", "secondary"]);
783
+ const ferryFilter = classFilter(["ferry"]);
784
+ const pathFilter = classFilter(["path", "track"]);
785
+ const railFilter = classFilter(["rail", "transit"]);
786
+ const minorRoadFilter = [
787
+ "all",
788
+ ["!", majorRoadFilter],
789
+ ["!", ferryFilter],
790
+ ["!", pathFilter],
791
+ ["!", railFilter]
792
+ ];
793
+ const nonTunnelRoadFilter = ["!=", ["get", "brunnel"], "tunnel"];
794
+ const roadClassColor = roadClassColorExpression(colors);
795
+ return [
796
+ {
797
+ id: "roads-tunnels-casing",
798
+ type: "line",
799
+ source,
800
+ "source-layer": sourceLayer,
801
+ minzoom: 10,
802
+ filter: ["==", ["get", "brunnel"], "tunnel"],
803
+ paint: {
804
+ "line-color": colors.roads.tunnel,
805
+ "line-dasharray": [0.6, 0.8],
806
+ "line-opacity": 0.28,
807
+ "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.5, 14, 1.7, 16, 3.8]
808
+ }
809
+ },
810
+ {
811
+ id: "roads-tunnels",
812
+ type: "line",
813
+ source,
814
+ "source-layer": sourceLayer,
815
+ minzoom: 10,
816
+ filter: ["==", ["get", "brunnel"], "tunnel"],
817
+ paint: {
818
+ "line-color": roadClassColor,
819
+ "line-dasharray": [0.6, 0.8],
820
+ "line-opacity": 0.2,
821
+ "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.25, 14, 1, 16, 2.4]
822
+ }
823
+ },
824
+ {
825
+ id: "roads-casing",
826
+ type: "line",
827
+ source,
828
+ "source-layer": sourceLayer,
829
+ minzoom: 6,
830
+ filter: nonTunnelRoadFilter,
831
+ paint: {
832
+ "line-color": colors.roads.casing,
833
+ "line-opacity": casingOpacity,
834
+ "line-width": ["interpolate", ["linear"], ["zoom"], 6, 0.4, 12, 1.2, 16, 4]
835
+ }
836
+ },
837
+ {
838
+ id: "roads-ferry",
839
+ type: "line",
840
+ source,
841
+ "source-layer": sourceLayer,
842
+ minzoom: 5,
843
+ filter: ["all", nonTunnelRoadFilter, ferryFilter],
844
+ paint: {
845
+ "line-color": colors.roads.ferry,
846
+ "line-dasharray": [1.2, 1.1],
847
+ "line-opacity": opacity * 0.8,
848
+ "line-width": ["interpolate", ["linear"], ["zoom"], 5, 0.25, 12, 0.8, 16, 2.6]
849
+ }
850
+ },
851
+ {
852
+ id: "roads-paths",
853
+ type: "line",
854
+ source,
855
+ "source-layer": sourceLayer,
856
+ minzoom: 10,
857
+ filter: ["all", nonTunnelRoadFilter, pathFilter],
858
+ paint: {
859
+ "line-color": colors.roads.path,
860
+ "line-dasharray": [0.7, 0.7],
861
+ "line-opacity": opacity * 0.68,
862
+ "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.25, 14, 0.8, 16, 2.2]
863
+ }
864
+ },
865
+ {
866
+ id: "roads-rail",
867
+ type: "line",
868
+ source,
869
+ "source-layer": sourceLayer,
870
+ minzoom: 7,
871
+ filter: ["all", nonTunnelRoadFilter, railFilter],
872
+ paint: {
873
+ "line-color": colors.roads.rail,
874
+ "line-opacity": opacity * 0.58,
875
+ "line-width": ["interpolate", ["linear"], ["zoom"], 7, 0.35, 12, 0.9, 16, 2.4]
876
+ }
877
+ },
878
+ {
879
+ id: "roads-minor",
880
+ type: "line",
881
+ source,
882
+ "source-layer": sourceLayer,
883
+ minzoom: 6,
884
+ filter: ["all", nonTunnelRoadFilter, minorRoadFilter],
885
+ paint: {
886
+ "line-color": colors.roads.minor,
887
+ "line-opacity": opacity * 0.88,
888
+ "line-width": ["interpolate", ["linear"], ["zoom"], 6, 0.2, 12, 0.8, 16, 3]
889
+ }
890
+ },
891
+ {
892
+ id: "roads-major",
893
+ type: "line",
894
+ source,
895
+ "source-layer": sourceLayer,
896
+ minzoom: 5,
897
+ filter: ["all", nonTunnelRoadFilter, majorRoadFilter],
898
+ paint: {
899
+ "line-color": roadClassColor,
900
+ "line-opacity": opacity,
901
+ "line-width": ["interpolate", ["linear"], ["zoom"], 5, 0.35, 12, 1.2, 16, 4]
902
+ }
903
+ },
904
+ {
905
+ id: "roads-bridges",
906
+ type: "line",
907
+ source,
908
+ "source-layer": sourceLayer,
909
+ minzoom: 10,
910
+ filter: ["==", ["get", "brunnel"], "bridge"],
911
+ paint: {
912
+ "line-color": colors.roads.bridge,
913
+ "line-opacity": opacity,
914
+ "line-width": ["interpolate", ["linear"], ["zoom"], 10, 0.45, 14, 1.6, 16, 4.4]
915
+ }
916
+ }
917
+ ];
918
+ }
919
+ function classFilter(classes) {
920
+ return ["match", ["get", "class"], classes, true, false];
921
+ }
922
+ function roadClassColorExpression(colors) {
923
+ return [
924
+ "match",
925
+ ["get", "class"],
926
+ "motorway",
927
+ colors.roads.motorway,
928
+ "trunk",
929
+ colors.roads.trunk,
930
+ "primary",
931
+ colors.roads.primary,
932
+ "secondary",
933
+ colors.roads.secondary,
934
+ "rail",
935
+ colors.roads.rail,
936
+ "transit",
937
+ colors.roads.rail,
938
+ "ferry",
939
+ colors.roads.ferry,
940
+ "path",
941
+ colors.roads.path,
942
+ "track",
943
+ colors.roads.path,
944
+ colors.roads.minor
945
+ ];
946
+ }
947
+
688
948
  // src/basemaps/osm/layers.ts
689
949
  function openMapTilesLayers(source, sourceLayers, colors, options) {
690
950
  return [
@@ -696,13 +956,27 @@ function openMapTilesLayers(source, sourceLayers, colors, options) {
696
956
  }
697
957
  },
698
958
  fillLayer("landuse", source, sourceLayers.landuse, colors.landuse.residential, 0.42),
699
- fillLayer("parks", source, sourceLayers.park, colors.landcover.park, parkOpacity(options.density)),
959
+ fillLayer(
960
+ "parks",
961
+ source,
962
+ sourceLayers.park,
963
+ colors.landcover.park,
964
+ parkOpacity(options.density)
965
+ ),
700
966
  fillLayer("water", source, sourceLayers.water, colors.hydro.water, 1),
701
967
  ...buildingLayers(source, sourceLayers.building, colors, options.density, options.buildings),
702
968
  ...roadLayers(source, sourceLayers.road, colors, options.roads),
703
969
  ...boundaryLayers(source, sourceLayers.boundary, colors),
704
970
  ...labelLayers(source, sourceLayers.place, colors, options.labels, options.typography),
705
- ...poiLayers(source, sourceLayers.poi, colors, options.poi, options.typography)
971
+ ...poiLayers(
972
+ source,
973
+ sourceLayers.poi,
974
+ colors,
975
+ options.poi,
976
+ options.typography,
977
+ options.customIconSpriteId,
978
+ options.customIconMapping
979
+ )
706
980
  ];
707
981
  }
708
982
  function fillLayer(id, source, sourceLayer, color, opacity) {
@@ -818,61 +1092,6 @@ function boundaryLayers(source, sourceLayer, colors) {
818
1092
  }
819
1093
  ];
820
1094
  }
821
- function labelLayers(source, sourceLayer, colors, labels, typography) {
822
- if (labels === "none") {
823
- return [];
824
- }
825
- const maxRank = labels === "essential" ? 3 : labels === "balanced" ? 7 : 12;
826
- return [
827
- {
828
- id: "place-labels",
829
- type: "symbol",
830
- source,
831
- "source-layer": sourceLayer,
832
- minzoom: 3,
833
- filter: ["<=", ["coalesce", ["get", "rank"], 10], maxRank],
834
- layout: {
835
- "text-field": ["coalesce", ["get", "name:en"], ["get", "name"]],
836
- "text-font": textFont(typography),
837
- "text-size": ["interpolate", ["linear"], ["zoom"], 3, 10, 8, 13, 14, 17],
838
- "text-letter-spacing": 0,
839
- "text-padding": 4
840
- },
841
- paint: {
842
- "text-color": colors.labels.primary,
843
- "text-halo-color": colors.labels.halo,
844
- "text-halo-width": 1.2
845
- }
846
- }
847
- ];
848
- }
849
- function poiLayers(source, sourceLayer, colors, poi, typography) {
850
- if (poi === "none") {
851
- return [];
852
- }
853
- const minzoom = poi === "minimal" ? 15 : poi === "balanced" ? 13 : 11;
854
- return [
855
- {
856
- id: "poi-labels",
857
- type: "symbol",
858
- source,
859
- "source-layer": sourceLayer,
860
- minzoom,
861
- layout: {
862
- "text-field": ["coalesce", ["get", "name:en"], ["get", "name"]],
863
- "text-font": textFont(typography),
864
- "text-size": ["interpolate", ["linear"], ["zoom"], minzoom, 10, 17, 12],
865
- "text-letter-spacing": 0,
866
- "text-padding": 2
867
- },
868
- paint: {
869
- "text-color": colors.labels.poi,
870
- "text-halo-color": alpha(colors.background, 0.78),
871
- "text-halo-width": 1
872
- }
873
- }
874
- ];
875
- }
876
1095
  function parkOpacity(density) {
877
1096
  return density === "clean" ? 0.64 : density === "balanced" ? 0.78 : 0.9;
878
1097
  }
@@ -983,6 +1202,7 @@ var colorConfigSchema = z.object({
983
1202
  "country",
984
1203
  "halo",
985
1204
  "muted",
1205
+ "neighborhood",
986
1206
  "poi",
987
1207
  "primary",
988
1208
  "road",
@@ -997,6 +1217,20 @@ var colorConfigSchema = z.object({
997
1217
  "industrial",
998
1218
  "residential"
999
1219
  ]).optional(),
1220
+ poi: colorGroupSchema([
1221
+ "coffee",
1222
+ "culture",
1223
+ "education",
1224
+ "food",
1225
+ "halo",
1226
+ "health",
1227
+ "icon",
1228
+ "label",
1229
+ "lodging",
1230
+ "services",
1231
+ "shopping",
1232
+ "transit"
1233
+ ]).optional(),
1000
1234
  roads: colorGroupSchema([
1001
1235
  "bridge",
1002
1236
  "casing",
@@ -1019,6 +1253,7 @@ var themeModulesSchema = z.object({
1019
1253
  "country",
1020
1254
  "halo",
1021
1255
  "muted",
1256
+ "neighborhood",
1022
1257
  "poi",
1023
1258
  "primary",
1024
1259
  "road",
@@ -1033,6 +1268,20 @@ var themeModulesSchema = z.object({
1033
1268
  "industrial",
1034
1269
  "residential"
1035
1270
  ]).optional(),
1271
+ poi: colorGroupSchema([
1272
+ "coffee",
1273
+ "culture",
1274
+ "education",
1275
+ "food",
1276
+ "halo",
1277
+ "health",
1278
+ "icon",
1279
+ "label",
1280
+ "lodging",
1281
+ "services",
1282
+ "shopping",
1283
+ "transit"
1284
+ ]).optional(),
1036
1285
  roads: colorGroupSchema([
1037
1286
  "bridge",
1038
1287
  "casing",
@@ -1103,12 +1352,43 @@ var roadsModuleSchema = z.object({
1103
1352
  density: z.enum(["clean", "balanced", "dense"]).optional(),
1104
1353
  preset: z.enum(["modern", "minimal"]).optional()
1105
1354
  }).strict();
1355
+ var labelDensitySchema = z.enum(["none", "minimal", "balanced", "detailed"]);
1356
+ var labelsModuleSchema = z.object({
1357
+ type: z.literal("labels"),
1358
+ language: z.string().trim().min(1).optional(),
1359
+ places: labelDensitySchema.optional(),
1360
+ preset: z.enum(["none", "essential", "balanced", "full"]).optional(),
1361
+ roads: labelDensitySchema.optional(),
1362
+ water: labelDensitySchema.optional()
1363
+ }).strict();
1364
+ var poiModuleSchema = z.object({
1365
+ type: z.literal("poi"),
1366
+ categories: z.array(identifierSchema).optional(),
1367
+ icons: z.union([z.boolean(), z.enum(["essential", "full"])]).optional(),
1368
+ labels: z.enum(["none", "minimal", "balanced", "full"]).optional(),
1369
+ minZoom: z.number().finite().min(0).max(24).optional(),
1370
+ preset: z.enum(["none", "minimal", "balanced", "full"]).optional()
1371
+ }).strict();
1106
1372
  var styleOverrideModuleSchema = z.object({
1107
1373
  type: z.literal("styleOverride"),
1108
1374
  layers: z.record(z.string(), styleLayerOverrideSchema).optional(),
1109
1375
  removeLayers: z.array(z.string().min(1)).optional()
1110
1376
  }).strict();
1111
- var moduleSchema = z.discriminatedUnion("type", [roadsModuleSchema, styleOverrideModuleSchema]);
1377
+ var moduleSchema = z.discriminatedUnion("type", [
1378
+ labelsModuleSchema,
1379
+ poiModuleSchema,
1380
+ roadsModuleSchema,
1381
+ styleOverrideModuleSchema
1382
+ ]);
1383
+ var iconSetSchema = z.union([
1384
+ z.string().trim().min(1),
1385
+ z.object({
1386
+ extends: z.string().trim().min(1).optional(),
1387
+ mapping: z.record(identifierSchema, z.string().trim().min(1)).optional(),
1388
+ source: z.string().trim().min(1).optional(),
1389
+ sprite: z.string().trim().min(1).optional()
1390
+ }).strict()
1391
+ ]);
1112
1392
  var viewSchema = z.object({
1113
1393
  bearing: z.number().finite().min(-180).max(180).optional(),
1114
1394
  center: z.tuple([z.number().finite(), z.number().finite()]).refine(([lng, lat]) => lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90, {
@@ -1148,6 +1428,7 @@ var tileflowMapSchema = z.object({
1148
1428
  buildings: z.enum(["hidden", "flat", "3d"]).optional(),
1149
1429
  terrain: z.union([z.enum(["none", "hillshade", "3d"]), terrainConfigSchema]).optional(),
1150
1430
  glyphs: z.string().min(1).optional(),
1431
+ icons: iconSetSchema.optional(),
1151
1432
  sprite: z.string().min(1).optional(),
1152
1433
  tiles: tilesSchema.optional(),
1153
1434
  view: viewSchema.optional(),
@@ -1161,6 +1442,7 @@ var tilesetConfigSchema = z.object({
1161
1442
  sourceLayers: sourceLayersSchema.optional()
1162
1443
  }).strict();
1163
1444
  var tileflowProjectSchema = z.object({
1445
+ icons: z.record(identifierSchema, iconSetSchema).optional(),
1164
1446
  themes: z.record(identifierSchema, themeConfigSchema).optional(),
1165
1447
  tilesets: z.record(identifierSchema, tilesetConfigSchema).optional(),
1166
1448
  maps: z.record(identifierSchema, configSchema).refine((maps) => {
@@ -1229,10 +1511,11 @@ function isProjectConfigLike(input) {
1229
1511
  function validateProjectThemeReferences(project) {
1230
1512
  const themeNames = new Set(Object.keys(project.themes ?? {}));
1231
1513
  const themeMessages = validateThemeExtends(project.themes, themeNames);
1514
+ const iconMessages = validateProjectIconReferences(project);
1232
1515
  const mapMessages = Object.entries(project.maps).flatMap(
1233
1516
  ([mapName, mapConfig]) => validateMapThemeReference(mapConfig, themeNames, `maps.${mapName}.theme`)
1234
1517
  );
1235
- return [...themeMessages, ...mapMessages];
1518
+ return [...themeMessages, ...iconMessages, ...mapMessages];
1236
1519
  }
1237
1520
  function validateMapThemeReference(config, themeNames, path) {
1238
1521
  if (!config.theme) {
@@ -1297,6 +1580,242 @@ function findThemeCycle(themeName, themes, path) {
1297
1580
  }
1298
1581
  return findThemeCycle(nextThemeName, themes, [...path, themeName]);
1299
1582
  }
1583
+ function validateProjectIconReferences(project) {
1584
+ const messages = [];
1585
+ const iconSets = project.icons ?? {};
1586
+ for (const [iconSetName, iconSet] of Object.entries(iconSets)) {
1587
+ const extendedName = iconSetReference(iconSet);
1588
+ if (extendedName && !iconSets[extendedName]) {
1589
+ messages.push({
1590
+ level: "error",
1591
+ path: iconSetExtendsPath(`icons.${iconSetName}`, iconSet),
1592
+ message: `Unknown Tileflow icon set "${extendedName}"`
1593
+ });
1594
+ }
1595
+ }
1596
+ for (const iconSetName of Object.keys(iconSets)) {
1597
+ const cycle = findIconSetCycle(iconSetName, iconSets, []);
1598
+ if (cycle) {
1599
+ messages.push({
1600
+ level: "error",
1601
+ path: iconSetExtendsPath(`icons.${iconSetName}`, iconSets[iconSetName]),
1602
+ message: `Circular Tileflow icon set extends: ${cycle.join(" -> ")}`
1603
+ });
1604
+ }
1605
+ }
1606
+ for (const [mapName, mapConfig] of Object.entries(project.maps)) {
1607
+ if (!isIconSetObject(mapConfig.icons) || !mapConfig.icons.extends) {
1608
+ continue;
1609
+ }
1610
+ if (!iconSets[mapConfig.icons.extends]) {
1611
+ messages.push({
1612
+ level: "error",
1613
+ path: `maps.${mapName}.icons.extends`,
1614
+ message: `Unknown Tileflow icon set "${mapConfig.icons.extends}"`
1615
+ });
1616
+ }
1617
+ }
1618
+ return messages;
1619
+ }
1620
+ function findIconSetCycle(iconSetName, iconSets, path) {
1621
+ if (path.includes(iconSetName)) {
1622
+ return [...path, iconSetName];
1623
+ }
1624
+ const nextIconSetName = iconSetReference(iconSets?.[iconSetName]);
1625
+ if (!nextIconSetName || !iconSets?.[nextIconSetName]) {
1626
+ return void 0;
1627
+ }
1628
+ return findIconSetCycle(nextIconSetName, iconSets, [...path, iconSetName]);
1629
+ }
1630
+ function iconSetReference(iconSet) {
1631
+ if (typeof iconSet === "string") {
1632
+ return iconSet;
1633
+ }
1634
+ return isIconSetObject(iconSet) ? iconSet.extends : void 0;
1635
+ }
1636
+ function iconSetExtendsPath(basePath, iconSet) {
1637
+ return isIconSetObject(iconSet) ? `${basePath}.extends` : basePath;
1638
+ }
1639
+ function isIconSetObject(value) {
1640
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1641
+ }
1642
+
1643
+ // src/terrain/index.ts
1644
+ function resolveTerrain(terrain, tileBaseUrl) {
1645
+ if (!terrain || terrain === "none") {
1646
+ return void 0;
1647
+ }
1648
+ const terrainConfig = typeof terrain === "string" ? { mode: terrain } : terrain;
1649
+ const mode = terrainConfig.mode ?? "hillshade";
1650
+ if (mode === "none") {
1651
+ return void 0;
1652
+ }
1653
+ const sourceId = terrainConfig.sourceId ?? "tileflow-terrain";
1654
+ const tileset = terrainConfig.tileset ?? "terrain";
1655
+ const url = terrainConfig.url ?? `${tileBaseUrl}/tiles/${tileset}/tiles.json`;
1656
+ const encoding = terrainConfig.encoding ?? "terrarium";
1657
+ return {
1658
+ exaggeration: terrainConfig.exaggeration ?? 1.2,
1659
+ mode,
1660
+ sourceId,
1661
+ source: {
1662
+ ...terrainConfig.attribution ? { attribution: terrainConfig.attribution } : {},
1663
+ encoding,
1664
+ type: "raster-dem",
1665
+ url
1666
+ }
1667
+ };
1668
+ }
1669
+ function withTerrainLayers(layers, terrain) {
1670
+ const hillshadeLayer = {
1671
+ id: "terrain-hillshade",
1672
+ type: "hillshade",
1673
+ source: terrain.sourceId,
1674
+ maxzoom: 15,
1675
+ paint: {
1676
+ "hillshade-accent-color": "rgba(255, 255, 255, 0.18)",
1677
+ "hillshade-exaggeration": terrain.mode === "3d" ? 0.24 : 0.42,
1678
+ "hillshade-highlight-color": "rgba(255, 255, 255, 0.28)",
1679
+ "hillshade-shadow-color": "rgba(38, 44, 50, 0.34)"
1680
+ }
1681
+ };
1682
+ const insertionIndex = layers.findIndex((layer) => layer.id === "water");
1683
+ if (insertionIndex === -1) {
1684
+ return [hillshadeLayer, ...layers];
1685
+ }
1686
+ return [
1687
+ ...layers.slice(0, insertionIndex + 1),
1688
+ hillshadeLayer,
1689
+ ...layers.slice(insertionIndex + 1)
1690
+ ];
1691
+ }
1692
+
1693
+ // src/templates/osm-bright/controls.ts
1694
+ function applyOsmBrightControls(layers, options) {
1695
+ return layers.map((layer) => {
1696
+ const id = stringValue(layer.id);
1697
+ const sourceLayer = stringValue(layer["source-layer"]);
1698
+ const type = stringValue(layer.type);
1699
+ if (type !== "symbol") {
1700
+ return layer;
1701
+ }
1702
+ if (isPoiLayer(id, sourceLayer)) {
1703
+ return applyPoiLayerControls(
1704
+ layer,
1705
+ options.poi,
1706
+ options.customIconSpriteId,
1707
+ options.customIconMapping
1708
+ );
1709
+ }
1710
+ if (isLabelLayer(id, sourceLayer)) {
1711
+ return applyLabelLayerControls(layer, options.labels);
1712
+ }
1713
+ return layer;
1714
+ });
1715
+ }
1716
+ function applyLabelLayerControls(layer, labels2) {
1717
+ const id = stringValue(layer.id);
1718
+ const sourceLayer = stringValue(layer["source-layer"]);
1719
+ const group = labelGroup(id, sourceLayer);
1720
+ const density = labelGroupDensity(labels2, group);
1721
+ if (labels2.mode === "none" || density === "none") {
1722
+ return setLayerVisibility(layer, "none");
1723
+ }
1724
+ const nextLayer = setLayerVisibility(layer, "visible");
1725
+ const layout = isRecord3(nextLayer.layout) ? { ...nextLayer.layout } : {};
1726
+ if ("text-field" in layout && !isHighwayShieldLayer(id)) {
1727
+ layout["text-field"] = labelTextField(labels2.language);
1728
+ }
1729
+ if (density === "minimal") {
1730
+ nextLayer.minzoom = Math.max(numberValue(nextLayer.minzoom) ?? 0, minimalMinZoom(group));
1731
+ }
1732
+ if (density === "detailed") {
1733
+ const currentMinZoom = numberValue(nextLayer.minzoom);
1734
+ if (currentMinZoom !== void 0) {
1735
+ nextLayer.minzoom = Math.max(0, currentMinZoom - 1);
1736
+ }
1737
+ }
1738
+ nextLayer.layout = layout;
1739
+ return nextLayer;
1740
+ }
1741
+ function applyPoiLayerControls(layer, poi2, customIconSpriteId2, customIconMapping) {
1742
+ if (poi2.mode === "none") {
1743
+ return setLayerVisibility(layer, "none");
1744
+ }
1745
+ const nextLayer = setLayerVisibility(layer, "visible");
1746
+ const layout = isRecord3(nextLayer.layout) ? { ...nextLayer.layout } : {};
1747
+ nextLayer.minzoom = Math.max(numberValue(nextLayer.minzoom) ?? 0, poi2.minZoom);
1748
+ if (poi2.categories && sourceLayerValue(nextLayer) === "poi") {
1749
+ nextLayer.filter = combineFilters(nextLayer.filter, poiCategoryFilter(poi2.categories));
1750
+ }
1751
+ if (poi2.labels === "none") {
1752
+ delete layout["text-field"];
1753
+ } else if ("text-field" in layout) {
1754
+ layout["text-field"] = labelTextField("auto");
1755
+ }
1756
+ if (poi2.icons === false) {
1757
+ delete layout["icon-image"];
1758
+ } else if ("icon-image" in layout || customIconSpriteId2) {
1759
+ layout["icon-image"] = poiIconImageExpression(customIconSpriteId2, customIconMapping);
1760
+ layout["icon-optional"] = true;
1761
+ }
1762
+ nextLayer.layout = layout;
1763
+ return nextLayer;
1764
+ }
1765
+ function labelGroupDensity(labels2, group) {
1766
+ if (group === "roads") return labels2.roads;
1767
+ if (group === "water") return labels2.water;
1768
+ return labels2.places;
1769
+ }
1770
+ function labelGroup(id, sourceLayer) {
1771
+ if (sourceLayer === "transportation_name" || id.includes("highway-name") || id.includes("highway-shield")) {
1772
+ return "roads";
1773
+ }
1774
+ if (sourceLayer === "water_name" || sourceLayer === "waterway" || id.includes("water-name")) {
1775
+ return "water";
1776
+ }
1777
+ return "places";
1778
+ }
1779
+ function minimalMinZoom(group) {
1780
+ if (group === "roads") return 13;
1781
+ if (group === "water") return 5;
1782
+ return 4;
1783
+ }
1784
+ function isLabelLayer(id, sourceLayer) {
1785
+ return sourceLayer === "place" || sourceLayer === "transportation_name" || sourceLayer === "water_name" || sourceLayer === "waterway" || id.includes("highway-name") || id.includes("highway-shield") || id.includes("water-name");
1786
+ }
1787
+ function isPoiLayer(id, sourceLayer) {
1788
+ return sourceLayer === "poi" || id.startsWith("poi-");
1789
+ }
1790
+ function isHighwayShieldLayer(id) {
1791
+ return id.includes("highway-shield");
1792
+ }
1793
+ function setLayerVisibility(layer, visibility) {
1794
+ const layout = isRecord3(layer.layout) ? { ...layer.layout } : {};
1795
+ layout.visibility = visibility;
1796
+ return {
1797
+ ...layer,
1798
+ layout
1799
+ };
1800
+ }
1801
+ function combineFilters(first, second) {
1802
+ if (!first) {
1803
+ return second;
1804
+ }
1805
+ return ["all", first, second];
1806
+ }
1807
+ function sourceLayerValue(layer) {
1808
+ return stringValue(layer["source-layer"]);
1809
+ }
1810
+ function stringValue(value) {
1811
+ return typeof value === "string" ? value : "";
1812
+ }
1813
+ function numberValue(value) {
1814
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1815
+ }
1816
+ function isRecord3(value) {
1817
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
1818
+ }
1300
1819
 
1301
1820
  // src/templates/osm-bright/style.json
1302
1821
  var style_default = {
@@ -3860,68 +4379,18 @@ var style_default = {
3860
4379
  id: "bright"
3861
4380
  };
3862
4381
 
3863
- // src/terrain/index.ts
3864
- function resolveTerrain(terrain, tileBaseUrl) {
3865
- if (!terrain || terrain === "none") {
3866
- return void 0;
3867
- }
3868
- const terrainConfig = typeof terrain === "string" ? { mode: terrain } : terrain;
3869
- const mode = terrainConfig.mode ?? "hillshade";
3870
- if (mode === "none") {
3871
- return void 0;
3872
- }
3873
- const sourceId = terrainConfig.sourceId ?? "tileflow-terrain";
3874
- const tileset = terrainConfig.tileset ?? "terrain";
3875
- const url = terrainConfig.url ?? `${tileBaseUrl}/tiles/${tileset}/tiles.json`;
3876
- const encoding = terrainConfig.encoding ?? "terrarium";
3877
- return {
3878
- exaggeration: terrainConfig.exaggeration ?? 1.2,
3879
- mode,
3880
- sourceId,
3881
- source: {
3882
- ...terrainConfig.attribution ? { attribution: terrainConfig.attribution } : {},
3883
- encoding,
3884
- type: "raster-dem",
3885
- url
3886
- }
3887
- };
3888
- }
3889
- function withTerrainLayers(layers, terrain) {
3890
- const hillshadeLayer = {
3891
- id: "terrain-hillshade",
3892
- type: "hillshade",
3893
- source: terrain.sourceId,
3894
- maxzoom: 15,
3895
- paint: {
3896
- "hillshade-accent-color": "rgba(255, 255, 255, 0.18)",
3897
- "hillshade-exaggeration": terrain.mode === "3d" ? 0.24 : 0.42,
3898
- "hillshade-highlight-color": "rgba(255, 255, 255, 0.28)",
3899
- "hillshade-shadow-color": "rgba(38, 44, 50, 0.34)"
3900
- }
3901
- };
3902
- const insertionIndex = layers.findIndex((layer) => layer.id === "water");
3903
- if (insertionIndex === -1) {
3904
- return [hillshadeLayer, ...layers];
3905
- }
3906
- return [
3907
- ...layers.slice(0, insertionIndex + 1),
3908
- hillshadeLayer,
3909
- ...layers.slice(insertionIndex + 1)
3910
- ];
3911
- }
3912
-
3913
4382
  // src/templates/osm-bright/theme.ts
3914
4383
  function applyOsmBrightTheme(layers, colors, theme, typography) {
3915
4384
  if (!theme.custom && theme.name === "light") {
3916
4385
  return layers;
3917
4386
  }
3918
4387
  return layers.map((layer) => {
3919
- const type = stringValue(layer.type);
3920
- const sourceLayer = stringValue(layer["source-layer"]);
3921
- const id = stringValue(layer.id);
4388
+ const type = stringValue2(layer.type);
4389
+ const sourceLayer = stringValue2(layer["source-layer"]);
4390
+ const id = stringValue2(layer.id);
3922
4391
  const nextLayer = { ...layer };
3923
- const paint = isRecord3(layer.paint) ? { ...layer.paint } : {};
3924
- const layout = isRecord3(layer.layout) ? { ...layer.layout } : {};
4392
+ const paint = isRecord4(layer.paint) ? { ...layer.paint } : {};
4393
+ const layout = isRecord4(layer.layout) ? { ...layer.layout } : {};
3925
4394
  let paintChanged = false;
3926
4395
  let layoutChanged = false;
3927
4396
  if (type === "background" && hasBaseColor(theme, ["background", "canvas"])) {
@@ -4032,6 +4501,9 @@ function osmBrightTextColor(id, sourceLayer, colors, theme) {
4032
4501
  return shouldThemeLabelColor(theme, "road") ? colors.labels.road : void 0;
4033
4502
  }
4034
4503
  if (sourceLayer === "poi" || id.includes("poi")) {
4504
+ if (hasNestedModuleColor(theme, "poi", "label")) {
4505
+ return colors.poi.label;
4506
+ }
4035
4507
  return shouldThemeLabelColor(theme, "poi") ? colors.labels.poi : void 0;
4036
4508
  }
4037
4509
  if (id.includes("country")) {
@@ -4167,10 +4639,10 @@ function hasNestedModuleColor(theme, group, key) {
4167
4639
  }
4168
4640
  return typeof value[key] === "string";
4169
4641
  }
4170
- function isRecord3(value) {
4642
+ function isRecord4(value) {
4171
4643
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
4172
4644
  }
4173
- function stringValue(value) {
4645
+ function stringValue2(value) {
4174
4646
  return typeof value === "string" ? value : "";
4175
4647
  }
4176
4648
 
@@ -4184,7 +4656,13 @@ function createOsmBrightStyle(options) {
4184
4656
  options.theme,
4185
4657
  options.typography
4186
4658
  );
4187
- const resolvedLayers = options.terrain ? withTerrainLayers(themedLayers, options.terrain) : themedLayers;
4659
+ const controlledLayers = applyOsmBrightControls(themedLayers, {
4660
+ customIconMapping: options.customIconMapping,
4661
+ customIconSpriteId: options.customIconSpriteId,
4662
+ labels: options.labelsModule,
4663
+ poi: options.poiModule
4664
+ });
4665
+ const resolvedLayers = options.terrain ? withTerrainLayers(controlledLayers, options.terrain) : controlledLayers;
4188
4666
  return {
4189
4667
  ...template,
4190
4668
  name: options.name,
@@ -4246,6 +4724,7 @@ var defaultConfig = {
4246
4724
  };
4247
4725
  var defaultTileBaseUrl = "https://api.tileflow.dev";
4248
4726
  var defaultTileset = "world";
4727
+ var customIconSpriteId = "tileflow";
4249
4728
  function createStyle(input = {}, options = {}) {
4250
4729
  const parsed = configSchema.safeParse(input);
4251
4730
  if (!parsed.success) {
@@ -4278,9 +4757,21 @@ function createStyle(input = {}, options = {}) {
4278
4757
  const roadModule = config.modules?.find(
4279
4758
  (moduleConfig) => moduleConfig.type === "roads"
4280
4759
  );
4760
+ const labelsModule = config.modules?.find(
4761
+ (moduleConfig) => moduleConfig.type === "labels"
4762
+ );
4763
+ const poiModule = config.modules?.find(
4764
+ (moduleConfig) => moduleConfig.type === "poi"
4765
+ );
4281
4766
  const roads2 = resolveRoads(config.roads, roadModule, theme.preset.roads ?? defaultConfig.roads);
4282
- const labels = config.labels ?? theme.preset.labels ?? defaultConfig.labels;
4283
- const poi = config.poi ?? theme.preset.poi ?? defaultConfig.poi;
4767
+ const resolvedLabels = resolveLabels(
4768
+ config.labels,
4769
+ labelsModule,
4770
+ theme.preset.labels ?? defaultConfig.labels
4771
+ );
4772
+ const resolvedPoi = resolvePoi(config.poi, poiModule, theme.preset.poi ?? defaultConfig.poi);
4773
+ const labels2 = resolvedLabels.mode;
4774
+ const poi2 = resolvedPoi.mode;
4284
4775
  const buildings = config.buildings ?? "flat";
4285
4776
  const colors = resolveColors(config.colors, theme.colors, {}, theme.modules);
4286
4777
  const sourceId = config.tiles?.sourceId ?? basemap.sourceId ?? "tileflow";
@@ -4290,7 +4781,10 @@ function createStyle(input = {}, options = {}) {
4290
4781
  const typography = resolveTypography(config.typography, theme.typography);
4291
4782
  const terrain = resolveTerrain(config.terrain, tileBaseUrl);
4292
4783
  const glyphs = config.glyphs ?? basemap.glyphs ?? `${tileBaseUrl}/fonts/{fontstack}/{range}.pbf`;
4293
- const sprite = config.sprite ?? basemap.sprite;
4784
+ const iconSet = resolveIconSet(config.icons, options.iconSets);
4785
+ const iconSprite = iconSet?.sprite;
4786
+ const sprite = config.sprite ?? resolveSprite(basemap.sprite, iconSprite);
4787
+ const customIconSpriteName = Array.isArray(sprite) && iconSprite ? customIconSpriteId : void 0;
4294
4788
  const attribution = config.tiles?.attribution ?? basemap.attribution ?? "\xA9 OpenMapTiles \xA9 OpenStreetMap contributors";
4295
4789
  const metadata = {
4296
4790
  "tileflow:basemap": basemap.type,
@@ -4298,8 +4792,15 @@ function createStyle(input = {}, options = {}) {
4298
4792
  "tileflow:themeMode": theme.mode,
4299
4793
  "tileflow:density": density,
4300
4794
  "tileflow:roads": roads2,
4301
- "tileflow:labels": labels,
4302
- "tileflow:poi": poi,
4795
+ "tileflow:labels": labels2,
4796
+ "tileflow:labelsModule": labelsModule,
4797
+ "tileflow:poi": poi2,
4798
+ "tileflow:poiModule": poiModule,
4799
+ "tileflow:icons": iconSet ? {
4800
+ customSpriteId: customIconSpriteName,
4801
+ mapping: iconSet.mapping,
4802
+ sprite: iconSprite
4803
+ } : void 0,
4303
4804
  "tileflow:buildings": buildings,
4304
4805
  "tileflow:modules": config.modules?.map((moduleConfig) => moduleConfig.type) ?? [],
4305
4806
  "tileflow:themeModules": Object.keys(theme.moduleOverrides),
@@ -4327,7 +4828,13 @@ function createStyle(input = {}, options = {}) {
4327
4828
  terrain,
4328
4829
  theme,
4329
4830
  tilesUrl,
4330
- typography
4831
+ typography,
4832
+ labels: labels2,
4833
+ labelsModule: resolvedLabels,
4834
+ poi: poi2,
4835
+ poiModule: resolvedPoi,
4836
+ customIconMapping: iconSet?.mapping,
4837
+ customIconSpriteId: customIconSpriteName
4331
4838
  });
4332
4839
  return {
4333
4840
  ...style2,
@@ -4345,10 +4852,12 @@ function createStyle(input = {}, options = {}) {
4345
4852
  const layers = openMapTilesLayers(sourceId, sourceLayers, colors, {
4346
4853
  buildings,
4347
4854
  density,
4348
- labels,
4349
- poi,
4855
+ labels: resolvedLabels,
4856
+ poi: resolvedPoi,
4350
4857
  roads: roads2,
4351
- typography
4858
+ typography,
4859
+ customIconMapping: iconSet?.mapping,
4860
+ customIconSpriteId: customIconSpriteName
4352
4861
  });
4353
4862
  const terrainLayers = terrain ? withTerrainLayers(layers, terrain) : layers;
4354
4863
  const resolvedLayers = applyStyleOverrides(terrainLayers, styleOverrideModules);
@@ -4408,6 +4917,7 @@ function createStyleFromProject(projectConfig, mapName, options = {}) {
4408
4917
  },
4409
4918
  {
4410
4919
  ...options,
4920
+ iconSets: projectConfig.icons,
4411
4921
  themes: projectConfig.themes,
4412
4922
  tileset: resolvedTileset
4413
4923
  }
@@ -4440,7 +4950,9 @@ function resolveSourceLayers(mapLayers, tilesetLayers) {
4440
4950
  }
4441
4951
  function shouldUseOsmBrightTemplate(config, basemap, theme) {
4442
4952
  const templateTheme = theme.custom || theme.name === "light" || theme.name === "dark";
4443
- return basemap.type === "osm" && templateTheme && !hasEffectiveColorOverrides(config.colors) && !config.typography && !config.density && !config.roads && !config.labels && !config.poi && !config.buildings && !config.modules?.some((moduleConfig) => moduleConfig.type !== "styleOverride") && !basemap.sourceLayers && !config.tiles?.sourceLayers;
4953
+ return basemap.type === "osm" && templateTheme && !hasEffectiveColorOverrides(config.colors) && !config.typography && !config.density && !config.roads && !config.buildings && !config.modules?.some(
4954
+ (moduleConfig) => moduleConfig.type !== "styleOverride" && moduleConfig.type !== "labels" && moduleConfig.type !== "poi"
4955
+ ) && !basemap.sourceLayers && !config.tiles?.sourceLayers;
4444
4956
  }
4445
4957
  function hasEffectiveColorOverrides(colors) {
4446
4958
  if (!colors) {
@@ -4450,13 +4962,13 @@ function hasEffectiveColorOverrides(colors) {
4450
4962
  if (key === "accent" || value === void 0) {
4451
4963
  return false;
4452
4964
  }
4453
- if (isRecord4(value)) {
4965
+ if (isRecord5(value)) {
4454
4966
  return Object.values(value).some((nestedValue) => typeof nestedValue === "string");
4455
4967
  }
4456
4968
  return typeof value === "string";
4457
4969
  });
4458
4970
  }
4459
- function isRecord4(value) {
4971
+ function isRecord5(value) {
4460
4972
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
4461
4973
  }
4462
4974
  function normalizeBaseUrl(value) {
@@ -4468,6 +4980,48 @@ function normalizeStyleBaseUrl(value) {
4468
4980
  function normalizeUrl(value) {
4469
4981
  return value.replace(/\/+$/, "");
4470
4982
  }
4983
+ function resolveIconSet(icons, iconSets, path = []) {
4984
+ if (!icons) {
4985
+ return void 0;
4986
+ }
4987
+ if (typeof icons === "string") {
4988
+ const referenced = iconSets?.[icons];
4989
+ if (referenced) {
4990
+ if (path.includes(icons)) {
4991
+ throw new Error(`Circular Tileflow icon set extends: ${[...path, icons].join(" -> ")}`);
4992
+ }
4993
+ return resolveIconSet(referenced, iconSets, [...path, icons]);
4994
+ }
4995
+ if (isSpriteReference(icons)) {
4996
+ return { sprite: icons };
4997
+ }
4998
+ return { source: icons };
4999
+ }
5000
+ const extended = icons.extends ? resolveIconSet(icons.extends, iconSets, path) : void 0;
5001
+ return {
5002
+ ...extended,
5003
+ ...icons,
5004
+ mapping: {
5005
+ ...extended?.mapping,
5006
+ ...icons.mapping
5007
+ }
5008
+ };
5009
+ }
5010
+ function resolveSprite(basemapSprite, iconSprite) {
5011
+ if (!basemapSprite) {
5012
+ return iconSprite;
5013
+ }
5014
+ if (!iconSprite || iconSprite === basemapSprite) {
5015
+ return basemapSprite;
5016
+ }
5017
+ return [
5018
+ { id: "default", url: basemapSprite },
5019
+ { id: customIconSpriteId, url: iconSprite }
5020
+ ];
5021
+ }
5022
+ function isSpriteReference(value) {
5023
+ return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("/") || value.startsWith("data:");
5024
+ }
4471
5025
 
4472
5026
  // src/config.ts
4473
5027
  function defineTileflow(config) {
@@ -4480,9 +5034,11 @@ export {
4480
5034
  createStyleFromProject,
4481
5035
  defineTileflow,
4482
5036
  getDefaultColors2 as getDefaultColors,
5037
+ labels,
4483
5038
  osm,
4484
5039
  parseTileflowMap,
4485
5040
  parseTileflowProject,
5041
+ poi,
4486
5042
  roads,
4487
5043
  styleOverride,
4488
5044
  tileflowMapSchema,