@geolibre/core 3.6.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,4 @@
1
+ import { _ as getPlanetaryBasemapByStyleUrl, h as getEllipsoid, m as getActiveSemiMajorAxisMeters } from "./ellipsoids-DlTLddDu.mjs";
1
2
  import { Color, createExpression } from "@maplibre/maplibre-gl-style-spec";
2
3
  import { v4 } from "uuid";
3
4
  // src/types.ts
@@ -98,6 +99,23 @@ const LAYER_TYPES = [
98
99
  "image"
99
100
  ];
100
101
  /**
102
+ * The named dash patterns the Style panel offers for {@link LayerStyle.strokeDash},
103
+ * in MapLibre `line-dasharray` units (multiples of the line width). `solid` is
104
+ * the empty pattern. QGIS pen styles map onto the same names.
105
+ */
106
+ const STROKE_DASH_PRESETS = {
107
+ solid: [],
108
+ dashed: [4, 2],
109
+ dotted: [1, 2],
110
+ "dash-dot": [
111
+ 4,
112
+ 2,
113
+ 1,
114
+ 2
115
+ ],
116
+ "long-dash": [8, 3]
117
+ };
118
+ /**
101
119
  * Cartographic blend modes offered for {@link LayerStyle.blendMode}.
102
120
  *
103
121
  * The list is bounded by what WebGL *fixed-function* blending can express --
@@ -142,6 +160,9 @@ const DEFAULT_LAYER_STYLE = {
142
160
  strokeColor: "#1e40af",
143
161
  strokeWidth: 2,
144
162
  strokeWidthUnit: "pixels",
163
+ strokeOpacity: 1,
164
+ strokeDash: [],
165
+ strokeBlur: 0,
145
166
  fillOpacity: .6,
146
167
  circleRadius: 6,
147
168
  textColor: "#111827",
@@ -182,9 +203,13 @@ const DEFAULT_LAYER_STYLE = {
182
203
  extrusionHeightProperty: "height",
183
204
  extrusionHeightScale: 1,
184
205
  extrusionBase: 0,
206
+ extrusionBaseProperty: "",
207
+ extrusionBaseScale: 1,
208
+ extrusionFloorHeight: 0,
185
209
  extrusionAdvancedStyleEnabled: false,
186
210
  extrusionColorExpression: "",
187
211
  extrusionHeightExpression: "",
212
+ extrusionBaseExpression: "",
188
213
  elevation3dEnabled: false,
189
214
  elevation3dVerticalScale: 1,
190
215
  elevation3dOffset: 0,
@@ -454,499 +479,6 @@ function detectNonGeographicCoordinates(geojson, sampleLimit = 1e3) {
454
479
  maxAbsY
455
480
  } : null;
456
481
  }
457
- // src/ellipsoids.ts
458
- /**
459
- * Built-in ellipsoids. Earth is WGS 84; every other body uses IAU-adopted
460
- * figures. Only Earth and Mars are appreciably oblate; the rest are modelled as
461
- * spheres (`inverseFlattening: 0`), which is how the IAU/USGS simple-cylindrical
462
- * products treat them and is well within the accuracy MapLibre's unit-sphere
463
- * rendering can represent anyway.
464
- */
465
- const ELLIPSOIDS = [
466
- {
467
- id: "earth",
468
- name: "Earth (WGS 84)",
469
- semiMajorAxisMeters: 6378137,
470
- inverseFlattening: 298.257223563
471
- },
472
- {
473
- id: "moon",
474
- name: "Moon",
475
- semiMajorAxisMeters: 1737400,
476
- inverseFlattening: 0
477
- },
478
- {
479
- id: "mars",
480
- name: "Mars (IAU 2000)",
481
- semiMajorAxisMeters: 3396190,
482
- inverseFlattening: 169.894447
483
- },
484
- {
485
- id: "mercury",
486
- name: "Mercury",
487
- semiMajorAxisMeters: 2439400,
488
- inverseFlattening: 0
489
- },
490
- {
491
- id: "venus",
492
- name: "Venus",
493
- semiMajorAxisMeters: 6051800,
494
- inverseFlattening: 0
495
- },
496
- {
497
- id: "io",
498
- name: "Io",
499
- semiMajorAxisMeters: 1821600,
500
- inverseFlattening: 0
501
- },
502
- {
503
- id: "europa",
504
- name: "Europa",
505
- semiMajorAxisMeters: 1560800,
506
- inverseFlattening: 0
507
- },
508
- {
509
- id: "ganymede",
510
- name: "Ganymede",
511
- semiMajorAxisMeters: 2631200,
512
- inverseFlattening: 0
513
- },
514
- {
515
- id: "callisto",
516
- name: "Callisto",
517
- semiMajorAxisMeters: 2410300,
518
- inverseFlattening: 0
519
- },
520
- {
521
- id: "titan",
522
- name: "Titan",
523
- semiMajorAxisMeters: 2574730,
524
- inverseFlattening: 0
525
- },
526
- {
527
- id: "pluto",
528
- name: "Pluto",
529
- semiMajorAxisMeters: 1188300,
530
- inverseFlattening: 0
531
- },
532
- {
533
- id: "charon",
534
- name: "Charon",
535
- semiMajorAxisMeters: 606e3,
536
- inverseFlattening: 0
537
- }
538
- ];
539
- const DEFAULT_ELLIPSOID_ID = "earth";
540
- /** Look an ellipsoid up by id, falling back to Earth for unknown ids. */
541
- function getEllipsoid(id) {
542
- return ELLIPSOIDS.find((e) => e.id === id) ?? ELLIPSOIDS.find((e) => e.id === "earth");
543
- }
544
- /**
545
- * Mean radius `R = (2a + b) / 3` in metres, where `b` is the polar radius
546
- * derived from the inverse flattening. This is the radius used for spherical
547
- * (haversine) distance and area math.
548
- */
549
- function meanRadiusMeters(ellipsoid) {
550
- const a = ellipsoid.semiMajorAxisMeters;
551
- if (!ellipsoid.inverseFlattening) return a;
552
- const b = a * (1 - 1 / ellipsoid.inverseFlattening);
553
- return (2 * a + b) / 3;
554
- }
555
- /** Earth's mean radius in metres — the radius Turf.js and MapLibre bake in. */
556
- const EARTH_MEAN_RADIUS_METERS = meanRadiusMeters(ELLIPSOIDS.find((e) => e.id === "earth"));
557
- let activeEllipsoidId = DEFAULT_ELLIPSOID_ID;
558
- /**
559
- * Point the measurement helpers at a body. Unknown ids fall back to Earth so a
560
- * malformed project can never break measurements. Safe to call on every
561
- * preferences change; it is a cheap assignment.
562
- */
563
- function setActiveEllipsoidId(id) {
564
- activeEllipsoidId = getEllipsoid(id).id;
565
- }
566
- function getActiveEllipsoid() {
567
- return getEllipsoid(activeEllipsoidId);
568
- }
569
- /** Mean radius (metres) of the active body — for haversine distance/area. */
570
- function getActiveMeanRadiusMeters() {
571
- return meanRadiusMeters(getActiveEllipsoid());
572
- }
573
- /** Semi-major axis (metres) of the active body — for its Web-Mercator scale. */
574
- function getActiveSemiMajorAxisMeters() {
575
- return getActiveEllipsoid().semiMajorAxisMeters;
576
- }
577
- /**
578
- * The active body's mean radius as a fraction of Earth's — `R_body / R_earth`,
579
- * and exactly `1` on Earth.
580
- *
581
- * Everything GeoLibre measures ultimately comes from lon/lat *angles*, and the
582
- * only thing that turns an angle into a ground distance is the radius it is
583
- * multiplied by. Turf.js hardcodes Earth's, with no per-call override (that is
584
- * the "Turf is Earth-locked" half of GeoLibre#1128), so rather than patch or
585
- * replace Turf we post-scale what it returns and pre-scale what we hand it. The
586
- * result is exact for a sphere, which is how every non-Earth body GeoLibre
587
- * offers is modelled anyway, and within Mars' 0.6% flattening at worst.
588
- *
589
- * Suggested by @thareUSGS (USGS Astrogeology) on GeoLibre#1128 as the practical
590
- * workaround planetary web maps have long used on Earth-locked engines.
591
- */
592
- function getActiveBodyRadiusRatio() {
593
- return getActiveMeanRadiusMeters() / EARTH_MEAN_RADIUS_METERS;
594
- }
595
- /**
596
- * Convert a length Turf.js measured on Earth into the active body's true ground
597
- * length. A no-op on Earth.
598
- *
599
- * Use on anything Turf *returns* — a distance, a length, a perimeter.
600
- *
601
- * The correction is a dimensionless ratio, so it is unit-agnostic: pass the
602
- * length in whatever unit Turf gave it back (metres, kilometres, miles) and the
603
- * result is in that same unit. There is no hidden unit conversion here.
604
- */
605
- function earthLengthToBody(length) {
606
- return length * getActiveBodyRadiusRatio();
607
- }
608
- /**
609
- * Convert a true ground length on the active body into the Earth-equivalent
610
- * length to hand Turf.js. A no-op on Earth.
611
- *
612
- * Use on anything passed *into* Turf as a distance — a buffer width, a circle
613
- * or sector radius, a search threshold — so the resulting geometry spans that
614
- * distance on this body rather than on Earth.
615
- *
616
- * Unit-agnostic like {@link earthLengthToBody}: pass the length in whatever
617
- * unit you are about to hand Turf alongside it, and the result is in that unit.
618
- */
619
- function bodyLengthToEarth(length) {
620
- return length / getActiveBodyRadiusRatio();
621
- }
622
- /**
623
- * Convert an area Turf.js measured on Earth into the active body's true ground
624
- * area. A no-op on Earth.
625
- *
626
- * Areas scale with the *square* of the radius ratio, so this is not the same
627
- * correction as {@link earthLengthToBody}. Unit-agnostic in the same way, for
628
- * whatever squared unit the area is expressed in.
629
- */
630
- function earthAreaToBody(area) {
631
- const ratio = getActiveBodyRadiusRatio();
632
- return area * ratio * ratio;
633
- }
634
- const PLANETARY_BASEMAP_SENTINEL_PREFIX = "geolibre://basemap/";
635
- const OPM_ATTRIBUTION = "<a href=\"https://www.openplanetary.org/opm\">OpenPlanetaryMap</a>";
636
- /** Data-source credit joined with the OpenPlanetaryMap attribution. */
637
- const opmCredit = (source) => `${source} · ${OPM_ATTRIBUTION}`;
638
- const TILE_PROXY_BASE = "https://tiles.geolibre.app/opm";
639
- const WMS_PROXY_BASE = "https://tiles.geolibre.app/wms";
640
- const USGS_ASTRO_ATTRIBUTION = "<a href=\"https://astrogeology.usgs.gov/\">USGS Astrogeology</a>";
641
- /** Data-source credit joined with the USGS Astrogeology attribution. */
642
- const usgsCredit = (source) => `${source} · ${USGS_ASTRO_ATTRIBUTION}`;
643
- const PLANETARY_BASEMAPS = [
644
- {
645
- id: "mars-colour-mola-elevation",
646
- name: "Colour MOLA Elevation",
647
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-colour-mola-elevation`,
648
- tileUrl: `${TILE_PROXY_BASE}/mars-mola-color-noshade/{z}/{x}/{y}.png`,
649
- scheme: "tms",
650
- maxZoom: 6,
651
- attribution: opmCredit("NASA / MOLA"),
652
- ellipsoidId: "mars"
653
- },
654
- {
655
- id: "mars-viking-mdim21",
656
- name: "Viking MDIM2.1",
657
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-viking-mdim21`,
658
- tileUrl: `${TILE_PROXY_BASE}/mars-viking-mdim21/{z}/{x}/{y}.png`,
659
- scheme: "tms",
660
- maxZoom: 7,
661
- attribution: opmCredit("NASA / Viking / USGS"),
662
- ellipsoidId: "mars"
663
- },
664
- {
665
- id: "mars-hillshade",
666
- name: "Hillshade",
667
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-hillshade`,
668
- tileUrl: `${TILE_PROXY_BASE}/mars-hillshade/{z}/{x}/{y}.png`,
669
- scheme: "tms",
670
- maxZoom: 6,
671
- attribution: opmCredit("NASA / MOLA"),
672
- ellipsoidId: "mars"
673
- },
674
- {
675
- id: "mars-basemap-v0-2",
676
- name: "OPM Basemap v0.2",
677
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-basemap-v0-2`,
678
- tileUrl: "https://cartocdn-gusc.global.ssl.fastly.net/opmbuilder/api/v1/map/named/opm-mars-basemap-v0-2/all/{z}/{x}/{y}.png",
679
- maxZoom: 6,
680
- attribution: OPM_ATTRIBUTION,
681
- ellipsoidId: "mars"
682
- },
683
- {
684
- id: "mars-shaded-colour-mola-elevation",
685
- name: "Shaded Colour MOLA Elevation",
686
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-shaded-colour-mola-elevation`,
687
- tileUrl: `${TILE_PROXY_BASE}/mars-mola-color/{z}/{x}/{y}.png`,
688
- scheme: "tms",
689
- maxZoom: 6,
690
- attribution: opmCredit("NASA / MOLA"),
691
- ellipsoidId: "mars"
692
- },
693
- {
694
- id: "mars-shaded-grayscale-mola-elevation",
695
- name: "Shaded Grayscale MOLA Elevation",
696
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-shaded-grayscale-mola-elevation`,
697
- tileUrl: `${TILE_PROXY_BASE}/mars-mola-gray/{z}/{x}/{y}.png`,
698
- scheme: "tms",
699
- maxZoom: 9,
700
- attribution: opmCredit("NASA / MOLA"),
701
- ellipsoidId: "mars"
702
- },
703
- {
704
- id: "moon-hillshaded-albedo",
705
- name: "Hillshaded Albedo",
706
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}moon-hillshaded-albedo`,
707
- tileUrl: `${TILE_PROXY_BASE}/moon-hillshaded-albedo/{z}/{x}/{y}.png`,
708
- scheme: "tms",
709
- maxZoom: 6,
710
- attribution: opmCredit("NASA / LOLA / USGS"),
711
- ellipsoidId: "moon"
712
- },
713
- {
714
- id: "moon-basemap-v0-1",
715
- name: "OPM Basemap v0.1",
716
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}moon-basemap-v0-1`,
717
- tileUrl: "https://cartocdn-gusc.global.ssl.fastly.net/opmbuilder/api/v1/map/named/opm-moon-basemap-v0-1/all/{z}/{x}/{y}.png",
718
- maxZoom: 6,
719
- attribution: OPM_ATTRIBUTION,
720
- ellipsoidId: "moon"
721
- },
722
- {
723
- id: "mercury-messenger-color",
724
- name: "MESSENGER Colour Mosaic",
725
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mercury-messenger-color`,
726
- tileUrl: `${WMS_PROXY_BASE}/mercury-messenger-color/{z}/{x}/{y}.png`,
727
- maxZoom: 7,
728
- attribution: usgsCredit("NASA / JHU APL / CIW"),
729
- ellipsoidId: "mercury"
730
- },
731
- {
732
- id: "mercury-messenger",
733
- name: "MESSENGER Basemap",
734
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mercury-messenger`,
735
- tileUrl: `${WMS_PROXY_BASE}/mercury-messenger/{z}/{x}/{y}.png`,
736
- maxZoom: 7,
737
- attribution: usgsCredit("NASA / JHU APL"),
738
- ellipsoidId: "mercury"
739
- },
740
- {
741
- id: "venus-magellan",
742
- name: "Magellan FMAP",
743
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}venus-magellan`,
744
- tileUrl: `${WMS_PROXY_BASE}/venus-magellan/{z}/{x}/{y}.png`,
745
- maxZoom: 6,
746
- attribution: usgsCredit("NASA / JPL"),
747
- ellipsoidId: "venus"
748
- },
749
- {
750
- id: "venus-magellan-color",
751
- name: "Magellan C3-MDIR Colour",
752
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}venus-magellan-color`,
753
- tileUrl: `${WMS_PROXY_BASE}/venus-magellan-color/{z}/{x}/{y}.png`,
754
- maxZoom: 6,
755
- attribution: usgsCredit("NASA / JPL"),
756
- ellipsoidId: "venus"
757
- },
758
- {
759
- id: "io-galileo-color",
760
- name: "Galileo SSI Colour",
761
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}io-galileo-color`,
762
- tileUrl: `${WMS_PROXY_BASE}/io-galileo-color/{z}/{x}/{y}.png`,
763
- maxZoom: 6,
764
- attribution: usgsCredit("NASA / JPL"),
765
- ellipsoidId: "io"
766
- },
767
- {
768
- id: "europa-galileo-voyager",
769
- name: "Galileo / Voyager",
770
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}europa-galileo-voyager`,
771
- tileUrl: `${WMS_PROXY_BASE}/europa-galileo-voyager/{z}/{x}/{y}.png`,
772
- maxZoom: 6,
773
- attribution: usgsCredit("NASA / JPL"),
774
- ellipsoidId: "europa"
775
- },
776
- {
777
- id: "ganymede-galileo-voyager",
778
- name: "Galileo / Voyager",
779
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}ganymede-galileo-voyager`,
780
- tileUrl: `${WMS_PROXY_BASE}/ganymede-galileo-voyager/{z}/{x}/{y}.png`,
781
- maxZoom: 6,
782
- attribution: usgsCredit("NASA / JPL"),
783
- ellipsoidId: "ganymede"
784
- },
785
- {
786
- id: "callisto-galileo-voyager",
787
- name: "Galileo / Voyager",
788
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}callisto-galileo-voyager`,
789
- tileUrl: `${WMS_PROXY_BASE}/callisto-galileo-voyager/{z}/{x}/{y}.png`,
790
- maxZoom: 6,
791
- attribution: usgsCredit("NASA / JPL"),
792
- ellipsoidId: "callisto"
793
- },
794
- {
795
- id: "titan-cassini",
796
- name: "Cassini ISS Mosaic",
797
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}titan-cassini`,
798
- tileUrl: `${WMS_PROXY_BASE}/titan-cassini/{z}/{x}/{y}.png`,
799
- maxZoom: 6,
800
- attribution: usgsCredit("NASA / JPL / SSI"),
801
- ellipsoidId: "titan"
802
- },
803
- {
804
- id: "titan-hisar",
805
- name: "Cassini HiSAR Mosaic",
806
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}titan-hisar`,
807
- tileUrl: `${WMS_PROXY_BASE}/titan-hisar/{z}/{x}/{y}.png`,
808
- maxZoom: 6,
809
- attribution: usgsCredit("NASA / JPL / USGS"),
810
- ellipsoidId: "titan"
811
- },
812
- {
813
- id: "pluto-mosaic",
814
- name: "New Horizons Mosaic",
815
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}pluto-mosaic`,
816
- tileUrl: `${WMS_PROXY_BASE}/pluto-mosaic/{z}/{x}/{y}.png`,
817
- maxZoom: 7,
818
- attribution: usgsCredit("NASA / JHU APL / SwRI"),
819
- ellipsoidId: "pluto"
820
- },
821
- {
822
- id: "pluto-color",
823
- name: "New Horizons Colour Shaded Relief",
824
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}pluto-color`,
825
- tileUrl: `${WMS_PROXY_BASE}/pluto-color/{z}/{x}/{y}.png`,
826
- maxZoom: 7,
827
- attribution: usgsCredit("NASA / JHU APL / SwRI"),
828
- ellipsoidId: "pluto"
829
- },
830
- {
831
- id: "charon-mosaic",
832
- name: "New Horizons Mosaic",
833
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}charon-mosaic`,
834
- tileUrl: `${WMS_PROXY_BASE}/charon-mosaic/{z}/{x}/{y}.png`,
835
- maxZoom: 6,
836
- attribution: usgsCredit("NASA / JHU APL / SwRI"),
837
- ellipsoidId: "charon"
838
- },
839
- {
840
- id: "earth-usgs-imagery",
841
- name: "USGS Imagery",
842
- styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}earth-usgs-imagery`,
843
- tileUrl: "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}",
844
- maxZoom: 16,
845
- attribution: "<a href=\"https://www.usgs.gov/programs/national-geospatial-program/national-map\">USGS The National Map</a>",
846
- ellipsoidId: "earth"
847
- }
848
- ];
849
- const DEDICATED_SECTION_BODIES = ["moon", "mars"];
850
- /**
851
- * {@link PLANETARY_BASEMAPS} grouped for the pickers: one section each for the
852
- * Moon and Mars, then a single "Other celestial bodies" section for the rest,
853
- * in {@link PLANETARY_BASEMAPS} order (grouped by body). Empty sections are
854
- * dropped.
855
- */
856
- const PLANETARY_BASEMAP_GROUPS = [
857
- {
858
- id: "moon",
859
- ellipsoidId: "moon"
860
- },
861
- {
862
- id: "mars",
863
- ellipsoidId: "mars"
864
- },
865
- {
866
- id: "other",
867
- ellipsoidId: null
868
- }
869
- ].map(({ id, ellipsoidId }) => {
870
- const basemaps = PLANETARY_BASEMAPS.filter((b) => ellipsoidId ? b.ellipsoidId === ellipsoidId : b.ellipsoidId !== "earth" && !DEDICATED_SECTION_BODIES.includes(b.ellipsoidId));
871
- return {
872
- id,
873
- basemaps: id === "other" ? [...basemaps].sort((a, b) => getEllipsoid(a.ellipsoidId).name.localeCompare(getEllipsoid(b.ellipsoidId).name)) : basemaps
874
- };
875
- }).filter((group) => group.basemaps.length > 0);
876
- const LEGACY_PLANETARY_BASEMAP_IDS = {
877
- "mars-opm": "mars-basemap-v0-2",
878
- "moon-opm": "moon-basemap-v0-1",
879
- "mars-viking": "mars-viking-mdim21",
880
- "moon-lroc": "moon-basemap-v0-1"
881
- };
882
- /** Resolve a `geolibre://basemap/<id>` sentinel to its planetary basemap. */
883
- function getPlanetaryBasemapByStyleUrl(styleUrl) {
884
- if (!styleUrl?.startsWith("geolibre://basemap/")) return void 0;
885
- const id = styleUrl.slice(19);
886
- const resolvedId = Object.prototype.hasOwnProperty.call(LEGACY_PLANETARY_BASEMAP_IDS, id) ? LEGACY_PLANETARY_BASEMAP_IDS[id] : id;
887
- return PLANETARY_BASEMAPS.find((b) => b.id === resolvedId);
888
- }
889
- /** Look a planetary basemap up by its stable id. */
890
- function getPlanetaryBasemapById(id) {
891
- return PLANETARY_BASEMAPS.find((b) => b.id === id);
892
- }
893
- /**
894
- * The celestial bodies offered by the Layers-panel planet switcher, in menu
895
- * order (like Google Earth's planet dropdown). Each body is paired with the
896
- * basemap the switcher applies for it — USGS satellite imagery for Earth, the
897
- * OpenPlanetaryMap Hillshaded Albedo mosaic for the Moon, and the OpenPlanetaryMap
898
- * Viking mosaic for Mars.
899
- */
900
- const PLANET_SWITCHER_OPTIONS = [
901
- {
902
- ellipsoidId: "earth",
903
- basemapId: "earth-usgs-imagery"
904
- },
905
- {
906
- ellipsoidId: "moon",
907
- basemapId: "moon-hillshaded-albedo"
908
- },
909
- {
910
- ellipsoidId: "mars",
911
- basemapId: "mars-viking-mdim21"
912
- },
913
- {
914
- ellipsoidId: "callisto",
915
- basemapId: "callisto-galileo-voyager"
916
- },
917
- {
918
- ellipsoidId: "charon",
919
- basemapId: "charon-mosaic"
920
- },
921
- {
922
- ellipsoidId: "europa",
923
- basemapId: "europa-galileo-voyager"
924
- },
925
- {
926
- ellipsoidId: "ganymede",
927
- basemapId: "ganymede-galileo-voyager"
928
- },
929
- {
930
- ellipsoidId: "io",
931
- basemapId: "io-galileo-color"
932
- },
933
- {
934
- ellipsoidId: "mercury",
935
- basemapId: "mercury-messenger-color"
936
- },
937
- {
938
- ellipsoidId: "pluto",
939
- basemapId: "pluto-mosaic"
940
- },
941
- {
942
- ellipsoidId: "titan",
943
- basemapId: "titan-cassini"
944
- },
945
- {
946
- ellipsoidId: "venus",
947
- basemapId: "venus-magellan-color"
948
- }
949
- ];
950
482
  // src/regional-basemaps.ts
951
483
  const REGIONAL_BASEMAP_SENTINEL_PREFIX = "geolibre://regional-basemap/";
952
484
  const AMAP_ATTRIBUTION = "&copy; <a href=\"https://www.amap.com\">高德地图 Amap</a>";
@@ -969,6 +501,7 @@ const CHINA_BASEMAPS = [
969
501
  id: "amap-street",
970
502
  region: "china",
971
503
  name: "高德地图",
504
+ provider: "Amap",
972
505
  styleUrl: sentinel("amap-street"),
973
506
  tileUrl: AMAP_STREET_URL,
974
507
  maxZoom: 19,
@@ -979,6 +512,7 @@ const CHINA_BASEMAPS = [
979
512
  id: "amap-satellite",
980
513
  region: "china",
981
514
  name: "高德卫星",
515
+ provider: "Amap",
982
516
  styleUrl: sentinel("amap-satellite"),
983
517
  tileUrl: AMAP_SATELLITE_URL,
984
518
  maxZoom: 18,
@@ -989,6 +523,7 @@ const CHINA_BASEMAPS = [
989
523
  id: "amap-hybrid",
990
524
  region: "china",
991
525
  name: "高德混合",
526
+ provider: "Amap",
992
527
  styleUrl: sentinel("amap-hybrid"),
993
528
  tileUrl: AMAP_SATELLITE_URL,
994
529
  overlayTileUrl: AMAP_LABELS_URL,
@@ -1000,6 +535,7 @@ const CHINA_BASEMAPS = [
1000
535
  id: "tencent-street",
1001
536
  region: "china",
1002
537
  name: "腾讯地图",
538
+ provider: "Tencent Maps",
1003
539
  styleUrl: sentinel("tencent-street"),
1004
540
  tileUrl: tencentUrl(1),
1005
541
  scheme: "tms",
@@ -1011,6 +547,7 @@ const CHINA_BASEMAPS = [
1011
547
  id: "tencent-dark",
1012
548
  region: "china",
1013
549
  name: "腾讯深色",
550
+ provider: "Tencent Maps",
1014
551
  styleUrl: sentinel("tencent-dark"),
1015
552
  tileUrl: tencentUrl(4),
1016
553
  scheme: "tms",
@@ -1045,6 +582,33 @@ function getRegionalBasemapById(id) {
1045
582
  function isRegionalBasemapSentinel(styleUrl) {
1046
583
  return Boolean(styleUrl?.startsWith(REGIONAL_BASEMAP_SENTINEL_PREFIX));
1047
584
  }
585
+ /**
586
+ * Decides which basemap a project load actually applies.
587
+ *
588
+ * Only a resolvable regional sentinel is gated. Every other style URL (and an
589
+ * unknown regional id, which the map controller already replaces with the
590
+ * default basemap) passes through untouched.
591
+ *
592
+ * @param styleUrl - The basemap style URL the project requests.
593
+ * @param policy - The store's current {@link RegionalBasemapPolicy}.
594
+ * @param fallbackStyleUrl - The basemap to apply instead (the default basemap).
595
+ * @param trusted - True when the user already chose this basemap in this
596
+ * session (a template, an undo step, a duplicate), so `"prompt"` does not
597
+ * apply. `"block"` still does.
598
+ * @returns The style URL to apply and, when it was replaced, why.
599
+ */
600
+ function gateRegionalBasemap(styleUrl, policy, fallbackStyleUrl, trusted = false) {
601
+ if (!getRegionalBasemapByStyleUrl(styleUrl)) return { styleUrl };
602
+ if (policy === "block") return {
603
+ styleUrl: fallbackStyleUrl,
604
+ held: "blocked"
605
+ };
606
+ if (policy === "prompt" && !trusted) return {
607
+ styleUrl: fallbackStyleUrl,
608
+ held: "prompt"
609
+ };
610
+ return { styleUrl };
611
+ }
1048
612
  // src/cesium-imagery.ts
1049
613
  /**
1050
614
  * Maps the project's basemap onto imagery the Cesium globe can render.
@@ -1689,6 +1253,24 @@ function normalizeMapCrs(value) {
1689
1253
  return MAP_CRS_PRESETS.find((preset) => preset.code === epsg)?.id ?? epsg;
1690
1254
  }
1691
1255
  /**
1256
+ * The map preferences that draw a map in a CRS, as a Settings choice writes them.
1257
+ *
1258
+ * Args:
1259
+ * mapCrs: A normalized map CRS, or `null` for the default map.
1260
+ *
1261
+ * Returns:
1262
+ * The preferences to merge. A CRS brings the body it maps, so measurements
1263
+ * follow it (the EPSG tables describe Earth); the default map leaves the body
1264
+ * alone.
1265
+ */
1266
+ function mapCrsPreferencePatch(mapCrs) {
1267
+ if (mapCrs === null) return { mapCrs: void 0 };
1268
+ return {
1269
+ mapCrs,
1270
+ ellipsoidId: getMapCrsPreset(mapCrs)?.ellipsoidId ?? "earth"
1271
+ };
1272
+ }
1273
+ /**
1692
1274
  * Point a Web Mercator WMS or ArcGIS export template at a map CRS.
1693
1275
  *
1694
1276
  * GeoLibre's WMS layers request `BBOX={bbox-epsg-3857}` in `EPSG:3857`. On a
@@ -1952,6 +1534,534 @@ function mapGeometryPositions(geometry, mapPosition) {
1952
1534
  default: return geometry;
1953
1535
  }
1954
1536
  }
1537
+ // src/color-ramp.ts
1538
+ /** The built-in color ramps offered by the style panels. */
1539
+ const VECTOR_COLOR_RAMPS = [
1540
+ {
1541
+ value: "viridis",
1542
+ label: "Viridis",
1543
+ colors: [
1544
+ "#440154",
1545
+ "#31688e",
1546
+ "#35b779",
1547
+ "#fde725"
1548
+ ]
1549
+ },
1550
+ {
1551
+ value: "plasma",
1552
+ label: "Plasma",
1553
+ colors: [
1554
+ "#0d0887",
1555
+ "#9c179e",
1556
+ "#ed7953",
1557
+ "#f0f921"
1558
+ ]
1559
+ },
1560
+ {
1561
+ value: "inferno",
1562
+ label: "Inferno",
1563
+ colors: [
1564
+ "#000004",
1565
+ "#781c6d",
1566
+ "#ed6925",
1567
+ "#fcffa4"
1568
+ ]
1569
+ },
1570
+ {
1571
+ value: "magma",
1572
+ label: "Magma",
1573
+ colors: [
1574
+ "#000004",
1575
+ "#721f81",
1576
+ "#f1605d",
1577
+ "#fcfdbf"
1578
+ ]
1579
+ },
1580
+ {
1581
+ value: "cividis",
1582
+ label: "Cividis",
1583
+ colors: [
1584
+ "#00204d",
1585
+ "#575d6d",
1586
+ "#a59c74",
1587
+ "#ffea46"
1588
+ ]
1589
+ },
1590
+ {
1591
+ value: "turbo",
1592
+ label: "Turbo",
1593
+ colors: [
1594
+ "#30123b",
1595
+ "#4777ef",
1596
+ "#1ccfd0",
1597
+ "#b9e642",
1598
+ "#fb8022",
1599
+ "#7a0403"
1600
+ ]
1601
+ },
1602
+ {
1603
+ value: "spectral",
1604
+ label: "Spectral",
1605
+ colors: [
1606
+ "#9e0142",
1607
+ "#f46d43",
1608
+ "#ffffbf",
1609
+ "#66c2a5",
1610
+ "#5e4fa2"
1611
+ ]
1612
+ },
1613
+ {
1614
+ value: "blues",
1615
+ label: "Blues",
1616
+ colors: [
1617
+ "#eff6ff",
1618
+ "#93c5fd",
1619
+ "#2563eb",
1620
+ "#1e3a8a"
1621
+ ]
1622
+ },
1623
+ {
1624
+ value: "greens",
1625
+ label: "Greens",
1626
+ colors: [
1627
+ "#f0fdf4",
1628
+ "#86efac",
1629
+ "#16a34a",
1630
+ "#14532d"
1631
+ ]
1632
+ },
1633
+ {
1634
+ value: "oranges",
1635
+ label: "Oranges",
1636
+ colors: [
1637
+ "#fff7ed",
1638
+ "#fdba74",
1639
+ "#f97316",
1640
+ "#7c2d12"
1641
+ ]
1642
+ },
1643
+ {
1644
+ value: "reds",
1645
+ label: "Reds",
1646
+ colors: [
1647
+ "#fff5f0",
1648
+ "#fcae91",
1649
+ "#fb6a4a",
1650
+ "#cb181d",
1651
+ "#67000d"
1652
+ ]
1653
+ },
1654
+ {
1655
+ value: "purples",
1656
+ label: "Purples",
1657
+ colors: [
1658
+ "#fcfbfd",
1659
+ "#bcbddc",
1660
+ "#807dba",
1661
+ "#54278f",
1662
+ "#3f007d"
1663
+ ]
1664
+ },
1665
+ {
1666
+ value: "terrain",
1667
+ label: "Terrain",
1668
+ colors: [
1669
+ "#333399",
1670
+ "#21bcb3",
1671
+ "#79d05a",
1672
+ "#e8e85a",
1673
+ "#a87b54",
1674
+ "#ffffff"
1675
+ ]
1676
+ },
1677
+ {
1678
+ value: "rdylgn",
1679
+ label: "Red-Yellow-Green",
1680
+ colors: [
1681
+ "#a50026",
1682
+ "#f46d43",
1683
+ "#ffffbf",
1684
+ "#66bd63",
1685
+ "#006837"
1686
+ ]
1687
+ },
1688
+ {
1689
+ value: "rdylbu",
1690
+ label: "Red-Yellow-Blue",
1691
+ colors: [
1692
+ "#a50026",
1693
+ "#f46d43",
1694
+ "#ffffbf",
1695
+ "#74add1",
1696
+ "#313695"
1697
+ ]
1698
+ },
1699
+ {
1700
+ value: "rdbu",
1701
+ label: "Red-Blue",
1702
+ colors: [
1703
+ "#b2182b",
1704
+ "#ef8a62",
1705
+ "#f7f7f7",
1706
+ "#67a9cf",
1707
+ "#2166ac"
1708
+ ]
1709
+ },
1710
+ {
1711
+ value: "coolwarm",
1712
+ label: "Cool-Warm",
1713
+ colors: [
1714
+ "#3b4cc0",
1715
+ "#7b9ff9",
1716
+ "#dddcdc",
1717
+ "#f49a7b",
1718
+ "#b40426"
1719
+ ]
1720
+ },
1721
+ {
1722
+ value: "jet",
1723
+ label: "Jet",
1724
+ colors: [
1725
+ "#000080",
1726
+ "#0000ff",
1727
+ "#00ffff",
1728
+ "#ffff00",
1729
+ "#ff0000",
1730
+ "#800000"
1731
+ ]
1732
+ },
1733
+ {
1734
+ value: "greys",
1735
+ label: "Greys",
1736
+ colors: [
1737
+ "#ffffff",
1738
+ "#bdbdbd",
1739
+ "#636363",
1740
+ "#000000"
1741
+ ]
1742
+ },
1743
+ {
1744
+ value: "gray",
1745
+ label: "Grayscale",
1746
+ colors: ["#000000", "#ffffff"]
1747
+ }
1748
+ ];
1749
+ /**
1750
+ * Resolves a ramp definition by name, falling back to the first ramp when the
1751
+ * name is unknown.
1752
+ *
1753
+ * @param value - The ramp `value` (e.g. "viridis").
1754
+ * @returns The matching ramp, or the first ramp.
1755
+ */
1756
+ function getVectorColorRamp(value) {
1757
+ return VECTOR_COLOR_RAMPS.find((colorRamp) => colorRamp.value === value) ?? VECTOR_COLOR_RAMPS[0];
1758
+ }
1759
+ /**
1760
+ * Samples a ramp into `count` evenly spaced hex colors by linearly
1761
+ * interpolating between the ramp's anchor colors.
1762
+ *
1763
+ * @param colorRamp - The ramp `value`.
1764
+ * @param count - Number of colors to produce.
1765
+ * @returns An array of `count` hex colors (a single end color when count <= 1).
1766
+ */
1767
+ function interpolateRampColors(colorRamp, count) {
1768
+ return interpolateColors(getVectorColorRamp(colorRamp).colors, count);
1769
+ }
1770
+ /**
1771
+ * Samples an explicit list of anchor colors into `count` evenly spaced hex
1772
+ * colors by linear interpolation. Backs both the named ramps and user-defined
1773
+ * custom ramps, so a list of hex codes is sampled identically to a built-in.
1774
+ *
1775
+ * @param colors - The anchor colors (at least one).
1776
+ * @param count - Number of colors to produce.
1777
+ * @returns An array of `count` hex colors (a single end color when count <= 1).
1778
+ */
1779
+ function interpolateColors(colors, count) {
1780
+ const anchors = colors.length > 0 ? colors : ["#000000"];
1781
+ if (count <= 0) return [];
1782
+ if (count === 1) return [anchors[anchors.length - 1]];
1783
+ if (anchors.length === 1) return Array.from({ length: count }, () => anchors[0]);
1784
+ return Array.from({ length: count }, (_, index) => {
1785
+ const scaled = index / (count - 1) * (anchors.length - 1);
1786
+ const lowerIndex = Math.floor(scaled);
1787
+ const upperIndex = Math.min(anchors.length - 1, Math.ceil(scaled));
1788
+ const ratio = scaled - lowerIndex;
1789
+ return interpolateHexColor(anchors[lowerIndex], anchors[upperIndex], ratio);
1790
+ });
1791
+ }
1792
+ /**
1793
+ * Normalizes a single user-entered color token into a canonical `#rrggbb`
1794
+ * lowercase hex string, or null when it is not a valid 3- or 6-digit hex
1795
+ * color. Accepts values with or without a leading `#` and expands shorthand
1796
+ * (`#abc` → `#aabbcc`).
1797
+ *
1798
+ * @param token - A raw color token (e.g. "FF0000", "#f00", " #AaBbCc ").
1799
+ * @returns The canonical `#rrggbb` color, or null when invalid.
1800
+ */
1801
+ function normalizeHexColor(token) {
1802
+ let value = token.trim().toLowerCase();
1803
+ if (value === "") return null;
1804
+ if (!value.startsWith("#")) value = `#${value}`;
1805
+ if (/^#[0-9a-f]{3}$/.test(value)) value = `#${value.slice(1).split("").map((channel) => channel + channel).join("")}`;
1806
+ return /^#[0-9a-f]{6}$/.test(value) ? value : null;
1807
+ }
1808
+ /**
1809
+ * Parses a free-text list of hex color codes into canonical `#rrggbb` colors,
1810
+ * for the custom color-ramp inputs. Tokens may be separated by commas,
1811
+ * semicolons, or any whitespace (including newlines); invalid tokens are
1812
+ * dropped so a partial paste still yields the colors it can.
1813
+ *
1814
+ * @param input - The raw text the user entered.
1815
+ * @returns The valid, normalized colors in input order.
1816
+ */
1817
+ function parseHexColorList(input) {
1818
+ return input.split(/[\s,;]+/).map(normalizeHexColor).filter((color) => color !== null);
1819
+ }
1820
+ /**
1821
+ * Linearly interpolates between two hex colors.
1822
+ *
1823
+ * @param from - Start hex color (e.g. "#440154").
1824
+ * @param to - End hex color.
1825
+ * @param ratio - Blend factor in [0, 1].
1826
+ * @returns The interpolated hex color.
1827
+ */
1828
+ function interpolateHexColor(from, to, ratio) {
1829
+ const start = parseHexColor(from);
1830
+ const end = parseHexColor(to);
1831
+ return rgbToHex({
1832
+ r: Math.round(start.r + (end.r - start.r) * ratio),
1833
+ g: Math.round(start.g + (end.g - start.g) * ratio),
1834
+ b: Math.round(start.b + (end.b - start.b) * ratio)
1835
+ });
1836
+ }
1837
+ /**
1838
+ * Parses a `#rrggbb` hex color into its RGB channels.
1839
+ *
1840
+ * @param value - A `#rrggbb` hex color.
1841
+ * @returns The red, green, and blue channels (0-255).
1842
+ */
1843
+ function parseHexColor(value) {
1844
+ const numeric = Number.parseInt(value.slice(1), 16);
1845
+ return {
1846
+ r: numeric >> 16 & 255,
1847
+ g: numeric >> 8 & 255,
1848
+ b: numeric & 255
1849
+ };
1850
+ }
1851
+ /**
1852
+ * Formats RGB channels as a `#rrggbb` hex color.
1853
+ *
1854
+ * @param color - The red, green, and blue channels (0-255).
1855
+ * @returns A `#rrggbb` hex color.
1856
+ */
1857
+ function rgbToHex(color) {
1858
+ return `#${[
1859
+ color.r,
1860
+ color.g,
1861
+ color.b
1862
+ ].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
1863
+ }
1864
+ /**
1865
+ * Builds `count` evenly spaced break values across [min, max].
1866
+ *
1867
+ * @param min - The minimum value.
1868
+ * @param max - The maximum value.
1869
+ * @param count - Number of breaks to produce.
1870
+ * @returns The break values (min..max inclusive when count > 1).
1871
+ */
1872
+ function createEqualIntervalBreaks(min, max, count) {
1873
+ return Array.from({ length: count }, (_, index) => {
1874
+ const ratio = count === 1 ? 0 : index / (count - 1);
1875
+ return min + (max - min) * ratio;
1876
+ });
1877
+ }
1878
+ /**
1879
+ * Builds `count` quantile break values from a sample of numeric values.
1880
+ *
1881
+ * These are ramp *edges* spanning the whole sample (the first break is the
1882
+ * minimum, the last the maximum), matching {@link createEqualIntervalBreaks}.
1883
+ * Graduated vector classes want class *lower bounds* instead; use
1884
+ * {@link createGraduatedClassBreaks}.
1885
+ *
1886
+ * @param values - The sample values.
1887
+ * @param count - Number of breaks to produce.
1888
+ * @returns The quantile break values.
1889
+ */
1890
+ function createQuantileBreaks(values, count) {
1891
+ if (count <= 0) return [];
1892
+ const sorted = [...values].sort((a, b) => a - b);
1893
+ if (sorted.length === 0) return [];
1894
+ return Array.from({ length: count }, (_, index) => {
1895
+ const position = count === 1 ? 0 : index / (count - 1) * (sorted.length - 1);
1896
+ return quantileAtPosition(sorted, position);
1897
+ });
1898
+ }
1899
+ /** Linearly interpolated order statistic at a fractional index into `sorted`. */
1900
+ function quantileAtPosition(sorted, position) {
1901
+ const lowerIndex = Math.floor(position);
1902
+ const upperIndex = Math.min(sorted.length - 1, Math.ceil(position));
1903
+ const ratio = position - lowerIndex;
1904
+ return sorted[lowerIndex] + (sorted[upperIndex] - sorted[lowerIndex]) * ratio;
1905
+ }
1906
+ /**
1907
+ * Builds the class *lower bounds* for a graduated vector renderer.
1908
+ *
1909
+ * A graduated renderer's stops are lower bounds, not ramp edges: with N stops
1910
+ * sorted ascending, class `i` covers `[stops[i], stops[i + 1])` and the last
1911
+ * class is open-ended above. The legend (`≥ value`) and the QML/SLD exporters
1912
+ * already read them that way, so the breaks must follow the same convention:
1913
+ * `count` classes yield `count` breaks whose first entry is the sample minimum
1914
+ * and whose last entry opens the top class. Producing edges instead (ending at
1915
+ * the maximum) leaves the top class holding only the single largest feature and
1916
+ * puts the interior breaks at the wrong percentiles.
1917
+ *
1918
+ * The result is strictly ascending: duplicate breaks (a skewed sample can push
1919
+ * several quantiles onto the same value) are collapsed, so the caller may get
1920
+ * fewer than `count` breaks and must size its colors off `breaks.length`.
1921
+ * MapLibre rejects a `step` expression whose inputs are not strictly ascending,
1922
+ * so this de-duplication is load-bearing, not cosmetic.
1923
+ *
1924
+ * Standard deviation and geometric interval derive their breaks from the data,
1925
+ * so `count` is a ceiling there: a standard-deviation break that falls outside
1926
+ * the sample range is dropped rather than producing an empty class. See
1927
+ * {@link standardDeviationClassBreaks} and {@link geometricIntervalClassBreaks}
1928
+ * for how each treats its edge cases.
1929
+ *
1930
+ * @param values - The finite numeric values of the classified property.
1931
+ * @param count - Number of classes requested.
1932
+ * @param scheme - The classification scheme.
1933
+ * @returns Up to `count` strictly ascending class lower bounds.
1934
+ */
1935
+ function createGraduatedClassBreaks(values, count, scheme) {
1936
+ if (count <= 0 || values.length === 0) return [];
1937
+ if (scheme === "natural-breaks") return ascendingUnique(naturalClassBreaks(values, count));
1938
+ const sorted = [...values].sort((a, b) => a - b);
1939
+ if (scheme === "quantile") return ascendingUnique(Array.from({ length: count }, (_, index) => quantileAtPosition(sorted, index / count * (sorted.length - 1))));
1940
+ if (scheme === "standard-deviation") return ascendingUnique(standardDeviationClassBreaks(sorted, count));
1941
+ if (scheme === "geometric-interval") {
1942
+ const breaks = geometricIntervalClassBreaks(sorted, count);
1943
+ if (breaks) return ascendingUnique(breaks);
1944
+ }
1945
+ return ascendingUnique(equalIntervalClassBreaks(sorted, count));
1946
+ }
1947
+ /** Equal-interval lower bounds over the range of an ascending sample. */
1948
+ function equalIntervalClassBreaks(sorted, count) {
1949
+ const min = sorted[0];
1950
+ const max = sorted[sorted.length - 1];
1951
+ return Array.from({ length: count }, (_, index) => min + (max - min) * index / count);
1952
+ }
1953
+ /**
1954
+ * Standard-deviation lower bounds: classes one standard deviation wide,
1955
+ * centered on the mean. An even `count` puts a break on the mean (classes
1956
+ * `[mean - σ, mean)` and `[mean, mean + σ)` meet there); an odd `count` makes
1957
+ * the middle class straddle it (`[mean - σ/2, mean + σ/2)`). The first break is
1958
+ * the sample minimum, and interior breaks outside `(min, max]` are dropped, so
1959
+ * the open-ended bottom and top classes absorb the tails and no class is empty.
1960
+ * A constant sample (σ = 0) yields the minimum alone.
1961
+ *
1962
+ * Uses the population standard deviation, as QGIS and ArcGIS do.
1963
+ */
1964
+ function standardDeviationClassBreaks(sorted, count) {
1965
+ const min = sorted[0];
1966
+ const max = sorted[sorted.length - 1];
1967
+ const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
1968
+ const variance = sorted.reduce((sum, value) => sum + (value - mean) * (value - mean), 0) / sorted.length;
1969
+ const deviation = Math.sqrt(variance);
1970
+ if (!(deviation > 0)) return [min];
1971
+ const interior = [];
1972
+ for (let index = 1; index < count; index += 1) {
1973
+ const value = mean + (index - count / 2) * deviation;
1974
+ if (value > min && value <= max) interior.push(value);
1975
+ }
1976
+ return [min, ...interior];
1977
+ }
1978
+ /**
1979
+ * Geometric-interval lower bounds: class edges growing by a constant ratio
1980
+ * from the smallest positive value to the maximum, so each class spans the
1981
+ * same multiple of its lower bound. Suits skewed data spanning orders of
1982
+ * magnitude (population, income, concentrations).
1983
+ *
1984
+ * A geometric series cannot start at or below zero, so it starts at the
1985
+ * smallest positive value and the first break stays the sample minimum: zero
1986
+ * and negative values all fall into the first class. When no value is
1987
+ * positive, the result is `null` and the caller falls back to equal interval.
1988
+ */
1989
+ function geometricIntervalClassBreaks(sorted, count) {
1990
+ const max = sorted[sorted.length - 1];
1991
+ const lowest = sorted.find((value) => value > 0);
1992
+ if (lowest === void 0) return null;
1993
+ const logLowest = Math.log(lowest);
1994
+ const logStep = (Math.log(max) - logLowest) / count;
1995
+ return [sorted[0], ...Array.from({ length: count - 1 }, (_, index) => Math.exp(logLowest + logStep * (index + 1)))];
1996
+ }
1997
+ /** Drops non-finite breaks and collapses repeats, keeping ascending order. */
1998
+ function ascendingUnique(breaks) {
1999
+ const out = [];
2000
+ for (const value of breaks) {
2001
+ if (!Number.isFinite(value)) continue;
2002
+ if (out.length > 0 && value <= out[out.length - 1]) continue;
2003
+ out.push(value);
2004
+ }
2005
+ return out;
2006
+ }
2007
+ /** Cap on the Jenks input size; the DP below is roughly O(n^2 * k). */
2008
+ const MAX_NATURAL_BREAK_SAMPLES = 1e3;
2009
+ /** Evenly thins a sorted array down to at most `maxSamples` entries. */
2010
+ function downsampleSortedValues(values, maxSamples) {
2011
+ if (values.length <= maxSamples) return values;
2012
+ const result = [];
2013
+ const step = (values.length - 1) / (maxSamples - 1);
2014
+ for (let index = 0; index < maxSamples; index += 1) result.push(values[Math.round(index * step)]);
2015
+ return result;
2016
+ }
2017
+ /**
2018
+ * Jenks natural breaks: the class lower bounds that minimize the summed
2019
+ * within-class variance. Returns the sample minimum first, then the first value
2020
+ * of each subsequent class.
2021
+ */
2022
+ function naturalClassBreaks(values, count) {
2023
+ const sorted = downsampleSortedValues(Array.from(new Set(values)).sort((a, b) => a - b), MAX_NATURAL_BREAK_SAMPLES);
2024
+ if (sorted.length <= count) return sorted;
2025
+ const lowerClassLimits = Array.from({ length: sorted.length + 1 }, () => Array(count + 1).fill(0));
2026
+ const varianceCombinations = Array.from({ length: sorted.length + 1 }, () => Array(count + 1).fill(Number.POSITIVE_INFINITY));
2027
+ for (let classIndex = 1; classIndex <= count; classIndex += 1) {
2028
+ lowerClassLimits[1][classIndex] = 1;
2029
+ varianceCombinations[1][classIndex] = 0;
2030
+ }
2031
+ for (let valueIndex = 2; valueIndex <= sorted.length; valueIndex += 1) {
2032
+ let sum = 0;
2033
+ let sumSquares = 0;
2034
+ let weight = 0;
2035
+ for (let lowerIndex = 1; lowerIndex <= valueIndex; lowerIndex += 1) {
2036
+ const currentIndex = valueIndex - lowerIndex + 1;
2037
+ const value = sorted[currentIndex - 1];
2038
+ weight += 1;
2039
+ sum += value;
2040
+ sumSquares += value * value;
2041
+ const variance = sumSquares - sum * sum / weight;
2042
+ const previousIndex = currentIndex - 1;
2043
+ if (previousIndex === 0) continue;
2044
+ for (let classIndex = 2; classIndex <= count; classIndex += 1) {
2045
+ const candidate = variance + varianceCombinations[previousIndex][classIndex - 1];
2046
+ if (varianceCombinations[valueIndex][classIndex] >= candidate) {
2047
+ lowerClassLimits[valueIndex][classIndex] = currentIndex;
2048
+ varianceCombinations[valueIndex][classIndex] = candidate;
2049
+ }
2050
+ }
2051
+ }
2052
+ lowerClassLimits[valueIndex][1] = 1;
2053
+ varianceCombinations[valueIndex][1] = sumSquares - sum * sum / Math.max(1, weight);
2054
+ }
2055
+ const breaks = Array(count).fill(sorted[0]);
2056
+ let valueIndex = sorted.length;
2057
+ for (let classIndex = count; classIndex >= 2; classIndex -= 1) {
2058
+ const lowerClassLimit = Math.max(1, lowerClassLimits[valueIndex][classIndex]);
2059
+ breaks[classIndex - 1] = sorted[lowerClassLimit - 1];
2060
+ valueIndex = lowerClassLimit - 1;
2061
+ }
2062
+ breaks[0] = sorted[0];
2063
+ return breaks;
2064
+ }
1955
2065
  // src/polyline.ts
1956
2066
  /**
1957
2067
  * Pure TypeScript Google Encoded Polyline algorithm codec.
@@ -2829,6 +2939,975 @@ function inferFieldTypes(features, names) {
2829
2939
  }
2830
2940
  return result;
2831
2941
  }
2942
+ // src/vector-color.ts
2943
+ /** Whether a color value is a data-driven expression rather than a flat color. */
2944
+ function isVectorColorExpression(value) {
2945
+ return Array.isArray(value);
2946
+ }
2947
+ /**
2948
+ * Whether a string is a 6-digit hex color (`#rrggbb`), the form the vector
2949
+ * renderer treats as valid. Exported so the symbology exporters can apply the
2950
+ * same validity check the live `match`/`case` expressions use rather than
2951
+ * re-deriving the regex.
2952
+ */
2953
+ function isHexColor(value) {
2954
+ return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value.trim());
2955
+ }
2956
+ /** A 3- or 6-digit hex color, as emitted by the simplestyle spec. */
2957
+ function isSimpleStyleColor(value) {
2958
+ return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim());
2959
+ }
2960
+ /**
2961
+ * simplestyle-spec color and numeric property names. Color keys carry CSS hex
2962
+ * colors; numeric keys carry plain numbers. See
2963
+ * https://github.com/mapbox/simplestyle-spec.
2964
+ */
2965
+ const SIMPLE_STYLE_COLOR_KEYS = [
2966
+ "fill",
2967
+ "stroke",
2968
+ "marker-color"
2969
+ ];
2970
+ const SIMPLE_STYLE_NUMBER_KEYS = [
2971
+ "fill-opacity",
2972
+ "stroke-width",
2973
+ "stroke-opacity",
2974
+ "marker-opacity"
2975
+ ];
2976
+ function isSimpleStyleEnabled(style) {
2977
+ return styleValue(style, "simpleStyleEnabled") === true;
2978
+ }
2979
+ /**
2980
+ * Wrap a resolved color value so a per-feature simplestyle property takes
2981
+ * precedence when {@link LayerStyle.simpleStyleEnabled} is set. Returns the base
2982
+ * value unchanged when the feature lacks the property or the mode is off.
2983
+ *
2984
+ * @param style - The layer style.
2985
+ * @param property - The simplestyle property name (e.g. `fill`, `stroke`).
2986
+ * @param base - The flat color or expression to fall back to.
2987
+ * @returns A `coalesce` expression, or the base value when disabled.
2988
+ */
2989
+ function withSimpleStyleColor(style, property, base) {
2990
+ if (!isSimpleStyleEnabled(style)) return base;
2991
+ return mapZoomStepOutputs(base, (value) => [
2992
+ "coalesce",
2993
+ ["get", property],
2994
+ value
2995
+ ]);
2996
+ }
2997
+ /**
2998
+ * Apply a per-feature transform to a value, descending into the outputs of a
2999
+ * top-level `["step", ["zoom"], …]` expression (as produced by the rule-based
3000
+ * per-rule zoom compilation) so the zoom step stays outermost. MapLibre
3001
+ * rejects any expression where `["zoom"]` is not the input of a top-level
3002
+ * `step`/`interpolate`, so wrappers (simplestyle coalesce, opacity folds)
3003
+ * must be pushed inside the step's outputs rather than wrapped around it.
3004
+ * Non-step values are transformed directly.
3005
+ */
3006
+ function mapZoomStepOutputs(value, transform) {
3007
+ if (Array.isArray(value) && value[0] === "step" && Array.isArray(value[1]) && value[1][0] === "zoom") {
3008
+ const out = [
3009
+ "step",
3010
+ value[1],
3011
+ transform(value[2])
3012
+ ];
3013
+ for (let index = 3; index < value.length; index += 2) out.push(value[index], transform(value[index + 1]));
3014
+ return out;
3015
+ }
3016
+ return transform(value);
3017
+ }
3018
+ /**
3019
+ * Resolve a numeric paint value, letting a per-feature simplestyle property
3020
+ * override the layer value when {@link LayerStyle.simpleStyleEnabled} is set.
3021
+ *
3022
+ * The lookup is wrapped in a `coalesce` rather than leaning on `to-number`'s
3023
+ * own fallback argument, because that fallback only fires when a value fails
3024
+ * to convert — and a *missing* property does not fail, it converts to 0. Since
3025
+ * simplestyle is enabled for the whole layer as soon as any feature carries any
3026
+ * one of these keys, features that carry only some of them (an ArcGIS KML
3027
+ * export writes `fill`/`fill-opacity` but no `marker-opacity`, for instance)
3028
+ * would otherwise be painted at opacity 0 or width 0 — present in the layer,
3029
+ * invisible on the map (#1552).
3030
+ *
3031
+ * @param style - The layer style.
3032
+ * @param property - The simplestyle property name (e.g. `stroke-width`).
3033
+ * @param base - The layer-level fallback value.
3034
+ * @returns A `to-number` expression, or `base` when disabled.
3035
+ */
3036
+ function simpleStyleNumberValue(style, property, base) {
3037
+ if (!isSimpleStyleEnabled(style)) return base;
3038
+ return [
3039
+ "to-number",
3040
+ [
3041
+ "coalesce",
3042
+ ["get", property],
3043
+ base
3044
+ ],
3045
+ base
3046
+ ];
3047
+ }
3048
+ /** Whether `node` is `["get", key]` for one of `keys`, returning the key. */
3049
+ function simpleStyleGet(node, keys) {
3050
+ if (!Array.isArray(node) || node.length !== 2 || node[0] !== "get") return null;
3051
+ return keys.includes(node[1]) ? node[1] : null;
3052
+ }
3053
+ /**
3054
+ * Read back the per-feature color lookup {@link withSimpleStyleColor} writes,
3055
+ * `["coalesce", ["get", key], base]` with a simplestyle color key, so a style
3056
+ * importer recognizes exactly what the exporter emits.
3057
+ *
3058
+ * @param node - A paint value or a node inside one.
3059
+ * @returns The simplestyle key and the layer-level base, or `null` when the
3060
+ * node is not that lookup.
3061
+ */
3062
+ function readSimpleStyleColorLookup(node) {
3063
+ if (!Array.isArray(node) || node.length !== 3 || node[0] !== "coalesce") return null;
3064
+ const property = simpleStyleGet(node[1], SIMPLE_STYLE_COLOR_KEYS);
3065
+ return property ? {
3066
+ property,
3067
+ base: node[2]
3068
+ } : null;
3069
+ }
3070
+ /**
3071
+ * Read back the per-feature number lookup {@link simpleStyleNumberValue}
3072
+ * writes, `["to-number", ["coalesce", ["get", key], base], base]` with a
3073
+ * simplestyle number key and a numeric base.
3074
+ *
3075
+ * @param node - A paint value or a node inside one.
3076
+ * @returns The simplestyle key and the layer-level base, or `null` when the
3077
+ * node is not that lookup.
3078
+ */
3079
+ function readSimpleStyleNumberLookup(node) {
3080
+ if (!Array.isArray(node) || node.length !== 3 || node[0] !== "to-number") return null;
3081
+ const inner = node[1];
3082
+ if (!Array.isArray(inner) || inner.length !== 3 || inner[0] !== "coalesce") return null;
3083
+ const property = simpleStyleGet(inner[1], SIMPLE_STYLE_NUMBER_KEYS);
3084
+ const base = node[2];
3085
+ if (!property || typeof base !== "number" || !Number.isFinite(base) || inner[2] !== base) return null;
3086
+ return {
3087
+ property,
3088
+ base
3089
+ };
3090
+ }
3091
+ /**
3092
+ * Whether a paint value reads per-feature simplestyle properties the way the
3093
+ * GeoLibre exporter writes them, anywhere inside it (a lookup can sit inside a
3094
+ * zoom step, a per-rule `case` or an opacity multiplication).
3095
+ *
3096
+ * @param value - A paint value.
3097
+ * @returns `true` when a color or number lookup is found.
3098
+ */
3099
+ function readsSimpleStyleProperties(value) {
3100
+ if (!Array.isArray(value)) return false;
3101
+ if (readSimpleStyleColorLookup(value) || readSimpleStyleNumberLookup(value)) return true;
3102
+ return value.some(readsSimpleStyleProperties);
3103
+ }
3104
+ /**
3105
+ * Ground resolution (meters per pixel) at MapLibre zoom 0 on the equator, for
3106
+ * the Web Mercator projection: earth circumference (2*pi*6378137) over the
3107
+ * 512px world at zoom 0. Resolution halves with every zoom level. Exported so
3108
+ * the Mapbox-style importer can reverse {@link metersWidthExpression} without a
3109
+ * second copy of the magic number.
3110
+ */
3111
+ const MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 = 2 * Math.PI * 6378137 / 512;
3112
+ /**
3113
+ * Same ground resolution as {@link MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0} but for
3114
+ * the project's active body: its circumference over the 512px zoom-0 world. On
3115
+ * Earth this equals the constant; on the Moon/Mars it uses that body's radius so
3116
+ * a stroke width given in ground meters renders at the correct on-screen size.
3117
+ * Exported so the Mapbox-style importer can reverse {@link metersWidthExpression}
3118
+ * with the same ellipsoid-aware factor the exporter used (keeping the round trip
3119
+ * correct on non-Earth projects), rather than the Earth-only constant.
3120
+ */
3121
+ function mercatorMetersPerPixelAtZoom0() {
3122
+ return 2 * Math.PI * getActiveSemiMajorAxisMeters() / 512;
3123
+ }
3124
+ const MAX_MERCATOR_ZOOM = 24;
3125
+ /**
3126
+ * Build a zoom-driven width expression that keeps a stroke proportional to the
3127
+ * map scale, so a width given in ground meters renders thicker when zoomed in
3128
+ * and thinner when zoomed out (QGIS "map units" behavior).
3129
+ *
3130
+ * In Web Mercator the pixels-per-meter ratio doubles with each zoom level, so
3131
+ * an `["exponential", 2]` interpolation between two stops one zoom apart is
3132
+ * exact across the whole range. The conversion is referenced to the equator;
3133
+ * because Mercator stretches distances toward the poles, the on-screen width at
3134
+ * higher latitudes is correspondingly larger, matching how the underlying map
3135
+ * is itself stretched.
3136
+ *
3137
+ * Typed maplibre-agnostically (`unknown[]`); consumers cast to the concrete
3138
+ * `PropertyValueSpecification<number>` where the MapLibre types are in scope.
3139
+ *
3140
+ * @param meters - The stroke width in ground meters.
3141
+ * @returns A MapLibre `interpolate` expression array.
3142
+ */
3143
+ function metersWidthExpression(meters) {
3144
+ const widthAtZoom0 = meters / mercatorMetersPerPixelAtZoom0();
3145
+ return [
3146
+ "interpolate",
3147
+ ["exponential", 2],
3148
+ ["zoom"],
3149
+ 0,
3150
+ widthAtZoom0,
3151
+ MAX_MERCATOR_ZOOM,
3152
+ widthAtZoom0 * 2 ** MAX_MERCATOR_ZOOM
3153
+ ];
3154
+ }
3155
+ /**
3156
+ * Resolve the `line-width` paint value for a layer style, honoring the
3157
+ * {@link LayerStyle.strokeWidthUnit}:
3158
+ *
3159
+ * - `"meters"`: a zoom-driven {@link metersWidthExpression} from the flat
3160
+ * `strokeWidth`, so the stroke scales with the map. A per-feature pixel
3161
+ * `stroke-width` override no longer applies in this mode.
3162
+ * - `"pixels"` (default): the constant pixel width, still honoring any
3163
+ * per-feature simplestyle `stroke-width`.
3164
+ *
3165
+ * Shared by the map style-mapper and the geo-editor plugin so the Sketches
3166
+ * store layer and Geoman's interaction display layers render an identical
3167
+ * width.
3168
+ *
3169
+ * @param style - The layer style.
3170
+ * @returns A number (constant pixels) or a MapLibre expression array.
3171
+ */
3172
+ function lineWidthValue(style) {
3173
+ const proportionalWidth = proportionalRadiusExpression(style);
3174
+ if (proportionalWidth) return vectorStrokeWidthValue(style, proportionalWidth);
3175
+ if (styleValue(style, "strokeWidthUnit") === "meters") return metersWidthExpression(styleValue(style, "strokeWidth"));
3176
+ return vectorStrokeWidthValue(style, simpleStyleNumberValue(style, "stroke-width", styleValue(style, "strokeWidth")));
3177
+ }
3178
+ /**
3179
+ * The layer's stroke opacity, clamped to [0, 1]. A missing or non-finite value
3180
+ * (an older project, a hand-edited file) reads as fully opaque.
3181
+ *
3182
+ * @param style - The layer style.
3183
+ * @returns The stroke opacity.
3184
+ */
3185
+ function strokeOpacityValue(style) {
3186
+ const value = styleValue(style, "strokeOpacity");
3187
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
3188
+ }
3189
+ /**
3190
+ * A stroke opacity paint value: the layer's {@link LayerStyle.strokeOpacity},
3191
+ * which a per-feature simplestyle `stroke-opacity` replaces when
3192
+ * {@link LayerStyle.simpleStyleEnabled} is set.
3193
+ *
3194
+ * @param style - The layer style.
3195
+ * @returns A number, or the simplestyle lookup expression.
3196
+ */
3197
+ function strokeOpacityPaintValue(style) {
3198
+ return simpleStyleNumberValue(style, "stroke-opacity", strokeOpacityValue(style));
3199
+ }
3200
+ /**
3201
+ * Whether a value is a usable dash pattern: finite, non-negative lengths with
3202
+ * at least one dash longer than zero. Exported so the style sanitizer and the
3203
+ * importers apply the same rule the renderer does.
3204
+ *
3205
+ * @param value - The candidate pattern.
3206
+ * @returns True when the value can be drawn as a dash pattern.
3207
+ */
3208
+ function isStrokeDashPattern(value) {
3209
+ return Array.isArray(value) && value.length > 0 && value.every((length) => typeof length === "number" && Number.isFinite(length) && length >= 0) && value.some((length) => length > 0);
3210
+ }
3211
+ /**
3212
+ * The `line-dasharray` for the layer, or `null` for a solid line. `null` is
3213
+ * what MapLibre's `setPaintProperty` takes to reset a property, so returning it
3214
+ * (rather than leaving the key out) clears a dash when the user goes back to
3215
+ * solid.
3216
+ *
3217
+ * @param style - The layer style.
3218
+ * @returns The dash pattern, or `null` when the line is solid.
3219
+ */
3220
+ function strokeDashValue(style) {
3221
+ const dash = styleValue(style, "strokeDash");
3222
+ return isStrokeDashPattern(dash) ? [...dash] : null;
3223
+ }
3224
+ /**
3225
+ * The {@link STROKE_DASH_PRESETS} name matching a pattern, or `"custom"` when
3226
+ * none does. An empty or unusable pattern is `"solid"`.
3227
+ *
3228
+ * @param dash - A dash pattern.
3229
+ * @returns The preset name, or `"custom"`.
3230
+ */
3231
+ function strokeDashPresetName(dash) {
3232
+ if (!isStrokeDashPattern(dash)) return "solid";
3233
+ for (const [name, pattern] of Object.entries(STROKE_DASH_PRESETS)) if (pattern.length === dash.length && pattern.every((length, index) => length === dash[index])) return name;
3234
+ return "custom";
3235
+ }
3236
+ /**
3237
+ * The layer's line blur in pixels; a missing, negative or non-finite value is 0.
3238
+ *
3239
+ * @param style - The layer style.
3240
+ * @returns The blur in pixels.
3241
+ */
3242
+ function strokeBlurValue(style) {
3243
+ const value = styleValue(style, "strokeBlur");
3244
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
3245
+ }
3246
+ /**
3247
+ * Whether a FeatureCollection carries per-feature simplestyle-spec properties
3248
+ * worth honoring: at least one feature with a valid hex color in a color key
3249
+ * (`fill`/`stroke`/`marker-color`) or a finite number in a numeric key
3250
+ * (`fill-opacity`/`stroke-width`/`stroke-opacity`). The scan is capped so very
3251
+ * large collections do not pay a full pass.
3252
+ *
3253
+ * @param geojson - The collection to inspect (may be undefined).
3254
+ * @returns `true` when simplestyle rendering should be enabled for the layer.
3255
+ */
3256
+ function hasSimpleStyleProperties(geojson) {
3257
+ const features = geojson?.features;
3258
+ if (!features?.length) return false;
3259
+ const limit = Math.min(features.length, 1e3);
3260
+ for (let index = 0; index < limit; index += 1) {
3261
+ const properties = features[index]?.properties;
3262
+ if (!properties) continue;
3263
+ for (const key of SIMPLE_STYLE_COLOR_KEYS) {
3264
+ const value = properties[key];
3265
+ if (typeof value === "string" && isSimpleStyleColor(value)) return true;
3266
+ }
3267
+ for (const key of SIMPLE_STYLE_NUMBER_KEYS) {
3268
+ const value = properties[key];
3269
+ if (typeof value === "number" && Number.isFinite(value)) return true;
3270
+ }
3271
+ }
3272
+ return false;
3273
+ }
3274
+ /**
3275
+ * Parses a user-entered MapLibre expression string into an expression array,
3276
+ * tolerating trailing commas. Returns null when the text is empty or not a
3277
+ * JSON array.
3278
+ */
3279
+ function parseJsonExpression(expression) {
3280
+ const trimmed = expression.trim();
3281
+ if (!trimmed) return null;
3282
+ try {
3283
+ const parsed = JSON.parse(removeTrailingJsonCommas(trimmed));
3284
+ if (!Array.isArray(parsed)) return null;
3285
+ return parsed;
3286
+ } catch {
3287
+ return null;
3288
+ }
3289
+ }
3290
+ /**
3291
+ * Builds the data-driven color value for a vector layer's current style mode.
3292
+ * `single` (or any mode that cannot produce a valid expression) returns the
3293
+ * flat fallback color; `categorized` returns a `match` expression, `graduated`
3294
+ * a `step` expression, and `expression` the parsed user expression.
3295
+ *
3296
+ * Graduated stops are class lower bounds (see
3297
+ * {@link createGraduatedClassBreaks}), so the classes are discrete: the map
3298
+ * paints exactly one color per class, which is what the legend (`≥ value`) and
3299
+ * the QML/SLD exporters already describe. Rendering them as a continuous
3300
+ * `interpolate` instead blends a shade per feature, so a 4-class layer draws in
3301
+ * many more than 4 colors and stops matching its own legend (#1384).
3302
+ *
3303
+ * @param style - The layer style.
3304
+ * @param fallbackColor - The flat color used for `single` mode and as the
3305
+ * expression fallback.
3306
+ * @returns A flat color string or a MapLibre color expression.
3307
+ */
3308
+ function vectorColorExpression(style, fallbackColor) {
3309
+ const mode = styleValue(style, "vectorStyleMode");
3310
+ if (mode === "single") return fallbackColor;
3311
+ if (mode === "expression") return parseJsonExpression(styleValue(style, "vectorStyleExpression")) ?? fallbackColor;
3312
+ if (mode === "rule-based") return ruleBasedColorExpression(style, fallbackColor);
3313
+ const property = styleValue(style, "vectorStyleProperty").trim();
3314
+ if (!property) return fallbackColor;
3315
+ if (mode === "categorized") {
3316
+ const stops = styleValue(style, "vectorStyleStops").filter((stop) => String(stop.value).trim().length > 0 && isHexColor(stop.color));
3317
+ if (stops.length === 0) return fallbackColor;
3318
+ return [
3319
+ "match",
3320
+ ["to-string", ["get", property]],
3321
+ ...stops.flatMap((stop) => [String(stop.value).trim(), stop.color]),
3322
+ fallbackColor
3323
+ ];
3324
+ }
3325
+ const stops = graduatedStops(style);
3326
+ if (stops.length < 2) return fallbackColor;
3327
+ return [
3328
+ "step",
3329
+ [
3330
+ "to-number",
3331
+ ["get", property],
3332
+ stops[0].value
3333
+ ],
3334
+ stops[0].color,
3335
+ ...stops.slice(1).flatMap((stop) => [stop.value, stop.color])
3336
+ ];
3337
+ }
3338
+ /**
3339
+ * The graduated stops of a style, cleaned for rendering: non-numeric values and
3340
+ * invalid colors dropped, sorted ascending, and de-duplicated. MapLibre rejects
3341
+ * a `step` expression whose inputs are not strictly ascending, and stops reach
3342
+ * here from hand-edited projects and style imports as well as the Style panel,
3343
+ * so repeats are dropped rather than trusted away.
3344
+ */
3345
+ function graduatedStops(style) {
3346
+ const sorted = styleValue(style, "vectorStyleStops").map((stop) => ({
3347
+ color: stop.color,
3348
+ value: typeof stop.value === "number" ? stop.value : Number.parseFloat(stop.value)
3349
+ })).filter((stop) => Number.isFinite(stop.value) && isHexColor(stop.color)).sort((a, b) => a.value - b.value);
3350
+ return sorted.filter((stop, index) => index === 0 || stop.value > sorted[index - 1].value);
3351
+ }
3352
+ /** A finite per-rule zoom bound, or undefined when unset/invalid. */
3353
+ function ruleZoom(value) {
3354
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3355
+ }
3356
+ /**
3357
+ * Resolve the layer's rule tree into the drawable leaf rules (see
3358
+ * {@link EffectiveVectorRule}) plus the catch-all else rule. Disabled rules
3359
+ * (and every descendant of a disabled group), rules with an invalid color or
3360
+ * filter, subtrees under a group whose non-blank filter does not parse, and
3361
+ * rules whose intersected zoom range is empty are all dropped. Cycles in
3362
+ * `parentId` are treated as broken chains and dropped rather than looping.
3363
+ *
3364
+ * @param style - The layer style (reads {@link LayerStyle.vectorRules}).
3365
+ * @returns The ordered drawable rules and the enabled else rule, if any.
3366
+ */
3367
+ function effectiveVectorRules(style) {
3368
+ const all = styleValue(style, "vectorRules");
3369
+ const elseRule = all.find((rule) => rule.isElse && rule.enabled !== false) ?? null;
3370
+ const byId = /* @__PURE__ */ new Map();
3371
+ for (const rule of all) if (!rule.isElse) byId.set(rule.id, rule);
3372
+ const groupIds = /* @__PURE__ */ new Set();
3373
+ for (const rule of byId.values()) if (rule.parentId && rule.parentId !== rule.id && byId.has(rule.parentId)) groupIds.add(rule.parentId);
3374
+ const rules = [];
3375
+ for (const rule of all) {
3376
+ if (rule.isElse || groupIds.has(rule.id)) continue;
3377
+ if (rule.enabled === false || !isHexColor(rule.color)) continue;
3378
+ const own = parseJsonExpression(rule.filter);
3379
+ if (!own || typeof own[0] !== "string") continue;
3380
+ const filters = [own];
3381
+ let minZoom = ruleZoom(rule.minZoom);
3382
+ let maxZoom = ruleZoom(rule.maxZoom);
3383
+ let dropped = false;
3384
+ const seen = /* @__PURE__ */ new Set([rule.id]);
3385
+ let parent = rule.parentId && rule.parentId !== rule.id ? byId.get(rule.parentId) : void 0;
3386
+ while (parent) {
3387
+ if (seen.has(parent.id) || parent.enabled === false) {
3388
+ dropped = true;
3389
+ break;
3390
+ }
3391
+ seen.add(parent.id);
3392
+ if (parent.filter.trim()) {
3393
+ const parentFilter = parseJsonExpression(parent.filter);
3394
+ if (!parentFilter || typeof parentFilter[0] !== "string") {
3395
+ dropped = true;
3396
+ break;
3397
+ }
3398
+ filters.unshift(parentFilter);
3399
+ }
3400
+ const parentMin = ruleZoom(parent.minZoom);
3401
+ const parentMax = ruleZoom(parent.maxZoom);
3402
+ if (parentMin !== void 0) minZoom = minZoom === void 0 ? parentMin : Math.max(minZoom, parentMin);
3403
+ if (parentMax !== void 0) maxZoom = maxZoom === void 0 ? parentMax : Math.min(maxZoom, parentMax);
3404
+ parent = parent.parentId && parent.parentId !== parent.id ? byId.get(parent.parentId) : void 0;
3405
+ }
3406
+ if (dropped) continue;
3407
+ if (minZoom !== void 0 && maxZoom !== void 0 && minZoom >= maxZoom) continue;
3408
+ rules.push({
3409
+ id: rule.id,
3410
+ label: rule.label,
3411
+ color: rule.color,
3412
+ filter: filters.length === 1 ? filters[0] : ["all", ...filters],
3413
+ ...minZoom !== void 0 ? { minZoom } : {},
3414
+ ...maxZoom !== void 0 ? { maxZoom } : {},
3415
+ ...isHexColor(rule.strokeColor) ? { strokeColor: rule.strokeColor } : {},
3416
+ ...typeof rule.strokeWidth === "number" && Number.isFinite(rule.strokeWidth) ? { strokeWidth: rule.strokeWidth } : {},
3417
+ ...typeof rule.fillOpacity === "number" && Number.isFinite(rule.fillOpacity) ? { fillOpacity: rule.fillOpacity } : {},
3418
+ ...typeof rule.circleRadius === "number" && Number.isFinite(rule.circleRadius) ? { circleRadius: rule.circleRadius } : {}
3419
+ });
3420
+ }
3421
+ return {
3422
+ rules,
3423
+ elseRule
3424
+ };
3425
+ }
3426
+ /**
3427
+ * A rule's match condition as a MapLibre filter: its (ancestor-combined)
3428
+ * attribute filter, with any scale range folded in as `["zoom"]` comparisons.
3429
+ * The zoom window is half-open (`min <= zoom < max`), matching how
3430
+ * {@link zoomWrappedRuleValue} activates rules per zoom segment. MapLibre only
3431
+ * re-evaluates `["zoom"]` in filters at integer zoom levels, so a scale-ranged
3432
+ * rule's features appear/disappear on whole zooms — the paint expressions keep
3433
+ * the fractional-zoom precision.
3434
+ */
3435
+ function ruleMatchFilter(rule) {
3436
+ const conditions = [rule.filter];
3437
+ if (rule.minZoom !== void 0) conditions.push([
3438
+ ">=",
3439
+ ["zoom"],
3440
+ rule.minZoom
3441
+ ]);
3442
+ if (rule.maxZoom !== void 0) conditions.push([
3443
+ "<",
3444
+ ["zoom"],
3445
+ rule.maxZoom
3446
+ ]);
3447
+ return conditions.length === 1 ? rule.filter : ["all", ...conditions];
3448
+ }
3449
+ const visibilityFilterCache = /* @__PURE__ */ new WeakMap();
3450
+ /**
3451
+ * The MapLibre filter that hides features matching no rule, for a rule-based
3452
+ * layer whose catch-all else rule has been switched off. Returns `null` — and
3453
+ * callers then leave their geometry filters untouched — unless the layer is in
3454
+ * `"rule-based"` mode AND carries an else record explicitly disabled
3455
+ * (`enabled: false`). An absent else record keeps the historical fallback
3456
+ * rendering, so projects saved before the toggle existed are unaffected.
3457
+ *
3458
+ * With the else rule off, the returned `["any", filter1, filter2, …]` keeps
3459
+ * only features matched by a drawable rule (see {@link effectiveVectorRules});
3460
+ * with no drawable rules it returns the vacuous `["any"]`, which matches
3461
+ * nothing, so every feature hides. Sub-layers combine it with their geometry
3462
+ * filters, which also drops unmatched features from labels and hit-testing —
3463
+ * mirroring QGIS, where features matching no rule are simply not drawn.
3464
+ *
3465
+ * @param style - The layer style (reads {@link LayerStyle.vectorRules}).
3466
+ * @returns The filter to `["all", …]`-combine into each sub-layer, or `null`.
3467
+ */
3468
+ function ruleBasedVisibilityFilter(style) {
3469
+ if (styleValue(style, "vectorStyleMode") !== "rule-based") return null;
3470
+ const all = styleValue(style, "vectorRules");
3471
+ const cached = visibilityFilterCache.get(all);
3472
+ if (cached !== void 0) return cached;
3473
+ const elseRecord = all.find((rule) => rule.isElse);
3474
+ const filter = !elseRecord || elseRecord.enabled !== false ? null : ["any", ...effectiveVectorRules(style).rules.map(ruleMatchFilter)];
3475
+ visibilityFilterCache.set(all, filter);
3476
+ return filter;
3477
+ }
3478
+ /**
3479
+ * Wrap a per-zoom-segment value builder in a `["step", ["zoom"], …]` expression
3480
+ * when any rule carries a zoom bound. MapLibre only allows `["zoom"]` as the
3481
+ * top-level input of a `step`/`interpolate` in paint properties (not inside a
3482
+ * `case` condition), so per-rule scale ranges compile by splitting the zoom
3483
+ * axis at every rule bound and rebuilding the `case` expression per segment
3484
+ * with only the rules active in that segment. With no zoom bounds the builder
3485
+ * runs once and its plain value is returned unchanged, keeping the historical
3486
+ * `case` shape (which the Mapbox-style importer recognizes).
3487
+ */
3488
+ function zoomWrappedRuleValue(rules, build) {
3489
+ const bounds = /* @__PURE__ */ new Set();
3490
+ for (const rule of rules) {
3491
+ if (rule.minZoom !== void 0) bounds.add(rule.minZoom);
3492
+ if (rule.maxZoom !== void 0) bounds.add(rule.maxZoom);
3493
+ }
3494
+ if (bounds.size === 0) return build(rules);
3495
+ const breaks = [...bounds].sort((a, b) => a - b);
3496
+ const activeAt = (segmentStart) => rules.filter((rule) => (rule.minZoom ?? -Infinity) <= segmentStart && segmentStart < (rule.maxZoom ?? Infinity));
3497
+ const expression = [
3498
+ "step",
3499
+ ["zoom"],
3500
+ build(activeAt(-Infinity))
3501
+ ];
3502
+ for (const zoom of breaks) expression.push(zoom, build(activeAt(zoom)));
3503
+ return expression;
3504
+ }
3505
+ /**
3506
+ * Compiles the `"rule-based"` renderer's ordered rules into a MapLibre `case`
3507
+ * color expression: `["case", filter1, color1, filter2, color2, …, elseColor]`.
3508
+ * Rules are evaluated top to bottom; the first matching filter wins. Disabled
3509
+ * rules and rules with an invalid filter JSON or a non-hex color are skipped;
3510
+ * nested rules AND their ancestors' filters (see {@link effectiveVectorRules}).
3511
+ * When any rule carries a zoom range the `case` is rebuilt per zoom segment
3512
+ * inside a `["step", ["zoom"], …]` wrapper. The catch-all (`isElse`) rule
3513
+ * supplies the trailing fallback; when absent or invalid the layer
3514
+ * `fallbackColor` is used. With no usable rules the flat fallback color is
3515
+ * returned.
3516
+ *
3517
+ * @param style - The layer style (reads {@link LayerStyle.vectorRules}).
3518
+ * @param fallbackColor - The color used when no else rule defines one.
3519
+ * @returns A MapLibre expression, or a flat color when no rule applies.
3520
+ */
3521
+ function ruleBasedColorExpression(style, fallbackColor) {
3522
+ const { rules, elseRule } = effectiveVectorRules(style);
3523
+ const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : fallbackColor;
3524
+ if (rules.length === 0) return elseColor;
3525
+ return zoomWrappedRuleValue(rules, (active) => {
3526
+ if (active.length === 0) return elseColor;
3527
+ return [
3528
+ "case",
3529
+ ...active.flatMap((rule) => [rule.filter, rule.color]),
3530
+ elseColor
3531
+ ];
3532
+ });
3533
+ }
3534
+ /**
3535
+ * Build a paint value honoring per-rule symbol overrides for one field: a
3536
+ * `case` expression (zoom-wrapped when rules carry scale ranges) where each
3537
+ * drawable rule's branch yields its override, or the layer fallback when that
3538
+ * rule does not override the field. Returns the fallback unchanged when the
3539
+ * layer is not in rule-based mode or no rule (including the else rule)
3540
+ * overrides the field, so simple styles keep their flat paint values.
3541
+ *
3542
+ * First-match semantics are preserved: every drawable rule contributes a
3543
+ * branch (even without an override) so a feature matched by an earlier rule
3544
+ * never picks up a later rule's override.
3545
+ */
3546
+ function ruleOverrideValue(style, key, fallback) {
3547
+ if (styleValue(style, "vectorStyleMode") !== "rule-based") return fallback;
3548
+ const { rules, elseRule } = effectiveVectorRules(style);
3549
+ const elseOverride = readRuleOverride(elseRule ?? void 0, key);
3550
+ if (elseOverride === void 0 && !rules.some((rule) => rule[key] !== void 0)) return fallback;
3551
+ const elseValue = elseOverride ?? fallback;
3552
+ return zoomWrappedRuleValue(rules, (active) => {
3553
+ if (active.length === 0) return elseValue;
3554
+ return [
3555
+ "case",
3556
+ ...active.flatMap((rule) => [rule.filter, rule[key] ?? fallback]),
3557
+ elseValue
3558
+ ];
3559
+ });
3560
+ }
3561
+ /** A validated override read straight off a (possibly else) {@link VectorRule}. */
3562
+ function readRuleOverride(rule, key) {
3563
+ const value = rule?.[key];
3564
+ if (key === "strokeColor") return isHexColor(value) ? value : void 0;
3565
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3566
+ }
3567
+ /**
3568
+ * The stroke/outline color for polygon outlines and circle strokes, honoring
3569
+ * per-rule {@link VectorRule.strokeColor} overrides in rule-based mode.
3570
+ * Falls back to the flat layer stroke color.
3571
+ */
3572
+ function vectorOutlineColorValue(style) {
3573
+ return ruleOverrideValue(style, "strokeColor", styleValue(style, "strokeColor"));
3574
+ }
3575
+ /**
3576
+ * A stroke-width paint value honoring per-rule {@link VectorRule.strokeWidth}
3577
+ * overrides in rule-based mode; `fallback` is the layer-level width (a number
3578
+ * or an already-built expression such as the meters-based interpolation).
3579
+ */
3580
+ function vectorStrokeWidthValue(style, fallback) {
3581
+ return ruleOverrideValue(style, "strokeWidth", fallback);
3582
+ }
3583
+ /**
3584
+ * A fill/circle opacity paint value honoring per-rule
3585
+ * {@link VectorRule.fillOpacity} overrides in rule-based mode; `fallback` is
3586
+ * the layer-level opacity value (number or expression).
3587
+ */
3588
+ function vectorFillOpacityValue(style, fallback) {
3589
+ return ruleOverrideValue(style, "fillOpacity", fallback);
3590
+ }
3591
+ /**
3592
+ * The color-ramp colors a density heatmap paints with.
3593
+ *
3594
+ * {@link getVectorColorRamp} falls back to the *first* ramp ("viridis") for an
3595
+ * unknown name, which is not the heatmap default ("turbo"). Both the map paint
3596
+ * (`heatmapPaint` in `@geolibre/map`) and the auto-legend gradient resolve the
3597
+ * ramp here so a stale or hand-edited `heatmapColorRamp` cannot make the legend
3598
+ * and the rendered heatmap disagree about which ramp is in use.
3599
+ *
3600
+ * @param style - The layer style.
3601
+ * @returns The ramp's colors, or the default ramp's colors for an unknown name.
3602
+ */
3603
+ function heatmapRampColors(style) {
3604
+ const value = styleValue(style, "heatmapColorRamp");
3605
+ const ramp = getVectorColorRamp(value);
3606
+ return (ramp.value === value ? ramp : getVectorColorRamp(DEFAULT_LAYER_STYLE.heatmapColorRamp)).colors;
3607
+ }
3608
+ /**
3609
+ * The proportional-size configuration, or `null` when proportional sizing
3610
+ * does not apply: disabled, no field chosen, non-finite or degenerate
3611
+ * (`maxValue <= minValue`) value range, or non-finite radii. This is the
3612
+ * single validation chain for every proportional consumer — circle radius,
3613
+ * line width, and marker icon-size (`@geolibre/map`) — so their notion of
3614
+ * "active" can never drift apart.
3615
+ *
3616
+ * @param style - The layer style.
3617
+ * @returns The validated range, or `null` when proportional sizing is off.
3618
+ */
3619
+ function proportionalSizeRange(style) {
3620
+ if (!styleValue(style, "proportionalSizeEnabled")) return null;
3621
+ return validatedSizeRange(styleValue(style, "proportionalSizeProperty"), styleValue(style, "proportionalSizeMinValue"), styleValue(style, "proportionalSizeMaxValue"), styleValue(style, "proportionalSizeMinRadius"), styleValue(style, "proportionalSizeMaxRadius"));
3622
+ }
3623
+ /**
3624
+ * The validation every field-driven size range shares: a chosen field, a
3625
+ * finite non-degenerate value span, and finite radii. Kept in one place so a
3626
+ * future tightening (rejecting negative radii, say) cannot land on the
3627
+ * layer-wide proportional renderer without also landing on the geometry
3628
+ * generator's centroids.
3629
+ */
3630
+ function validatedSizeRange(rawProperty, minValue, maxValue, minRadius, maxRadius) {
3631
+ const property = rawProperty.trim();
3632
+ if (!property) return null;
3633
+ if (!(Number.isFinite(minValue) && Number.isFinite(maxValue))) return null;
3634
+ if (maxValue <= minValue) return null;
3635
+ if (!(Number.isFinite(minRadius) && Number.isFinite(maxRadius))) return null;
3636
+ return {
3637
+ property,
3638
+ minValue,
3639
+ maxValue,
3640
+ minRadius,
3641
+ maxRadius
3642
+ };
3643
+ }
3644
+ /**
3645
+ * The proportional `interpolate` expression mapping
3646
+ * `proportionalSizeMinValue..proportionalSizeMaxValue` onto
3647
+ * `proportionalSizeMinRadius..proportionalSizeMaxRadius`, or `null` when
3648
+ * proportional sizing does not apply (see {@link proportionalSizeRange}).
3649
+ *
3650
+ * @param style - The layer style.
3651
+ * @returns The `interpolate` expression, or `null`.
3652
+ */
3653
+ function proportionalRadiusExpression(style) {
3654
+ const range = proportionalSizeRange(style);
3655
+ return range ? radiusInterpolateExpression(range) : null;
3656
+ }
3657
+ /**
3658
+ * The `interpolate` that maps a validated range's value span onto its radius
3659
+ * span. Shared by the layer-wide proportional renderer and the geometry
3660
+ * generator's centroid sizing so both scale identically (linear, with the
3661
+ * value clamped to the range's ends by `interpolate`'s own stop behavior).
3662
+ */
3663
+ function radiusInterpolateExpression(range) {
3664
+ return [
3665
+ "interpolate",
3666
+ ["linear"],
3667
+ [
3668
+ "to-number",
3669
+ ["get", range.property],
3670
+ range.minValue
3671
+ ],
3672
+ range.minValue,
3673
+ Math.max(0, range.minRadius),
3674
+ range.maxValue,
3675
+ Math.max(0, range.maxRadius)
3676
+ ];
3677
+ }
3678
+ /**
3679
+ * The geometry generator's centroid-size configuration, or `null` when the
3680
+ * generated points should all take the flat
3681
+ * {@link LayerStyle.geometryGeneratorCircleRadius}: no field chosen, non-finite
3682
+ * or degenerate (`maxValue <= minValue`) value range, or non-finite radii.
3683
+ * Mirrors {@link proportionalSizeRange} against the generator's own keys.
3684
+ *
3685
+ * @param style - The layer style.
3686
+ * @returns The validated range, or `null` when field-driven sizing is off.
3687
+ */
3688
+ function generatorSizeRange(style) {
3689
+ return validatedSizeRange(styleValue(style, "geometryGeneratorSizeProperty"), styleValue(style, "geometryGeneratorSizeMinValue"), styleValue(style, "geometryGeneratorSizeMaxValue"), styleValue(style, "geometryGeneratorSizeMinRadius"), styleValue(style, "geometryGeneratorSizeMaxRadius"));
3690
+ }
3691
+ /**
3692
+ * Builds the `circle-radius` paint value for the geometry generator's derived
3693
+ * centroid points: an `interpolate` over
3694
+ * {@link LayerStyle.geometryGeneratorSizeProperty} when field-driven sizing
3695
+ * applies (see {@link generatorSizeRange}), otherwise the flat
3696
+ * {@link LayerStyle.geometryGeneratorCircleRadius}.
3697
+ *
3698
+ * The flat fallback keeps the 1px floor the generator has always applied; the
3699
+ * interpolate is left as authored, so a 0px min radius still hides the
3700
+ * smallest symbols on purpose.
3701
+ *
3702
+ * @param style - The layer style.
3703
+ * @returns A constant radius (pixels) or a MapLibre `interpolate` expression.
3704
+ */
3705
+ function generatorCircleRadiusValue(style) {
3706
+ const range = generatorSizeRange(style);
3707
+ if (range) return radiusInterpolateExpression(range);
3708
+ return Math.max(1, styleValue(style, "geometryGeneratorCircleRadius"));
3709
+ }
3710
+ /**
3711
+ * Builds the `circle-radius` paint value, honoring proportional (graduated)
3712
+ * symbol sizing. When {@link LayerStyle.proportionalSizeEnabled} is set with a
3713
+ * chosen numeric field and a valid value range, returns an `interpolate` that
3714
+ * maps `proportionalSizeMinValue..proportionalSizeMaxValue` onto
3715
+ * `proportionalSizeMinRadius..proportionalSizeMaxRadius`; otherwise the constant
3716
+ * {@link LayerStyle.circleRadius}.
3717
+ *
3718
+ * @param style - The layer style.
3719
+ * @returns A constant radius (pixels) or a MapLibre `interpolate` expression.
3720
+ */
3721
+ function circleRadiusValue(style) {
3722
+ return ruleOverrideValue(style, "circleRadius", baseCircleRadiusValue(style));
3723
+ }
3724
+ /** The layer-level circle radius before per-rule overrides. */
3725
+ function baseCircleRadiusValue(style) {
3726
+ return proportionalRadiusExpression(style) ?? styleValue(style, "circleRadius");
3727
+ }
3728
+ /** Fill color value for a polygon layer (fallback: the layer fill color). */
3729
+ function vectorFillColorValue(style) {
3730
+ return withSimpleStyleColor(style, "fill", vectorColorExpression(style, styleValue(style, "fillColor")));
3731
+ }
3732
+ /**
3733
+ * Circle color value for a point layer. Intentionally identical to
3734
+ * `vectorFillColorValue`: GeoLibre has no separate point-fill color, so point
3735
+ * circles share the polygon fill color (matching `circlePaint` in the map
3736
+ * package). Kept as its own function so the per-geometry callers read in
3737
+ * parallel and a future dedicated circle color stays a one-line change here.
3738
+ */
3739
+ function vectorCircleColorValue(style) {
3740
+ return withSimpleStyleColor(style, "marker-color", vectorColorExpression(style, styleValue(style, "fillColor")));
3741
+ }
3742
+ /**
3743
+ * Line color value for line geometry and polygon outlines (fallback: the
3744
+ * layer stroke color). For non-`expression` modes the data-driven color is
3745
+ * applied to line geometry only, while polygon outlines keep the flat stroke
3746
+ * color, matching the polygon-fill-only behavior of categorized/graduated
3747
+ * styling.
3748
+ */
3749
+ function vectorLineColorValue(style) {
3750
+ const strokeColor = styleValue(style, "strokeColor");
3751
+ if (styleValue(style, "vectorStyleMode") === "rule-based") return withSimpleStyleColor(style, "stroke", ruleBasedLineColorExpression(style));
3752
+ const vectorColor = vectorColorExpression(style, strokeColor);
3753
+ return withSimpleStyleColor(style, "stroke", vectorColor === strokeColor ? strokeColor : styleValue(style, "vectorStyleMode") === "expression" ? vectorColor : [
3754
+ "case",
3755
+ [
3756
+ "==",
3757
+ ["geometry-type"],
3758
+ "Polygon"
3759
+ ],
3760
+ strokeColor,
3761
+ vectorColor
3762
+ ]);
3763
+ }
3764
+ /**
3765
+ * The line color for the rule-based renderer: line geometry takes the rule
3766
+ * color, polygon outlines take the per-rule stroke color override (falling
3767
+ * back to the flat layer stroke). Both channels are built inside one shared
3768
+ * zoom wrapper, since composing two independently zoom-stepped expressions in
3769
+ * a single `case` would nest `["zoom"]` below the top level, which MapLibre
3770
+ * rejects.
3771
+ */
3772
+ function ruleBasedLineColorExpression(style) {
3773
+ const strokeColor = styleValue(style, "strokeColor");
3774
+ const { rules, elseRule } = effectiveVectorRules(style);
3775
+ const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : strokeColor;
3776
+ const elseOutline = readRuleOverride(elseRule ?? void 0, "strokeColor") ?? strokeColor;
3777
+ const hasOutlineOverride = rules.some((rule) => rule.strokeColor !== void 0) || elseOutline !== strokeColor;
3778
+ if (rules.length === 0 && elseColor === strokeColor && !hasOutlineOverride) return strokeColor;
3779
+ return zoomWrappedRuleValue(rules, (active) => {
3780
+ const colorValue = active.length === 0 ? elseColor : [
3781
+ "case",
3782
+ ...active.flatMap((rule) => [rule.filter, rule.color]),
3783
+ elseColor
3784
+ ];
3785
+ const outlineValue = !hasOutlineOverride ? strokeColor : active.length === 0 ? elseOutline : [
3786
+ "case",
3787
+ ...active.flatMap((rule) => [rule.filter, rule.strokeColor ?? strokeColor]),
3788
+ elseOutline
3789
+ ];
3790
+ if (colorValue === strokeColor && outlineValue === strokeColor) return strokeColor;
3791
+ return [
3792
+ "case",
3793
+ [
3794
+ "==",
3795
+ ["geometry-type"],
3796
+ "Polygon"
3797
+ ],
3798
+ outlineValue,
3799
+ colorValue
3800
+ ];
3801
+ });
3802
+ }
3803
+ /**
3804
+ * Resolves the 3D-extrusion height (the roof's altitude) for a layer style
3805
+ * into a MapLibre value: a plain meters number, or a data-driven expression.
3806
+ * With a positive `extrusionFloorHeight` the roof sits that far above the
3807
+ * base (`extrusionBaseValue`, clamped to the ground as MapLibre clamps
3808
+ * `fill-extrusion-base`), whatever the height settings say. Otherwise, in
3809
+ * advanced mode a valid `extrusionHeightExpression` wins; else the height is
3810
+ * the chosen property scaled by `extrusionHeightScale` (`["*", ["to-number",
3811
+ * ["get", prop], 0], scale]`), or a flat `0` when no property is set (so the
3812
+ * layer renders flat rather than erroring). Shared by the map's
3813
+ * fill-extrusion paint and the Add Vector Layer control's extrusion mapping
3814
+ * so both extrude identically.
3815
+ *
3816
+ * @param style - The layer style.
3817
+ * @returns The extrusion height as a number or a MapLibre expression array.
3818
+ */
3819
+ function extrusionHeightValue(style) {
3820
+ const floorHeight = extrusionFloorHeightValue(style);
3821
+ if (floorHeight > 0) {
3822
+ const base = extrusionBaseValue(style);
3823
+ return typeof base === "number" ? Math.max(0, base) + floorHeight : [
3824
+ "+",
3825
+ [
3826
+ "max",
3827
+ 0,
3828
+ base
3829
+ ],
3830
+ floorHeight
3831
+ ];
3832
+ }
3833
+ const advancedExpression = parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionHeightExpression") : "");
3834
+ if (advancedExpression) return advancedExpression;
3835
+ const property = styleValue(style, "extrusionHeightProperty").trim();
3836
+ if (!property) return 0;
3837
+ const scale = styleValue(style, "extrusionHeightScale");
3838
+ return [
3839
+ "*",
3840
+ [
3841
+ "to-number",
3842
+ ["get", property],
3843
+ 0
3844
+ ],
3845
+ scale
3846
+ ];
3847
+ }
3848
+ /**
3849
+ * The layer's floor height in metres: a finite `extrusionFloorHeight` above
3850
+ * 0, else 0 (the height is then the roof's altitude). Also 0 for a project
3851
+ * saved before the field existed.
3852
+ *
3853
+ * @param style - The layer style.
3854
+ * @returns The floor height, or 0 when it is off.
3855
+ */
3856
+ function extrusionFloorHeightValue(style) {
3857
+ const value = styleValue(style, "extrusionFloorHeight");
3858
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
3859
+ }
3860
+ /**
3861
+ * Resolves the 3D-extrusion base for a layer style into a MapLibre value: a
3862
+ * plain meters number, or a data-driven expression. In advanced mode a valid
3863
+ * `extrusionBaseExpression` wins; otherwise, with `extrusionBaseProperty` set,
3864
+ * the base is `extrusionBase` plus the property scaled by `extrusionBaseScale`
3865
+ * (so floors stack from a `level` field), else the flat `extrusionBase`. Like
3866
+ * {@link extrusionHeightValue} it is an altitude: MapLibre draws a feature
3867
+ * only where its height is above its base.
3868
+ *
3869
+ * @param style - The layer style.
3870
+ * @returns The extrusion base as a number or a MapLibre expression array.
3871
+ */
3872
+ function extrusionBaseValue(style) {
3873
+ const advancedExpression = parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionBaseExpression") : "");
3874
+ if (advancedExpression) return advancedExpression;
3875
+ const rawOffset = styleValue(style, "extrusionBase");
3876
+ const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
3877
+ const property = styleValue(style, "extrusionBaseProperty").trim();
3878
+ if (!property) return offset;
3879
+ const scale = styleValue(style, "extrusionBaseScale");
3880
+ const scaled = [
3881
+ "*",
3882
+ [
3883
+ "to-number",
3884
+ ["get", property],
3885
+ 0
3886
+ ],
3887
+ Number.isFinite(scale) ? scale : 1
3888
+ ];
3889
+ return offset === 0 ? scaled : [
3890
+ "+",
3891
+ scaled,
3892
+ offset
3893
+ ];
3894
+ }
3895
+ /**
3896
+ * Resolves the 3D-extrusion color for a layer style: a data-driven expression
3897
+ * when the layer's symbology mode produces one (categorized/graduated/rule/
3898
+ * expression) or an advanced `extrusionColorExpression` is set, otherwise the
3899
+ * flat `extrusionColor`. Mirrors the fill-color contract so an extruded layer
3900
+ * honors the same attribute-driven styling.
3901
+ *
3902
+ * @param style - The layer style.
3903
+ * @returns A flat color string or a MapLibre color expression array.
3904
+ */
3905
+ function extrusionColorValue(style) {
3906
+ const flat = styleValue(style, "extrusionColor");
3907
+ const vectorExpression = vectorColorExpression(style, flat);
3908
+ if (vectorExpression !== flat) return vectorExpression;
3909
+ return parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionColorExpression") : "") ?? flat;
3910
+ }
2832
3911
  // src/layer-descriptive-metadata.ts
2833
3912
  /** STAC specification version the Item export targets. */
2834
3913
  const STAC_VERSION = "1.0.0";
@@ -4651,6 +5730,8 @@ const DEFAULT_PRINT_LAYOUT = {
4651
5730
  customHeight: 720,
4652
5731
  customUnit: "px",
4653
5732
  pageMargin: "normal",
5733
+ pageBackground: "#ffffff",
5734
+ panelTheme: "light",
4654
5735
  showPageBorder: false,
4655
5736
  pageBorderColor: "#111827",
4656
5737
  pageBorderWidth: 2,
@@ -4740,6 +5821,10 @@ function createDefaultPrintLayout() {
4740
5821
  function str(value, fallback) {
4741
5822
  return typeof value === "string" ? value : fallback;
4742
5823
  }
5824
+ /** A `#RGB` / `#RRGGBB` colour, or `fallback` for anything else. */
5825
+ function hexColor(value, fallback) {
5826
+ return typeof value === "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim()) ? value.trim() : fallback;
5827
+ }
4743
5828
  function bool(value, fallback) {
4744
5829
  return typeof value === "boolean" ? value : fallback;
4745
5830
  }
@@ -4841,6 +5926,8 @@ function normalizePrintLayoutConfig(value) {
4841
5926
  "narrow",
4842
5927
  "none"
4843
5928
  ], d.pageMargin),
5929
+ pageBackground: hexColor(raw.pageBackground, d.pageBackground),
5930
+ panelTheme: oneOf(raw.panelTheme, ["light", "dark"], d.panelTheme),
4844
5931
  showPageBorder: bool(raw.showPageBorder, d.showPageBorder),
4845
5932
  pageBorderColor: str(raw.pageBorderColor, d.pageBorderColor),
4846
5933
  pageBorderWidth: num(raw.pageBorderWidth, d.pageBorderWidth, 1, 20),
@@ -5029,6 +6116,9 @@ const SYMBOL_STYLE_KEYS = [
5029
6116
  "strokeColor",
5030
6117
  "strokeWidth",
5031
6118
  "strokeWidthUnit",
6119
+ "strokeOpacity",
6120
+ "strokeDash",
6121
+ "strokeBlur",
5032
6122
  "fillOpacity",
5033
6123
  "circleRadius",
5034
6124
  "fillPattern",
@@ -5228,7 +6318,9 @@ function sanitizeLayerStylePatch(value) {
5228
6318
  ...circleRadius !== void 0 ? { circleRadius } : {}
5229
6319
  }];
5230
6320
  });
5231
- else out[key] = given;
6321
+ else if (key === "strokeDash") {
6322
+ if (given.length === 0 || isStrokeDashPattern(given)) out[key] = given;
6323
+ } else out[key] = given;
5232
6324
  continue;
5233
6325
  }
5234
6326
  if (typeof given !== typeof fallback) continue;
@@ -9791,6 +10883,7 @@ const LAYER_GROUPED_KEYS = /* @__PURE__ */ new Set([
9791
10883
  "geojson",
9792
10884
  "filterExpression",
9793
10885
  "quickFilters",
10886
+ "levelFilter",
9794
10887
  "source",
9795
10888
  "sourcePath",
9796
10889
  "beforeId"
@@ -10141,10 +11234,12 @@ function diffLayer(beforeProject, afterProject, a, b, maxLength) {
10141
11234
  }
10142
11235
  diffRecords({
10143
11236
  filterExpression: a.filterExpression,
10144
- quickFilters: a.quickFilters
11237
+ quickFilters: a.quickFilters,
11238
+ levelFilter: a.levelFilter
10145
11239
  }, {
10146
11240
  filterExpression: b.filterExpression,
10147
- quickFilters: b.quickFilters
11241
+ quickFilters: b.quickFilters,
11242
+ levelFilter: b.levelFilter
10148
11243
  }, {
10149
11244
  depth: 0,
10150
11245
  maxLength
@@ -10408,6 +11503,6 @@ function metadataRank(path) {
10408
11503
  if (path === "description") return 1;
10409
11504
  return 2;
10410
11505
  }
10411
- export { GEOCODING_PROVIDERS as $, getPlanetaryBasemapById as $i, groupMemberIndices as $n, getBuildEnvironment as $r, CESIUM_BING_AERIAL_ASSET_ID as $t, normalizeSecondaryMapViews as A, styleValue as Aa, sameCesiumImagery as Ai, createStyleLibraryEntryId as An, isValidMetadataDate as Ar, selectedGeocodingLocator as At, storyMapHasContent as B, ELLIPSOIDS as Bi, normalizePrintLayoutConfig as Bn, evaluateMapExpression as Br, PROJECT_CREDENTIAL_FIELDS as Bt, normalizeModelGraph as C, OPENFREEMAP_BASEMAPS as Ca, parseMapCrsEpsgCode as Ci, isGeoArrowBatch as Cn, reapplyLayerJoins as Cr, normalizeArcGISLocatorUrl as Ct, normalizeProcessingHistory as D, detectNonGeographicCoordinates as Da, availableCesiumBasemap as Di, STYLE_LIBRARY_BUNDLE_TYPE as Dn, STAC_PROCESSING_EXTENSION as Dr, parseArcGISSuggestions as Dt, normalizePrimaryRenderer as E, SQL_QUERY_SOURCE_KIND as Ea, CESIUM_BASEMAPS as Ei, RAMP_STYLE_KEYS as En, stripVirtualFieldColumns as Er, parseArcGISLocatorInfo as Et, projectFromStore as F, getRegionalBasemapById as Fi, sanitizeLayerStylePatch as Fn, parseMetadataKeywords as Fr, DEFAULT_STORY_CHAPTER_DURATION_SECONDS as Ft, DEFAULT_REVERSE_GEOCODE_ENDPOINT as G, bodyLengthToEarth as Gi, scrubCommentsForRemovedLayers as Gn, removeTrailingJsonCommas as Gr, redactCredentials as Gt, CARTOCIUDAD_PUBLIC_HOST as H, PLANETARY_BASEMAP_GROUPS as Hi, scrubPrintLayoutForLayers as Hn, inferFieldTypes as Hr, isCredentialFieldName as Ht, resolveMapGrid as I, getRegionalBasemapByStyleUrl as Ii, serializeStyleLibrary as In, stacAssetMediaType as Ir, MAX_STORY_CHAPTER_DURATION_SECONDS as It, GEOCODE_LAT_KEY as J, getActiveBodyRadiusRatio as Ji, DEFAULT_LAYER_GROUP_OPACITY as Jn, SELECTION_MODES as Jr, setRegistryPublishableSettings as Jt, GEOCODE_ATTRIBUTE_PREFIX as K, earthAreaToBody as Ki, scrubLegendForRemovedLayers as Kn, substituteExpressionVariables as Kr, redactProjectCredentials as Kt, serializeProject as L, isRegionalBasemapSentinel as Li, DEFAULT_PRINT_LAYOUT as Ln, validateLayerDescriptiveMetadata as Lr, MIN_STORY_CHAPTER_DURATION_SECONDS as Lt, normalizeWidgets as M, REGIONAL_BASEMAPS as Mi, isStyleLibraryTargetLayer as Mn, isValidMetadataUrl as Mr, setGeocodingTokenProvider as Mt, parseProject as N, REGIONAL_BASEMAP_GROUPS as Ni, normalizeStyleLibraryEntries as Nn, metadataDateToRfc3339 as Nr, shouldThrottle as Nt, normalizeProjectComments as O, isDuckDBQueryLayer as Oa, basemapToCesiumImagery as Oi, STYLE_LIBRARY_BUNDLE_VERSION as On, STAC_VERSION as Or, resolveGeocoderConfig as Ot, portableWmsTileUrl as P, REGIONAL_BASEMAP_SENTINEL_PREFIX as Pi, parseStyleLibrary as Pn, normalizeLayerDescriptiveMetadata as Pr, unmatchedGeocodeFeature as Pt, GEOCODE_STATUS_KEY as Q, getEllipsoid as Qi, effectiveLayerRenderState as Qn, getArcgisApiKey as Qr, resolveHeaderReferences as Qt, serializeProjectWithLayerCache as R, DEFAULT_ELLIPSOID_ID as Ri, createDefaultPrintLayout as Rn, EXPRESSION_FUNCTION_CATEGORIES as Rr, normalizeStoryChapterDurationSeconds as Rt, normalizeMapViewState as S, NETCDF_IMAGE_SOURCE_KIND as Sa, normalizeMapCrs as Si, geoArrowToFeatureCollection as Sn, layerJoinKey as Sr, nominatimReverseResultToDisplay as St, normalizeObiaWorkbench as T, PROTOMAPS_BASEMAPS as Ta, normalizeArcgisWkid as Ti, BUILT_IN_STYLE_PRESETS as Tn, applyLayerVirtualFields as Tr, normalizeGeocodingProviderId as Tt, DEFAULT_FORWARD_GEOCODE_ENDPOINT as U, PLANETARY_BASEMAP_SENTINEL_PREFIX as Ui, scrubPrintLayoutForRemovedLayers as Un, isStyleSpecColor as Ur, isCredentialUrlParam as Ut, CARTOCIUDAD_MIN_INTERVAL_MS as V, PLANETARY_BASEMAPS as Vi, printLayoutConfigsEqual as Vn, formatExpressionPreviewValue as Vr, PUBLISHABLE_PLUGIN_SETTINGS as Vt, DEFAULT_GEOCODING_PROVIDER_ID as W, PLANET_SWITCHER_OPTIONS as Wi, removedLayerIdSet as Wn, matchFeaturesByExpression as Wr, redactConfigurationCredentials as Wt, GEOCODE_PROVIDER_KEY as X, getActiveMeanRadiusMeters as Xi, buildLayerPanelUnits as Xn, featureSelectionId as Xr, hasHeaderReferences as Xt, GEOCODE_LON_KEY as Y, getActiveEllipsoid as Yi, applyGroupEffects as Yn, applySelectionMode as Yr, allowsCredentialHeaders as Yt, GEOCODE_SCORE_KEY as Z, getActiveSemiMajorAxisMeters as Zi, buildLayerTree as Zn, invertSelection as Zr, isHeaderReferenceOnly as Zt, normalizeBookmarkGroups as _, LAYER_TYPES as _a, MAP_CRS_CHANGE_EVENT as _i, czmlSource as _n, resolveRolePrivileges as _r, geocoderSupportsSuggest as _t, DEFAULT_PROJECT_NAME as a, DEFAULT_BASEMAP as aa, getRuntimeEnvironment as ai, cesiumIonAssetKind as an, reorderLayerGroupInPanel as ar, arcgisLocatorInputField as at, normalizeLegendConfig as b, MAX_PROCESSING_HISTORY as ba, deriveTileMatrixSet as bi, computeGeoArrowBBox as bn, cascadeLayerJoinRefresh as br, nextDelayMs as bt, createDefaultMapView as c, DEFAULT_LAYER_STYLE as ca, decodePolyline as ci, isCesiumOnlyLayer as cn, ROLE_PRIVILEGES as cr, csvRowsToGeocodeRequests as ct, createProjectTemplate as d, DEFAULT_PRIMARY_RENDERER as da, geoJSONToPolylineStr as di, cesiumKmlSource as dn, hasAppPrivilege as dr, geocodeMatchToFeature as dt, getPlanetaryBasemapByStyleUrl as ea, getCesiumIonToken as ei, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID as en, layerGroupDepth as er, GeocoderServiceError as et, detachProjectCopy as f, DEFAULT_PROJECT_PREFERENCES as fa, polylineStrToGeoJSON as fi, createCesiumKmlLayer as fn, inferLayerCapabilities as fr, geocodeReverse as ft, normalizeBlankBackgroundColor as g, LARGE_VECTOR_FEATURE_THRESHOLD as ga, transformGeojsonElevation as gi, createCzmlLayer as gn, resolveLayerCapabilities as gr, geocoderSupportsReverse as gt, isSessionOnlyLayer as h, DUCKDB_VECTOR_ROUTE_BYTES as ha, horizontalBbox as hi, CZML_SOURCE_KIND as hn, normalizeLayerCapabilities as hr, geocoderNeedsApiKey as ht, previewValue as i, BLEND_MODES as ia, getProtomapsStyleUrl as ii, cesiumIonAssetId as in, normalizeGroupContiguity as ir, PUBLIC_GEOCODE_ROW_CAP as it, normalizeStoryMap as j, CHINA_BASEMAPS as ji, extractStyleLibraryStyle as jn, isValidMetadataEmail as jr, setGeocodingFetch as jt, normalizeProjectInteraction as k, shouldUseTiledRendering as ka, normalizeCesiumBasemap as ki, SYMBOL_STYLE_KEYS as kn, buildLayerStacItem as kr, rowCap as kt, createEmptyProject as l, DEFAULT_LEGEND_CONFIG as la, decodePolylineDetailed as li, parseCesiumIonAssetId as ln, appPrivilegeReason as lr, fetchArcGISLocatorInfo as lt, isAllowedPluginManifestUrl as m, DUCKDB_VECTOR_FEATURE_WARN_COUNT as ma, geojsonHasZCoordinates as mi, CZML_QUICK_PICKS as mn, normalizeAppPrivileges as mr, geocoderMinIntervalMs as mt, diffProjects as n, setActiveEllipsoidId as na, getMapboxAccessToken as ni, CESIUM_ION_SOURCE_KIND as nn, layerGroupSortability as nr, NOMINATIM_MIN_INTERVAL_MS as nt, MAX_PROJECT_BOOKMARKS as o, DEFAULT_BLEND_MODE as oa, getSpatialExtensionPath as oi, createCesiumIonLayer as on, sortLayerGroupInPanel as or, buildForwardGeocodeUrl as ot, hydrateProjectLayer as p, DEFAULT_STORY_MAP as pa, unescapePolyline as pi, isCesiumKmlLayer as pn, intersectPrivileges as pr, geocodeSuggest as pt, GEOCODE_DISPLAY_NAME_KEY as q, earthLengthToBody as qi, scrubWidgetsForRemovedLayers as qn, validateMapExpression as qr, redactUrlCredentials as qt, jsonValuesEqual as r, BLANK_BASEMAP as ra, getProtomapsApiKey as ri, CESIUM_OSM_BUILDINGS_ASSET_ID as rn, layerPanelGroupHeaders as rr, NOMINATIM_PUBLIC_HOST as rt, applyProjectToStore as s, DEFAULT_DASHBOARD_COLUMNS as sa, batchDecodePolylines as si, isCesiumIonLayer as sn, ALL_APP_PRIVILEGES as sr, buildReverseGeocodeUrl as st, diffFeatureCollections as t, meanRadiusMeters as ta, getGoogleMapsApiKey as ti, CESIUM_ION_QUICK_PICKS as tn, layerGroupMoveability as tr, GeocoderUnsupportedError as tt, createProjectLayerSerializationCache as u, DEFAULT_MAP_GRID_LAYOUT as ua, encodePolyline as ui, CESIUM_KML_SOURCE_KIND as un, createDefaultAppCapabilities as ur, geocodeForward as ut, normalizeBookmarks as v, MAX_DASHBOARD_COLUMNS as va, MAP_CRS_PRESETS as vi, isCzmlLayer as vn, applyJoinsToLayer as vr, getGeocoderConfig as vt, normalizeModels as w, PROJECT_VERSION as wa, ARCGIS_PROJECTION_PRESETS as wi, withRasterSourceDefaults as wn, stripJoinFields as wr, normalizeGeocodingLocator as wt, normalizeMapLayout as x, MIN_DASHBOARD_COLUMNS as xa, getMapCrsPreset as xi, featureCollectionToGeoArrow as xn, collectTransitiveJoinSourceIds as xr, nominatimResultToFeature as xt, normalizeDashboardColumns as y, MAX_MAP_GRID_DIM as ya, adaptBboxTemplateToCrs as yi, parseCzml as yn, applyLayerJoins as yr, getGeocodingProvider as yt, serializeProjectWithLayerCacheAsync as z, EARTH_MEAN_RADIUS_METERS as zi, isDefaultPrintLayout as zn, compileFeatureExpression as zr, MAX_REDACT_DEPTH as zt };
11506
+ export { GEOCODING_PROVIDERS as $, MAX_PROCESSING_HISTORY as $a, createEqualIntervalBreaks as $i, groupMemberIndices as $n, mapZoomStepOutputs as $r, CESIUM_BING_AERIAL_ASSET_ID as $t, normalizeSecondaryMapViews as A, REGIONAL_BASEMAP_GROUPS as Aa, substituteExpressionVariables as Ai, createStyleLibraryEntryId as An, isValidMetadataDate as Ar, selectedGeocodingLocator as At, storyMapHasContent as B, DEFAULT_DASHBOARD_COLUMNS as Ba, getMapboxAccessToken as Bi, normalizePrintLayoutConfig as Bn, effectiveVectorRules as Br, PROJECT_CREDENTIAL_FIELDS as Bt, normalizeModelGraph as C, CESIUM_BASEMAPS as Ca, compileFeatureExpression as Ci, isGeoArrowBatch as Cn, reapplyLayerJoins as Cr, normalizeArcGISLocatorUrl as Ct, normalizeProcessingHistory as D, sameCesiumImagery as Da, isStyleSpecColor as Di, STYLE_LIBRARY_BUNDLE_TYPE as Dn, STAC_PROCESSING_EXTENSION as Dr, parseArcGISSuggestions as Dt, normalizePrimaryRenderer as E, normalizeCesiumBasemap as Ea, inferFieldTypes as Ei, RAMP_STYLE_KEYS as En, stripVirtualFieldColumns as Er, parseArcGISLocatorInfo as Et, projectFromStore as F, isRegionalBasemapSentinel as Fa, invertSelection as Fi, sanitizeLayerStylePatch as Fn, parseMetadataKeywords as Fr, DEFAULT_STORY_CHAPTER_DURATION_SECONDS as Ft, DEFAULT_REVERSE_GEOCODE_ENDPOINT as G, DEFAULT_PROJECT_PREFERENCES as Ga, batchDecodePolylines as Gi, scrubCommentsForRemovedLayers as Gn, generatorCircleRadiusValue as Gr, redactCredentials as Gt, CARTOCIUDAD_PUBLIC_HOST as H, DEFAULT_LEGEND_CONFIG as Ha, getProtomapsStyleUrl as Hi, scrubPrintLayoutForLayers as Hn, extrusionColorValue as Hr, isCredentialFieldName as Ht, resolveMapGrid as I, BLANK_BASEMAP as Ia, getArcgisApiKey as Ii, serializeStyleLibrary as In, stacAssetMediaType as Ir, MAX_STORY_CHAPTER_DURATION_SECONDS as It, GEOCODE_LAT_KEY as J, DUCKDB_VECTOR_ROUTE_BYTES as Ja, encodePolyline as Ji, DEFAULT_LAYER_GROUP_OPACITY as Jn, heatmapRampColors as Jr, setRegistryPublishableSettings as Jt, GEOCODE_ATTRIBUTE_PREFIX as K, DEFAULT_STORY_MAP as Ka, decodePolyline as Ki, scrubLegendForRemovedLayers as Kn, generatorSizeRange as Kr, redactProjectCredentials as Kt, serializeProject as L, BLEND_MODES as La, getBuildEnvironment as Li, DEFAULT_PRINT_LAYOUT as Ln, validateLayerDescriptiveMetadata as Lr, MIN_STORY_CHAPTER_DURATION_SECONDS as Lt, normalizeWidgets as M, gateRegionalBasemap as Ma, SELECTION_MODES as Mi, isStyleLibraryTargetLayer as Mn, isValidMetadataUrl as Mr, setGeocodingTokenProvider as Mt, parseProject as N, getRegionalBasemapById as Na, applySelectionMode as Ni, normalizeStyleLibraryEntries as Nn, metadataDateToRfc3339 as Nr, shouldThrottle as Nt, normalizeProjectComments as O, CHINA_BASEMAPS as Oa, matchFeaturesByExpression as Oi, STYLE_LIBRARY_BUNDLE_VERSION as On, STAC_VERSION as Or, resolveGeocoderConfig as Ot, portableWmsTileUrl as P, getRegionalBasemapByStyleUrl as Pa, featureSelectionId as Pi, parseStyleLibrary as Pn, normalizeLayerDescriptiveMetadata as Pr, unmatchedGeocodeFeature as Pt, GEOCODE_STATUS_KEY as Q, MAX_MAP_GRID_DIM as Qa, VECTOR_COLOR_RAMPS as Qi, effectiveLayerRenderState as Qn, lineWidthValue as Qr, resolveHeaderReferences as Qt, serializeProjectWithLayerCache as R, DEFAULT_BASEMAP as Ra, getCesiumIonToken as Ri, createDefaultPrintLayout as Rn, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 as Rr, normalizeStoryChapterDurationSeconds as Rt, normalizeMapViewState as S, normalizeArcgisWkid as Sa, EXPRESSION_FUNCTION_CATEGORIES as Si, geoArrowToFeatureCollection as Sn, layerJoinKey as Sr, nominatimReverseResultToDisplay as St, normalizeObiaWorkbench as T, basemapToCesiumImagery as Ta, formatExpressionPreviewValue as Ti, BUILT_IN_STYLE_PRESETS as Tn, applyLayerVirtualFields as Tr, normalizeGeocodingProviderId as Tt, DEFAULT_FORWARD_GEOCODE_ENDPOINT as U, DEFAULT_MAP_GRID_LAYOUT as Ua, getRuntimeEnvironment as Ui, scrubPrintLayoutForRemovedLayers as Un, extrusionFloorHeightValue as Ur, isCredentialUrlParam as Ut, CARTOCIUDAD_MIN_INTERVAL_MS as V, DEFAULT_LAYER_STYLE as Va, getProtomapsApiKey as Vi, printLayoutConfigsEqual as Vn, extrusionBaseValue as Vr, PUBLISHABLE_PLUGIN_SETTINGS as Vt, DEFAULT_GEOCODING_PROVIDER_ID as W, DEFAULT_PRIMARY_RENDERER as Wa, getSpatialExtensionPath as Wi, removedLayerIdSet as Wn, extrusionHeightValue as Wr, redactConfigurationCredentials as Wt, GEOCODE_PROVIDER_KEY as X, LAYER_TYPES as Xa, polylineStrToGeoJSON as Xi, buildLayerPanelUnits as Xn, isStrokeDashPattern as Xr, hasHeaderReferences as Xt, GEOCODE_LON_KEY as Y, LARGE_VECTOR_FEATURE_THRESHOLD as Ya, geoJSONToPolylineStr as Yi, applyGroupEffects as Yn, isHexColor as Yr, allowsCredentialHeaders as Yt, GEOCODE_SCORE_KEY as Z, MAX_DASHBOARD_COLUMNS as Za, unescapePolyline as Zi, buildLayerTree as Zn, isVectorColorExpression as Zr, isHeaderReferenceOnly as Zt, normalizeBookmarkGroups as _, getMapCrsPreset as _a, vectorFillColorValue as _i, czmlSource as _n, resolveRolePrivileges as _r, geocoderSupportsSuggest as _t, DEFAULT_PROJECT_NAME as a, interpolateRampColors as aa, readSimpleStyleNumberLookup as ai, cesiumIonAssetKind as an, SQL_QUERY_SOURCE_KIND as ao, reorderLayerGroupInPanel as ar, arcgisLocatorInputField as at, normalizeLegendConfig as b, parseMapCrsEpsgCode as ba, vectorOutlineColorValue as bi, computeGeoArrowBBox as bn, cascadeLayerJoinRefresh as br, nextDelayMs as bt, createDefaultMapView as c, parseHexColorList as ca, ruleBasedVisibilityFilter as ci, isCesiumOnlyLayer as cn, isDuckDBQueryLayer as co, ROLE_PRIVILEGES as cr, csvRowsToGeocodeRequests as ct, createProjectTemplate as d, horizontalBbox as da, strokeDashPresetName as di, cesiumKmlSource as dn, hasAppPrivilege as dr, geocodeMatchToFeature as dt, createGraduatedClassBreaks as ea, mercatorMetersPerPixelAtZoom0 as ei, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID as en, MIN_DASHBOARD_COLUMNS as eo, layerGroupDepth as er, GeocoderServiceError as et, detachProjectCopy as f, transformGeojsonElevation as fa, strokeDashValue as fi, createCesiumKmlLayer as fn, inferLayerCapabilities as fr, geocodeReverse as ft, normalizeBlankBackgroundColor as g, deriveTileMatrixSet as ga, vectorColorExpression as gi, createCzmlLayer as gn, resolveLayerCapabilities as gr, geocoderSupportsReverse as gt, isSessionOnlyLayer as h, adaptBboxTemplateToCrs as ha, vectorCircleColorValue as hi, CZML_SOURCE_KIND as hn, normalizeLayerCapabilities as hr, geocoderNeedsApiKey as ht, previewValue as i, interpolateHexColor as ia, proportionalSizeRange as ii, cesiumIonAssetId as in, PROTOMAPS_BASEMAPS as io, normalizeGroupContiguity as ir, PUBLIC_GEOCODE_ROW_CAP as it, normalizeStoryMap as j, REGIONAL_BASEMAP_SENTINEL_PREFIX as ja, validateMapExpression as ji, extractStyleLibraryStyle as jn, isValidMetadataEmail as jr, setGeocodingFetch as jt, normalizeProjectInteraction as k, REGIONAL_BASEMAPS as ka, removeTrailingJsonCommas as ki, SYMBOL_STYLE_KEYS as kn, buildLayerStacItem as kr, rowCap as kt, createEmptyProject as l, rgbToHex as la, simpleStyleNumberValue as li, parseCesiumIonAssetId as ln, shouldUseTiledRendering as lo, appPrivilegeReason as lr, fetchArcGISLocatorInfo as lt, isAllowedPluginManifestUrl as m, MAP_CRS_PRESETS as ma, strokeOpacityValue as mi, CZML_QUICK_PICKS as mn, normalizeAppPrivileges as mr, geocoderMinIntervalMs as mt, diffProjects as n, getVectorColorRamp as na, parseJsonExpression as ni, CESIUM_ION_SOURCE_KIND as nn, OPENFREEMAP_BASEMAPS as no, layerGroupSortability as nr, NOMINATIM_MIN_INTERVAL_MS as nt, MAX_PROJECT_BOOKMARKS as o, normalizeHexColor as oa, readsSimpleStyleProperties as oi, createCesiumIonLayer as on, STROKE_DASH_PRESETS as oo, sortLayerGroupInPanel as or, buildForwardGeocodeUrl as ot, hydrateProjectLayer as p, MAP_CRS_CHANGE_EVENT as pa, strokeOpacityPaintValue as pi, isCesiumKmlLayer as pn, intersectPrivileges as pr, geocodeSuggest as pt, GEOCODE_DISPLAY_NAME_KEY as q, DUCKDB_VECTOR_FEATURE_WARN_COUNT as qa, decodePolylineDetailed as qi, scrubWidgetsForRemovedLayers as qn, hasSimpleStyleProperties as qr, redactUrlCredentials as qt, jsonValuesEqual as r, interpolateColors as ra, proportionalRadiusExpression as ri, CESIUM_OSM_BUILDINGS_ASSET_ID as rn, PROJECT_VERSION as ro, layerPanelGroupHeaders as rr, NOMINATIM_PUBLIC_HOST as rt, applyProjectToStore as s, parseHexColor as sa, ruleBasedColorExpression as si, isCesiumIonLayer as sn, detectNonGeographicCoordinates as so, ALL_APP_PRIVILEGES as sr, buildReverseGeocodeUrl as st, diffFeatureCollections as t, createQuantileBreaks as ta, metersWidthExpression as ti, CESIUM_ION_QUICK_PICKS as tn, NETCDF_IMAGE_SOURCE_KIND as to, layerGroupMoveability as tr, GeocoderUnsupportedError as tt, createProjectLayerSerializationCache as u, geojsonHasZCoordinates as ua, strokeBlurValue as ui, CESIUM_KML_SOURCE_KIND as un, styleValue as uo, createDefaultAppCapabilities as ur, geocodeForward as ut, normalizeBookmarks as v, mapCrsPreferencePatch as va, vectorFillOpacityValue as vi, isCzmlLayer as vn, applyJoinsToLayer as vr, getGeocoderConfig as vt, normalizeModels as w, availableCesiumBasemap as wa, evaluateMapExpression as wi, withRasterSourceDefaults as wn, stripJoinFields as wr, normalizeGeocodingLocator as wt, normalizeMapLayout as x, ARCGIS_PROJECTION_PRESETS as xa, vectorStrokeWidthValue as xi, featureCollectionToGeoArrow as xn, collectTransitiveJoinSourceIds as xr, nominatimResultToFeature as xt, normalizeDashboardColumns as y, normalizeMapCrs as ya, vectorLineColorValue as yi, parseCzml as yn, applyLayerJoins as yr, getGeocodingProvider as yt, serializeProjectWithLayerCacheAsync as z, DEFAULT_BLEND_MODE as za, getGoogleMapsApiKey as zi, isDefaultPrintLayout as zn, circleRadiusValue as zr, MAX_REDACT_DEPTH as zt };
10412
11507
 
10413
- //# sourceMappingURL=project-diff-CSG1AgSK.mjs.map
11508
+ //# sourceMappingURL=project-diff-DPGO-zCS.mjs.map