@geolibre/core 3.6.1 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ellipsoids-BdHOMRmm.d.mts +448 -0
- package/dist/ellipsoids-BdHOMRmm.d.mts.map +1 -0
- package/dist/ellipsoids-DlTLddDu.mjs +496 -0
- package/dist/ellipsoids-DlTLddDu.mjs.map +1 -0
- package/dist/geodesic.d.mts +70 -0
- package/dist/geodesic.d.mts.map +1 -0
- package/dist/geodesic.mjs +155 -0
- package/dist/geodesic.mjs.map +1 -0
- package/dist/index.d.mts +822 -18
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4976 -3886
- package/dist/index.mjs.map +1 -1
- package/dist/{project-diff-BJDslj2U.d.mts → project-diff-BksHN-qY.d.mts} +194 -451
- package/dist/project-diff-BksHN-qY.d.mts.map +1 -0
- package/dist/{project-diff-RBRpSza4.mjs → project-diff-DJkOd8ff.mjs} +1964 -687
- package/dist/project-diff-DJkOd8ff.mjs.map +1 -0
- package/dist/worker-safe.d.mts +2 -1
- package/dist/worker-safe.mjs +3 -1
- package/package.json +10 -5
- package/dist/project-diff-BJDslj2U.d.mts.map +0 -1
- package/dist/project-diff-RBRpSza4.mjs.map +0 -1
|
@@ -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,
|
|
@@ -304,6 +329,32 @@ const SQL_QUERY_SOURCE_KIND = "sql-query";
|
|
|
304
329
|
*/
|
|
305
330
|
const NETCDF_IMAGE_SOURCE_KIND = "netcdf-image";
|
|
306
331
|
/**
|
|
332
|
+
* Metadata `sourceKind` marking an ArcGIS ImageServer drawn as a `raster` layer.
|
|
333
|
+
*
|
|
334
|
+
* Defined here so the plugin that adds these layers and `@geolibre/map` (whose
|
|
335
|
+
* Identify reads their pixel values through the service's `/identify`) share
|
|
336
|
+
* one value.
|
|
337
|
+
*/
|
|
338
|
+
const ARCGIS_IMAGE_SERVICE_SOURCE_KIND = "arcgis-image-service";
|
|
339
|
+
/**
|
|
340
|
+
* Metadata `sourceKind` marking a Microsoft Planetary Computer raster layer
|
|
341
|
+
* (one STAC item, or a mosaic of many) drawn by the Planetary Computer panel.
|
|
342
|
+
*
|
|
343
|
+
* Defined here so the plugin that adds these layers, the Layer Library's
|
|
344
|
+
* restore dispatch, and `@geolibre/map` (whose Identify reads their pixel
|
|
345
|
+
* values through the application's raster bridge) share one value.
|
|
346
|
+
*/
|
|
347
|
+
const PLANETARY_COMPUTER_SOURCE_KIND = "planetary-computer-raster";
|
|
348
|
+
/**
|
|
349
|
+
* Whether a layer is a Planetary Computer raster the panel's control draws.
|
|
350
|
+
*
|
|
351
|
+
* @param layer - The layer to test.
|
|
352
|
+
* @returns True for a control-drawn Planetary Computer item or mosaic layer.
|
|
353
|
+
*/
|
|
354
|
+
function isPlanetaryComputerLayer(layer) {
|
|
355
|
+
return layer.type === "raster" && layer.metadata.sourceKind === "planetary-computer-raster" && layer.metadata.externalNativeLayer === true;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
307
358
|
* Detect a DuckDB query layer rendered through the plugin's external deck.gl
|
|
308
359
|
* overlay. Shared by `@geolibre/map`, `@geolibre/plugins`, and the desktop
|
|
309
360
|
* app so the detection criteria cannot drift.
|
|
@@ -454,499 +505,6 @@ function detectNonGeographicCoordinates(geojson, sampleLimit = 1e3) {
|
|
|
454
505
|
maxAbsY
|
|
455
506
|
} : null;
|
|
456
507
|
}
|
|
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
508
|
// src/regional-basemaps.ts
|
|
951
509
|
const REGIONAL_BASEMAP_SENTINEL_PREFIX = "geolibre://regional-basemap/";
|
|
952
510
|
const AMAP_ATTRIBUTION = "© <a href=\"https://www.amap.com\">高德地图 Amap</a>";
|
|
@@ -969,6 +527,7 @@ const CHINA_BASEMAPS = [
|
|
|
969
527
|
id: "amap-street",
|
|
970
528
|
region: "china",
|
|
971
529
|
name: "高德地图",
|
|
530
|
+
provider: "Amap",
|
|
972
531
|
styleUrl: sentinel("amap-street"),
|
|
973
532
|
tileUrl: AMAP_STREET_URL,
|
|
974
533
|
maxZoom: 19,
|
|
@@ -979,6 +538,7 @@ const CHINA_BASEMAPS = [
|
|
|
979
538
|
id: "amap-satellite",
|
|
980
539
|
region: "china",
|
|
981
540
|
name: "高德卫星",
|
|
541
|
+
provider: "Amap",
|
|
982
542
|
styleUrl: sentinel("amap-satellite"),
|
|
983
543
|
tileUrl: AMAP_SATELLITE_URL,
|
|
984
544
|
maxZoom: 18,
|
|
@@ -989,6 +549,7 @@ const CHINA_BASEMAPS = [
|
|
|
989
549
|
id: "amap-hybrid",
|
|
990
550
|
region: "china",
|
|
991
551
|
name: "高德混合",
|
|
552
|
+
provider: "Amap",
|
|
992
553
|
styleUrl: sentinel("amap-hybrid"),
|
|
993
554
|
tileUrl: AMAP_SATELLITE_URL,
|
|
994
555
|
overlayTileUrl: AMAP_LABELS_URL,
|
|
@@ -1000,6 +561,7 @@ const CHINA_BASEMAPS = [
|
|
|
1000
561
|
id: "tencent-street",
|
|
1001
562
|
region: "china",
|
|
1002
563
|
name: "腾讯地图",
|
|
564
|
+
provider: "Tencent Maps",
|
|
1003
565
|
styleUrl: sentinel("tencent-street"),
|
|
1004
566
|
tileUrl: tencentUrl(1),
|
|
1005
567
|
scheme: "tms",
|
|
@@ -1011,6 +573,7 @@ const CHINA_BASEMAPS = [
|
|
|
1011
573
|
id: "tencent-dark",
|
|
1012
574
|
region: "china",
|
|
1013
575
|
name: "腾讯深色",
|
|
576
|
+
provider: "Tencent Maps",
|
|
1014
577
|
styleUrl: sentinel("tencent-dark"),
|
|
1015
578
|
tileUrl: tencentUrl(4),
|
|
1016
579
|
scheme: "tms",
|
|
@@ -1045,6 +608,33 @@ function getRegionalBasemapById(id) {
|
|
|
1045
608
|
function isRegionalBasemapSentinel(styleUrl) {
|
|
1046
609
|
return Boolean(styleUrl?.startsWith(REGIONAL_BASEMAP_SENTINEL_PREFIX));
|
|
1047
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Decides which basemap a project load actually applies.
|
|
613
|
+
*
|
|
614
|
+
* Only a resolvable regional sentinel is gated. Every other style URL (and an
|
|
615
|
+
* unknown regional id, which the map controller already replaces with the
|
|
616
|
+
* default basemap) passes through untouched.
|
|
617
|
+
*
|
|
618
|
+
* @param styleUrl - The basemap style URL the project requests.
|
|
619
|
+
* @param policy - The store's current {@link RegionalBasemapPolicy}.
|
|
620
|
+
* @param fallbackStyleUrl - The basemap to apply instead (the default basemap).
|
|
621
|
+
* @param trusted - True when the user already chose this basemap in this
|
|
622
|
+
* session (a template, an undo step, a duplicate), so `"prompt"` does not
|
|
623
|
+
* apply. `"block"` still does.
|
|
624
|
+
* @returns The style URL to apply and, when it was replaced, why.
|
|
625
|
+
*/
|
|
626
|
+
function gateRegionalBasemap(styleUrl, policy, fallbackStyleUrl, trusted = false) {
|
|
627
|
+
if (!getRegionalBasemapByStyleUrl(styleUrl)) return { styleUrl };
|
|
628
|
+
if (policy === "block") return {
|
|
629
|
+
styleUrl: fallbackStyleUrl,
|
|
630
|
+
held: "blocked"
|
|
631
|
+
};
|
|
632
|
+
if (policy === "prompt" && !trusted) return {
|
|
633
|
+
styleUrl: fallbackStyleUrl,
|
|
634
|
+
held: "prompt"
|
|
635
|
+
};
|
|
636
|
+
return { styleUrl };
|
|
637
|
+
}
|
|
1048
638
|
// src/cesium-imagery.ts
|
|
1049
639
|
/**
|
|
1050
640
|
* Maps the project's basemap onto imagery the Cesium globe can render.
|
|
@@ -1970,35 +1560,563 @@ function mapGeometryPositions(geometry, mapPosition) {
|
|
|
1970
1560
|
default: return geometry;
|
|
1971
1561
|
}
|
|
1972
1562
|
}
|
|
1973
|
-
// src/
|
|
1563
|
+
// src/color-ramp.ts
|
|
1564
|
+
/** The built-in color ramps offered by the style panels. */
|
|
1565
|
+
const VECTOR_COLOR_RAMPS = [
|
|
1566
|
+
{
|
|
1567
|
+
value: "viridis",
|
|
1568
|
+
label: "Viridis",
|
|
1569
|
+
colors: [
|
|
1570
|
+
"#440154",
|
|
1571
|
+
"#31688e",
|
|
1572
|
+
"#35b779",
|
|
1573
|
+
"#fde725"
|
|
1574
|
+
]
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
value: "plasma",
|
|
1578
|
+
label: "Plasma",
|
|
1579
|
+
colors: [
|
|
1580
|
+
"#0d0887",
|
|
1581
|
+
"#9c179e",
|
|
1582
|
+
"#ed7953",
|
|
1583
|
+
"#f0f921"
|
|
1584
|
+
]
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
value: "inferno",
|
|
1588
|
+
label: "Inferno",
|
|
1589
|
+
colors: [
|
|
1590
|
+
"#000004",
|
|
1591
|
+
"#781c6d",
|
|
1592
|
+
"#ed6925",
|
|
1593
|
+
"#fcffa4"
|
|
1594
|
+
]
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
value: "magma",
|
|
1598
|
+
label: "Magma",
|
|
1599
|
+
colors: [
|
|
1600
|
+
"#000004",
|
|
1601
|
+
"#721f81",
|
|
1602
|
+
"#f1605d",
|
|
1603
|
+
"#fcfdbf"
|
|
1604
|
+
]
|
|
1605
|
+
},
|
|
1606
|
+
{
|
|
1607
|
+
value: "cividis",
|
|
1608
|
+
label: "Cividis",
|
|
1609
|
+
colors: [
|
|
1610
|
+
"#00204d",
|
|
1611
|
+
"#575d6d",
|
|
1612
|
+
"#a59c74",
|
|
1613
|
+
"#ffea46"
|
|
1614
|
+
]
|
|
1615
|
+
},
|
|
1616
|
+
{
|
|
1617
|
+
value: "turbo",
|
|
1618
|
+
label: "Turbo",
|
|
1619
|
+
colors: [
|
|
1620
|
+
"#30123b",
|
|
1621
|
+
"#4777ef",
|
|
1622
|
+
"#1ccfd0",
|
|
1623
|
+
"#b9e642",
|
|
1624
|
+
"#fb8022",
|
|
1625
|
+
"#7a0403"
|
|
1626
|
+
]
|
|
1627
|
+
},
|
|
1628
|
+
{
|
|
1629
|
+
value: "spectral",
|
|
1630
|
+
label: "Spectral",
|
|
1631
|
+
colors: [
|
|
1632
|
+
"#9e0142",
|
|
1633
|
+
"#f46d43",
|
|
1634
|
+
"#ffffbf",
|
|
1635
|
+
"#66c2a5",
|
|
1636
|
+
"#5e4fa2"
|
|
1637
|
+
]
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
value: "blues",
|
|
1641
|
+
label: "Blues",
|
|
1642
|
+
colors: [
|
|
1643
|
+
"#eff6ff",
|
|
1644
|
+
"#93c5fd",
|
|
1645
|
+
"#2563eb",
|
|
1646
|
+
"#1e3a8a"
|
|
1647
|
+
]
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
value: "greens",
|
|
1651
|
+
label: "Greens",
|
|
1652
|
+
colors: [
|
|
1653
|
+
"#f0fdf4",
|
|
1654
|
+
"#86efac",
|
|
1655
|
+
"#16a34a",
|
|
1656
|
+
"#14532d"
|
|
1657
|
+
]
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
value: "oranges",
|
|
1661
|
+
label: "Oranges",
|
|
1662
|
+
colors: [
|
|
1663
|
+
"#fff7ed",
|
|
1664
|
+
"#fdba74",
|
|
1665
|
+
"#f97316",
|
|
1666
|
+
"#7c2d12"
|
|
1667
|
+
]
|
|
1668
|
+
},
|
|
1669
|
+
{
|
|
1670
|
+
value: "reds",
|
|
1671
|
+
label: "Reds",
|
|
1672
|
+
colors: [
|
|
1673
|
+
"#fff5f0",
|
|
1674
|
+
"#fcae91",
|
|
1675
|
+
"#fb6a4a",
|
|
1676
|
+
"#cb181d",
|
|
1677
|
+
"#67000d"
|
|
1678
|
+
]
|
|
1679
|
+
},
|
|
1680
|
+
{
|
|
1681
|
+
value: "purples",
|
|
1682
|
+
label: "Purples",
|
|
1683
|
+
colors: [
|
|
1684
|
+
"#fcfbfd",
|
|
1685
|
+
"#bcbddc",
|
|
1686
|
+
"#807dba",
|
|
1687
|
+
"#54278f",
|
|
1688
|
+
"#3f007d"
|
|
1689
|
+
]
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
value: "terrain",
|
|
1693
|
+
label: "Terrain",
|
|
1694
|
+
colors: [
|
|
1695
|
+
"#333399",
|
|
1696
|
+
"#21bcb3",
|
|
1697
|
+
"#79d05a",
|
|
1698
|
+
"#e8e85a",
|
|
1699
|
+
"#a87b54",
|
|
1700
|
+
"#ffffff"
|
|
1701
|
+
]
|
|
1702
|
+
},
|
|
1703
|
+
{
|
|
1704
|
+
value: "rdylgn",
|
|
1705
|
+
label: "Red-Yellow-Green",
|
|
1706
|
+
colors: [
|
|
1707
|
+
"#a50026",
|
|
1708
|
+
"#f46d43",
|
|
1709
|
+
"#ffffbf",
|
|
1710
|
+
"#66bd63",
|
|
1711
|
+
"#006837"
|
|
1712
|
+
]
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
value: "rdylbu",
|
|
1716
|
+
label: "Red-Yellow-Blue",
|
|
1717
|
+
colors: [
|
|
1718
|
+
"#a50026",
|
|
1719
|
+
"#f46d43",
|
|
1720
|
+
"#ffffbf",
|
|
1721
|
+
"#74add1",
|
|
1722
|
+
"#313695"
|
|
1723
|
+
]
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
value: "rdbu",
|
|
1727
|
+
label: "Red-Blue",
|
|
1728
|
+
colors: [
|
|
1729
|
+
"#b2182b",
|
|
1730
|
+
"#ef8a62",
|
|
1731
|
+
"#f7f7f7",
|
|
1732
|
+
"#67a9cf",
|
|
1733
|
+
"#2166ac"
|
|
1734
|
+
]
|
|
1735
|
+
},
|
|
1736
|
+
{
|
|
1737
|
+
value: "coolwarm",
|
|
1738
|
+
label: "Cool-Warm",
|
|
1739
|
+
colors: [
|
|
1740
|
+
"#3b4cc0",
|
|
1741
|
+
"#7b9ff9",
|
|
1742
|
+
"#dddcdc",
|
|
1743
|
+
"#f49a7b",
|
|
1744
|
+
"#b40426"
|
|
1745
|
+
]
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
value: "jet",
|
|
1749
|
+
label: "Jet",
|
|
1750
|
+
colors: [
|
|
1751
|
+
"#000080",
|
|
1752
|
+
"#0000ff",
|
|
1753
|
+
"#00ffff",
|
|
1754
|
+
"#ffff00",
|
|
1755
|
+
"#ff0000",
|
|
1756
|
+
"#800000"
|
|
1757
|
+
]
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
value: "greys",
|
|
1761
|
+
label: "Greys",
|
|
1762
|
+
colors: [
|
|
1763
|
+
"#ffffff",
|
|
1764
|
+
"#bdbdbd",
|
|
1765
|
+
"#636363",
|
|
1766
|
+
"#000000"
|
|
1767
|
+
]
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
value: "gray",
|
|
1771
|
+
label: "Grayscale",
|
|
1772
|
+
colors: ["#000000", "#ffffff"]
|
|
1773
|
+
}
|
|
1774
|
+
];
|
|
1974
1775
|
/**
|
|
1975
|
-
*
|
|
1776
|
+
* Resolves a ramp definition by name, falling back to the first ramp when the
|
|
1777
|
+
* name is unknown.
|
|
1976
1778
|
*
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
1979
|
-
|
|
1779
|
+
* @param value - The ramp `value` (e.g. "viridis").
|
|
1780
|
+
* @returns The matching ramp, or the first ramp.
|
|
1781
|
+
*/
|
|
1782
|
+
function getVectorColorRamp(value) {
|
|
1783
|
+
return VECTOR_COLOR_RAMPS.find((colorRamp) => colorRamp.value === value) ?? VECTOR_COLOR_RAMPS[0];
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Samples a ramp into `count` evenly spaced hex colors by linearly
|
|
1787
|
+
* interpolating between the ramp's anchor colors.
|
|
1980
1788
|
*
|
|
1981
|
-
*
|
|
1789
|
+
* @param colorRamp - The ramp `value`.
|
|
1790
|
+
* @param count - Number of colors to produce.
|
|
1791
|
+
* @returns An array of `count` hex colors (a single end color when count <= 1).
|
|
1982
1792
|
*/
|
|
1793
|
+
function interpolateRampColors(colorRamp, count) {
|
|
1794
|
+
return interpolateColors(getVectorColorRamp(colorRamp).colors, count);
|
|
1795
|
+
}
|
|
1983
1796
|
/**
|
|
1984
|
-
*
|
|
1797
|
+
* Samples an explicit list of anchor colors into `count` evenly spaced hex
|
|
1798
|
+
* colors by linear interpolation. Backs both the named ramps and user-defined
|
|
1799
|
+
* custom ramps, so a list of hex codes is sampled identically to a built-in.
|
|
1985
1800
|
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1801
|
+
* @param colors - The anchor colors (at least one).
|
|
1802
|
+
* @param count - Number of colors to produce.
|
|
1803
|
+
* @returns An array of `count` hex colors (a single end color when count <= 1).
|
|
1804
|
+
*/
|
|
1805
|
+
function interpolateColors(colors, count) {
|
|
1806
|
+
const anchors = colors.length > 0 ? colors : ["#000000"];
|
|
1807
|
+
if (count <= 0) return [];
|
|
1808
|
+
if (count === 1) return [anchors[anchors.length - 1]];
|
|
1809
|
+
if (anchors.length === 1) return Array.from({ length: count }, () => anchors[0]);
|
|
1810
|
+
return Array.from({ length: count }, (_, index) => {
|
|
1811
|
+
const scaled = index / (count - 1) * (anchors.length - 1);
|
|
1812
|
+
const lowerIndex = Math.floor(scaled);
|
|
1813
|
+
const upperIndex = Math.min(anchors.length - 1, Math.ceil(scaled));
|
|
1814
|
+
const ratio = scaled - lowerIndex;
|
|
1815
|
+
return interpolateHexColor(anchors[lowerIndex], anchors[upperIndex], ratio);
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Normalizes a single user-entered color token into a canonical `#rrggbb`
|
|
1820
|
+
* lowercase hex string, or null when it is not a valid 3- or 6-digit hex
|
|
1821
|
+
* color. Accepts values with or without a leading `#` and expands shorthand
|
|
1822
|
+
* (`#abc` → `#aabbcc`).
|
|
1988
1823
|
*
|
|
1989
|
-
* @param
|
|
1990
|
-
* @returns
|
|
1824
|
+
* @param token - A raw color token (e.g. "FF0000", "#f00", " #AaBbCc ").
|
|
1825
|
+
* @returns The canonical `#rrggbb` color, or null when invalid.
|
|
1991
1826
|
*/
|
|
1992
|
-
function
|
|
1993
|
-
|
|
1994
|
-
|
|
1827
|
+
function normalizeHexColor(token) {
|
|
1828
|
+
let value = token.trim().toLowerCase();
|
|
1829
|
+
if (value === "") return null;
|
|
1830
|
+
if (!value.startsWith("#")) value = `#${value}`;
|
|
1831
|
+
if (/^#[0-9a-f]{3}$/.test(value)) value = `#${value.slice(1).split("").map((channel) => channel + channel).join("")}`;
|
|
1832
|
+
return /^#[0-9a-f]{6}$/.test(value) ? value : null;
|
|
1995
1833
|
}
|
|
1996
1834
|
/**
|
|
1997
|
-
*
|
|
1998
|
-
*
|
|
1999
|
-
*
|
|
1835
|
+
* Parses a free-text list of hex color codes into canonical `#rrggbb` colors,
|
|
1836
|
+
* for the custom color-ramp inputs. Tokens may be separated by commas,
|
|
1837
|
+
* semicolons, or any whitespace (including newlines); invalid tokens are
|
|
1838
|
+
* dropped so a partial paste still yields the colors it can.
|
|
2000
1839
|
*
|
|
2001
|
-
* @param
|
|
1840
|
+
* @param input - The raw text the user entered.
|
|
1841
|
+
* @returns The valid, normalized colors in input order.
|
|
1842
|
+
*/
|
|
1843
|
+
function parseHexColorList(input) {
|
|
1844
|
+
return input.split(/[\s,;]+/).map(normalizeHexColor).filter((color) => color !== null);
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Linearly interpolates between two hex colors.
|
|
1848
|
+
*
|
|
1849
|
+
* @param from - Start hex color (e.g. "#440154").
|
|
1850
|
+
* @param to - End hex color.
|
|
1851
|
+
* @param ratio - Blend factor in [0, 1].
|
|
1852
|
+
* @returns The interpolated hex color.
|
|
1853
|
+
*/
|
|
1854
|
+
function interpolateHexColor(from, to, ratio) {
|
|
1855
|
+
const start = parseHexColor(from);
|
|
1856
|
+
const end = parseHexColor(to);
|
|
1857
|
+
return rgbToHex({
|
|
1858
|
+
r: Math.round(start.r + (end.r - start.r) * ratio),
|
|
1859
|
+
g: Math.round(start.g + (end.g - start.g) * ratio),
|
|
1860
|
+
b: Math.round(start.b + (end.b - start.b) * ratio)
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Parses a `#rrggbb` hex color into its RGB channels.
|
|
1865
|
+
*
|
|
1866
|
+
* @param value - A `#rrggbb` hex color.
|
|
1867
|
+
* @returns The red, green, and blue channels (0-255).
|
|
1868
|
+
*/
|
|
1869
|
+
function parseHexColor(value) {
|
|
1870
|
+
const numeric = Number.parseInt(value.slice(1), 16);
|
|
1871
|
+
return {
|
|
1872
|
+
r: numeric >> 16 & 255,
|
|
1873
|
+
g: numeric >> 8 & 255,
|
|
1874
|
+
b: numeric & 255
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Formats RGB channels as a `#rrggbb` hex color.
|
|
1879
|
+
*
|
|
1880
|
+
* @param color - The red, green, and blue channels (0-255).
|
|
1881
|
+
* @returns A `#rrggbb` hex color.
|
|
1882
|
+
*/
|
|
1883
|
+
function rgbToHex(color) {
|
|
1884
|
+
return `#${[
|
|
1885
|
+
color.r,
|
|
1886
|
+
color.g,
|
|
1887
|
+
color.b
|
|
1888
|
+
].map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
1889
|
+
}
|
|
1890
|
+
/**
|
|
1891
|
+
* Builds `count` evenly spaced break values across [min, max].
|
|
1892
|
+
*
|
|
1893
|
+
* @param min - The minimum value.
|
|
1894
|
+
* @param max - The maximum value.
|
|
1895
|
+
* @param count - Number of breaks to produce.
|
|
1896
|
+
* @returns The break values (min..max inclusive when count > 1).
|
|
1897
|
+
*/
|
|
1898
|
+
function createEqualIntervalBreaks(min, max, count) {
|
|
1899
|
+
return Array.from({ length: count }, (_, index) => {
|
|
1900
|
+
const ratio = count === 1 ? 0 : index / (count - 1);
|
|
1901
|
+
return min + (max - min) * ratio;
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
/**
|
|
1905
|
+
* Builds `count` quantile break values from a sample of numeric values.
|
|
1906
|
+
*
|
|
1907
|
+
* These are ramp *edges* spanning the whole sample (the first break is the
|
|
1908
|
+
* minimum, the last the maximum), matching {@link createEqualIntervalBreaks}.
|
|
1909
|
+
* Graduated vector classes want class *lower bounds* instead; use
|
|
1910
|
+
* {@link createGraduatedClassBreaks}.
|
|
1911
|
+
*
|
|
1912
|
+
* @param values - The sample values.
|
|
1913
|
+
* @param count - Number of breaks to produce.
|
|
1914
|
+
* @returns The quantile break values.
|
|
1915
|
+
*/
|
|
1916
|
+
function createQuantileBreaks(values, count) {
|
|
1917
|
+
if (count <= 0) return [];
|
|
1918
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1919
|
+
if (sorted.length === 0) return [];
|
|
1920
|
+
return Array.from({ length: count }, (_, index) => {
|
|
1921
|
+
const position = count === 1 ? 0 : index / (count - 1) * (sorted.length - 1);
|
|
1922
|
+
return quantileAtPosition(sorted, position);
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
/** Linearly interpolated order statistic at a fractional index into `sorted`. */
|
|
1926
|
+
function quantileAtPosition(sorted, position) {
|
|
1927
|
+
const lowerIndex = Math.floor(position);
|
|
1928
|
+
const upperIndex = Math.min(sorted.length - 1, Math.ceil(position));
|
|
1929
|
+
const ratio = position - lowerIndex;
|
|
1930
|
+
return sorted[lowerIndex] + (sorted[upperIndex] - sorted[lowerIndex]) * ratio;
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Builds the class *lower bounds* for a graduated vector renderer.
|
|
1934
|
+
*
|
|
1935
|
+
* A graduated renderer's stops are lower bounds, not ramp edges: with N stops
|
|
1936
|
+
* sorted ascending, class `i` covers `[stops[i], stops[i + 1])` and the last
|
|
1937
|
+
* class is open-ended above. The legend (`≥ value`) and the QML/SLD exporters
|
|
1938
|
+
* already read them that way, so the breaks must follow the same convention:
|
|
1939
|
+
* `count` classes yield `count` breaks whose first entry is the sample minimum
|
|
1940
|
+
* and whose last entry opens the top class. Producing edges instead (ending at
|
|
1941
|
+
* the maximum) leaves the top class holding only the single largest feature and
|
|
1942
|
+
* puts the interior breaks at the wrong percentiles.
|
|
1943
|
+
*
|
|
1944
|
+
* The result is strictly ascending: duplicate breaks (a skewed sample can push
|
|
1945
|
+
* several quantiles onto the same value) are collapsed, so the caller may get
|
|
1946
|
+
* fewer than `count` breaks and must size its colors off `breaks.length`.
|
|
1947
|
+
* MapLibre rejects a `step` expression whose inputs are not strictly ascending,
|
|
1948
|
+
* so this de-duplication is load-bearing, not cosmetic.
|
|
1949
|
+
*
|
|
1950
|
+
* Standard deviation and geometric interval derive their breaks from the data,
|
|
1951
|
+
* so `count` is a ceiling there: a standard-deviation break that falls outside
|
|
1952
|
+
* the sample range is dropped rather than producing an empty class. See
|
|
1953
|
+
* {@link standardDeviationClassBreaks} and {@link geometricIntervalClassBreaks}
|
|
1954
|
+
* for how each treats its edge cases.
|
|
1955
|
+
*
|
|
1956
|
+
* @param values - The finite numeric values of the classified property.
|
|
1957
|
+
* @param count - Number of classes requested.
|
|
1958
|
+
* @param scheme - The classification scheme.
|
|
1959
|
+
* @returns Up to `count` strictly ascending class lower bounds.
|
|
1960
|
+
*/
|
|
1961
|
+
function createGraduatedClassBreaks(values, count, scheme) {
|
|
1962
|
+
if (count <= 0 || values.length === 0) return [];
|
|
1963
|
+
if (scheme === "natural-breaks") return ascendingUnique(naturalClassBreaks(values, count));
|
|
1964
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1965
|
+
if (scheme === "quantile") return ascendingUnique(Array.from({ length: count }, (_, index) => quantileAtPosition(sorted, index / count * (sorted.length - 1))));
|
|
1966
|
+
if (scheme === "standard-deviation") return ascendingUnique(standardDeviationClassBreaks(sorted, count));
|
|
1967
|
+
if (scheme === "geometric-interval") {
|
|
1968
|
+
const breaks = geometricIntervalClassBreaks(sorted, count);
|
|
1969
|
+
if (breaks) return ascendingUnique(breaks);
|
|
1970
|
+
}
|
|
1971
|
+
return ascendingUnique(equalIntervalClassBreaks(sorted, count));
|
|
1972
|
+
}
|
|
1973
|
+
/** Equal-interval lower bounds over the range of an ascending sample. */
|
|
1974
|
+
function equalIntervalClassBreaks(sorted, count) {
|
|
1975
|
+
const min = sorted[0];
|
|
1976
|
+
const max = sorted[sorted.length - 1];
|
|
1977
|
+
return Array.from({ length: count }, (_, index) => min + (max - min) * index / count);
|
|
1978
|
+
}
|
|
1979
|
+
/**
|
|
1980
|
+
* Standard-deviation lower bounds: classes one standard deviation wide,
|
|
1981
|
+
* centered on the mean. An even `count` puts a break on the mean (classes
|
|
1982
|
+
* `[mean - σ, mean)` and `[mean, mean + σ)` meet there); an odd `count` makes
|
|
1983
|
+
* the middle class straddle it (`[mean - σ/2, mean + σ/2)`). The first break is
|
|
1984
|
+
* the sample minimum, and interior breaks outside `(min, max]` are dropped, so
|
|
1985
|
+
* the open-ended bottom and top classes absorb the tails and no class is empty.
|
|
1986
|
+
* A constant sample (σ = 0) yields the minimum alone.
|
|
1987
|
+
*
|
|
1988
|
+
* Uses the population standard deviation, as QGIS and ArcGIS do.
|
|
1989
|
+
*/
|
|
1990
|
+
function standardDeviationClassBreaks(sorted, count) {
|
|
1991
|
+
const min = sorted[0];
|
|
1992
|
+
const max = sorted[sorted.length - 1];
|
|
1993
|
+
const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
|
|
1994
|
+
const variance = sorted.reduce((sum, value) => sum + (value - mean) * (value - mean), 0) / sorted.length;
|
|
1995
|
+
const deviation = Math.sqrt(variance);
|
|
1996
|
+
if (!(deviation > 0)) return [min];
|
|
1997
|
+
const interior = [];
|
|
1998
|
+
for (let index = 1; index < count; index += 1) {
|
|
1999
|
+
const value = mean + (index - count / 2) * deviation;
|
|
2000
|
+
if (value > min && value <= max) interior.push(value);
|
|
2001
|
+
}
|
|
2002
|
+
return [min, ...interior];
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Geometric-interval lower bounds: class edges growing by a constant ratio
|
|
2006
|
+
* from the smallest positive value to the maximum, so each class spans the
|
|
2007
|
+
* same multiple of its lower bound. Suits skewed data spanning orders of
|
|
2008
|
+
* magnitude (population, income, concentrations).
|
|
2009
|
+
*
|
|
2010
|
+
* A geometric series cannot start at or below zero, so it starts at the
|
|
2011
|
+
* smallest positive value and the first break stays the sample minimum: zero
|
|
2012
|
+
* and negative values all fall into the first class. When no value is
|
|
2013
|
+
* positive, the result is `null` and the caller falls back to equal interval.
|
|
2014
|
+
*/
|
|
2015
|
+
function geometricIntervalClassBreaks(sorted, count) {
|
|
2016
|
+
const max = sorted[sorted.length - 1];
|
|
2017
|
+
const lowest = sorted.find((value) => value > 0);
|
|
2018
|
+
if (lowest === void 0) return null;
|
|
2019
|
+
const logLowest = Math.log(lowest);
|
|
2020
|
+
const logStep = (Math.log(max) - logLowest) / count;
|
|
2021
|
+
return [sorted[0], ...Array.from({ length: count - 1 }, (_, index) => Math.exp(logLowest + logStep * (index + 1)))];
|
|
2022
|
+
}
|
|
2023
|
+
/** Drops non-finite breaks and collapses repeats, keeping ascending order. */
|
|
2024
|
+
function ascendingUnique(breaks) {
|
|
2025
|
+
const out = [];
|
|
2026
|
+
for (const value of breaks) {
|
|
2027
|
+
if (!Number.isFinite(value)) continue;
|
|
2028
|
+
if (out.length > 0 && value <= out[out.length - 1]) continue;
|
|
2029
|
+
out.push(value);
|
|
2030
|
+
}
|
|
2031
|
+
return out;
|
|
2032
|
+
}
|
|
2033
|
+
/** Cap on the Jenks input size; the DP below is roughly O(n^2 * k). */
|
|
2034
|
+
const MAX_NATURAL_BREAK_SAMPLES = 1e3;
|
|
2035
|
+
/** Evenly thins a sorted array down to at most `maxSamples` entries. */
|
|
2036
|
+
function downsampleSortedValues(values, maxSamples) {
|
|
2037
|
+
if (values.length <= maxSamples) return values;
|
|
2038
|
+
const result = [];
|
|
2039
|
+
const step = (values.length - 1) / (maxSamples - 1);
|
|
2040
|
+
for (let index = 0; index < maxSamples; index += 1) result.push(values[Math.round(index * step)]);
|
|
2041
|
+
return result;
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Jenks natural breaks: the class lower bounds that minimize the summed
|
|
2045
|
+
* within-class variance. Returns the sample minimum first, then the first value
|
|
2046
|
+
* of each subsequent class.
|
|
2047
|
+
*/
|
|
2048
|
+
function naturalClassBreaks(values, count) {
|
|
2049
|
+
const sorted = downsampleSortedValues(Array.from(new Set(values)).sort((a, b) => a - b), MAX_NATURAL_BREAK_SAMPLES);
|
|
2050
|
+
if (sorted.length <= count) return sorted;
|
|
2051
|
+
const lowerClassLimits = Array.from({ length: sorted.length + 1 }, () => Array(count + 1).fill(0));
|
|
2052
|
+
const varianceCombinations = Array.from({ length: sorted.length + 1 }, () => Array(count + 1).fill(Number.POSITIVE_INFINITY));
|
|
2053
|
+
for (let classIndex = 1; classIndex <= count; classIndex += 1) {
|
|
2054
|
+
lowerClassLimits[1][classIndex] = 1;
|
|
2055
|
+
varianceCombinations[1][classIndex] = 0;
|
|
2056
|
+
}
|
|
2057
|
+
for (let valueIndex = 2; valueIndex <= sorted.length; valueIndex += 1) {
|
|
2058
|
+
let sum = 0;
|
|
2059
|
+
let sumSquares = 0;
|
|
2060
|
+
let weight = 0;
|
|
2061
|
+
for (let lowerIndex = 1; lowerIndex <= valueIndex; lowerIndex += 1) {
|
|
2062
|
+
const currentIndex = valueIndex - lowerIndex + 1;
|
|
2063
|
+
const value = sorted[currentIndex - 1];
|
|
2064
|
+
weight += 1;
|
|
2065
|
+
sum += value;
|
|
2066
|
+
sumSquares += value * value;
|
|
2067
|
+
const variance = sumSquares - sum * sum / weight;
|
|
2068
|
+
const previousIndex = currentIndex - 1;
|
|
2069
|
+
if (previousIndex === 0) continue;
|
|
2070
|
+
for (let classIndex = 2; classIndex <= count; classIndex += 1) {
|
|
2071
|
+
const candidate = variance + varianceCombinations[previousIndex][classIndex - 1];
|
|
2072
|
+
if (varianceCombinations[valueIndex][classIndex] >= candidate) {
|
|
2073
|
+
lowerClassLimits[valueIndex][classIndex] = currentIndex;
|
|
2074
|
+
varianceCombinations[valueIndex][classIndex] = candidate;
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
lowerClassLimits[valueIndex][1] = 1;
|
|
2079
|
+
varianceCombinations[valueIndex][1] = sumSquares - sum * sum / Math.max(1, weight);
|
|
2080
|
+
}
|
|
2081
|
+
const breaks = Array(count).fill(sorted[0]);
|
|
2082
|
+
let valueIndex = sorted.length;
|
|
2083
|
+
for (let classIndex = count; classIndex >= 2; classIndex -= 1) {
|
|
2084
|
+
const lowerClassLimit = Math.max(1, lowerClassLimits[valueIndex][classIndex]);
|
|
2085
|
+
breaks[classIndex - 1] = sorted[lowerClassLimit - 1];
|
|
2086
|
+
valueIndex = lowerClassLimit - 1;
|
|
2087
|
+
}
|
|
2088
|
+
breaks[0] = sorted[0];
|
|
2089
|
+
return breaks;
|
|
2090
|
+
}
|
|
2091
|
+
// src/polyline.ts
|
|
2092
|
+
/**
|
|
2093
|
+
* Pure TypeScript Google Encoded Polyline algorithm codec.
|
|
2094
|
+
*
|
|
2095
|
+
* Supports arbitrary precision:
|
|
2096
|
+
* - precision = 5: Google Maps standard, OSRM, HERE (factor 1e5, ~1.1m precision)
|
|
2097
|
+
* - precision = 6: Valhalla, Mapbox, Open Source Routing Machine 6 (factor 1e6, ~0.11m precision)
|
|
2098
|
+
*
|
|
2099
|
+
* Coordinates are represented as `[lon, lat]` pairs in GeoJSON format.
|
|
2100
|
+
*/
|
|
2101
|
+
/**
|
|
2102
|
+
* Unescapes backslashes in a polyline string.
|
|
2103
|
+
*
|
|
2104
|
+
* When polyline strings are copied from JSON payloads, log files, or source code,
|
|
2105
|
+
* backslashes are frequently double-escaped as `\\` or formatted with escape sequences.
|
|
2106
|
+
*
|
|
2107
|
+
* @param str - Input string with potentially escaped backslashes.
|
|
2108
|
+
* @returns Cleaned polyline string.
|
|
2109
|
+
*/
|
|
2110
|
+
function unescapePolyline(str) {
|
|
2111
|
+
if (!str || typeof str !== "string") return "";
|
|
2112
|
+
return str.replace(/\\\\/g, "\\").replace(/\\"/g, "\"").replace(/\\'/g, "'");
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Decodes an encoded polyline string into an array of `[lon, lat]` coordinates,
|
|
2116
|
+
* along with a `complete` flag indicating whether the entire string was consumed
|
|
2117
|
+
* as complete coordinate pairs without truncation or malformed characters.
|
|
2118
|
+
*
|
|
2119
|
+
* @param encoded - The ASCII-encoded polyline string.
|
|
2002
2120
|
* @param precision - Number of decimal digits (default: 5).
|
|
2003
2121
|
* @param unescape - Whether to unescape double-escaped backslashes before decoding (default: false).
|
|
2004
2122
|
* @returns Object with decoded `coordinates` and `complete` boolean status.
|
|
@@ -2664,188 +2782,1157 @@ function compileMapExpression(source, options) {
|
|
|
2664
2782
|
};
|
|
2665
2783
|
}
|
|
2666
2784
|
/**
|
|
2667
|
-
* Replaces every string node that exactly matches a known `@token` with that
|
|
2668
|
-
* variable's value, returning a new structure (the input is not mutated).
|
|
2669
|
-
* Unknown `@` strings are left untouched so field values that happen to start
|
|
2670
|
-
* with `@` never get mangled.
|
|
2785
|
+
* Replaces every string node that exactly matches a known `@token` with that
|
|
2786
|
+
* variable's value, returning a new structure (the input is not mutated).
|
|
2787
|
+
* Unknown `@` strings are left untouched so field values that happen to start
|
|
2788
|
+
* with `@` never get mangled.
|
|
2789
|
+
*/
|
|
2790
|
+
function substituteExpressionVariables(node, variables) {
|
|
2791
|
+
if (typeof node === "string" && node.startsWith("@")) {
|
|
2792
|
+
const match = variables.find((variable) => variable.token === node);
|
|
2793
|
+
return match ? match.value : node;
|
|
2794
|
+
}
|
|
2795
|
+
if (Array.isArray(node)) return node.map((entry) => substituteExpressionVariables(entry, variables));
|
|
2796
|
+
if (node && typeof node === "object") return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, substituteExpressionVariables(value, variables)]));
|
|
2797
|
+
return node;
|
|
2798
|
+
}
|
|
2799
|
+
/**
|
|
2800
|
+
* Evaluates an expression source against a sample feature for the builder's
|
|
2801
|
+
* live preview. Variables are substituted first, then the expression is
|
|
2802
|
+
* compiled with the MapLibre style spec and evaluated with the given zoom.
|
|
2803
|
+
* Runtime failures (e.g. `to-color` on garbage) come back as errors instead
|
|
2804
|
+
* of throwing.
|
|
2805
|
+
*/
|
|
2806
|
+
function evaluateMapExpression(source, options = {}) {
|
|
2807
|
+
const trimmed = source.trim();
|
|
2808
|
+
if (!trimmed) return { kind: "empty" };
|
|
2809
|
+
const { validation, expression } = compileMapExpression(trimmed, {
|
|
2810
|
+
variables: options.variables,
|
|
2811
|
+
expectedType: options.expectedType
|
|
2812
|
+
});
|
|
2813
|
+
if (!validation.ok || !expression) return {
|
|
2814
|
+
kind: "error",
|
|
2815
|
+
errors: validation.errors
|
|
2816
|
+
};
|
|
2817
|
+
const feature = options.feature ?? null;
|
|
2818
|
+
const geometryType = feature?.geometry?.type ?? "Unknown";
|
|
2819
|
+
try {
|
|
2820
|
+
return {
|
|
2821
|
+
kind: "value",
|
|
2822
|
+
value: expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {
|
|
2823
|
+
type: geometryType,
|
|
2824
|
+
properties: feature?.properties ?? {},
|
|
2825
|
+
...feature?.id !== void 0 ? { id: feature.id } : {},
|
|
2826
|
+
geometry: feature?.geometry
|
|
2827
|
+
})
|
|
2828
|
+
};
|
|
2829
|
+
} catch (error) {
|
|
2830
|
+
return {
|
|
2831
|
+
kind: "error",
|
|
2832
|
+
errors: [error instanceof Error ? error.message : String(error)]
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* Compiles an expression source once for bulk per-feature evaluation (virtual
|
|
2838
|
+
* fields, and any other caller that runs one expression over a whole layer).
|
|
2839
|
+
* Unlike {@link evaluateMapExpression} the compile cost is paid once, not per
|
|
2840
|
+
* feature. An empty source is reported as a failure with no errors — there is
|
|
2841
|
+
* nothing to evaluate.
|
|
2842
|
+
*/
|
|
2843
|
+
function compileFeatureExpression(source, options = {}) {
|
|
2844
|
+
const trimmed = source.trim();
|
|
2845
|
+
if (!trimmed) return {
|
|
2846
|
+
ok: false,
|
|
2847
|
+
errors: []
|
|
2848
|
+
};
|
|
2849
|
+
const { validation, expression } = compileMapExpression(trimmed, {
|
|
2850
|
+
variables: options.variables,
|
|
2851
|
+
expectedType: options.expectedType
|
|
2852
|
+
});
|
|
2853
|
+
if (!validation.ok || !expression) return {
|
|
2854
|
+
ok: false,
|
|
2855
|
+
errors: validation.errors
|
|
2856
|
+
};
|
|
2857
|
+
const compiledZoom = options.zoom ?? 0;
|
|
2858
|
+
return {
|
|
2859
|
+
ok: true,
|
|
2860
|
+
errors: [],
|
|
2861
|
+
evaluate: (feature, zoom = compiledZoom) => expression.evaluateWithoutErrorHandling({ zoom }, {
|
|
2862
|
+
type: feature.geometry?.type ?? "Unknown",
|
|
2863
|
+
properties: feature.properties ?? {},
|
|
2864
|
+
...feature.id !== void 0 ? { id: feature.id } : {},
|
|
2865
|
+
geometry: feature.geometry
|
|
2866
|
+
})
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
/**
|
|
2870
|
+
* Evaluates a boolean expression against every feature and returns the
|
|
2871
|
+
* selection ids of the matches (Select by Expression). Unlike
|
|
2872
|
+
* {@link evaluateMapExpression} this compiles the source once and reuses the
|
|
2873
|
+
* compiled expression across the whole array, so it stays fast on large
|
|
2874
|
+
* layers. An empty source is reported as `ok: false` with no errors — there
|
|
2875
|
+
* is nothing to select by.
|
|
2876
|
+
*/
|
|
2877
|
+
function matchFeaturesByExpression(features, source, options = {}) {
|
|
2878
|
+
const trimmed = source.trim();
|
|
2879
|
+
if (!trimmed) return {
|
|
2880
|
+
ok: false,
|
|
2881
|
+
errors: [],
|
|
2882
|
+
ids: [],
|
|
2883
|
+
errorCount: 0
|
|
2884
|
+
};
|
|
2885
|
+
const { validation, expression } = compileMapExpression(trimmed, {
|
|
2886
|
+
variables: options.variables,
|
|
2887
|
+
expectedType: "boolean"
|
|
2888
|
+
});
|
|
2889
|
+
if (!validation.ok || !expression) return {
|
|
2890
|
+
ok: false,
|
|
2891
|
+
errors: validation.errors,
|
|
2892
|
+
ids: [],
|
|
2893
|
+
errorCount: 0
|
|
2894
|
+
};
|
|
2895
|
+
const ids = [];
|
|
2896
|
+
let errorCount = 0;
|
|
2897
|
+
features.forEach((feature, index) => {
|
|
2898
|
+
try {
|
|
2899
|
+
if (expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {
|
|
2900
|
+
type: feature.geometry?.type ?? "Unknown",
|
|
2901
|
+
properties: feature.properties ?? {},
|
|
2902
|
+
...feature.id !== void 0 ? { id: feature.id } : {},
|
|
2903
|
+
geometry: feature.geometry
|
|
2904
|
+
}) === true) ids.push(featureSelectionId(feature, index));
|
|
2905
|
+
} catch {
|
|
2906
|
+
errorCount += 1;
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2909
|
+
return {
|
|
2910
|
+
ok: true,
|
|
2911
|
+
errors: [],
|
|
2912
|
+
ids,
|
|
2913
|
+
errorCount
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
/**
|
|
2917
|
+
* True when a preview value is a real style-spec `Color` instance (its
|
|
2918
|
+
* evaluated color representation: premultiplied r/g/b/a floats). Shared by
|
|
2919
|
+
* the preview formatter and the dialog's color-swatch rendering so the two
|
|
2920
|
+
* checks cannot drift apart. An instanceof check (not duck typing) so user
|
|
2921
|
+
* attribute data that happens to carry numeric r/g/b/a fields (e.g. via
|
|
2922
|
+
* `["properties"]`) is never misclassified as a color.
|
|
2923
|
+
*/
|
|
2924
|
+
function isStyleSpecColor(value) {
|
|
2925
|
+
return value instanceof Color;
|
|
2926
|
+
}
|
|
2927
|
+
/**
|
|
2928
|
+
* Renders a preview value as a short display string: style-spec Color
|
|
2929
|
+
* instances become rgba() strings, plain values render as JSON, and
|
|
2930
|
+
* `null`/`undefined` render as "null".
|
|
2931
|
+
*/
|
|
2932
|
+
function formatExpressionPreviewValue(value) {
|
|
2933
|
+
if (value === null || value === void 0) return "null";
|
|
2934
|
+
if (isStyleSpecColor(value)) {
|
|
2935
|
+
const color = value;
|
|
2936
|
+
const alpha = color.a;
|
|
2937
|
+
const channel = (component) => Math.round((alpha === 0 ? 0 : component / alpha) * 255);
|
|
2938
|
+
return `rgba(${channel(color.r)}, ${channel(color.g)}, ${channel(color.b)}, ${Number(alpha.toFixed(3))})`;
|
|
2939
|
+
}
|
|
2940
|
+
try {
|
|
2941
|
+
return JSON.stringify(value) ?? String(value);
|
|
2942
|
+
} catch {
|
|
2943
|
+
return String(value);
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Infers a display type for each named field by scanning feature properties:
|
|
2948
|
+
* a single consistent primitive type wins, disagreements become "mixed", and
|
|
2949
|
+
* fields with no non-null values are "unknown".
|
|
2950
|
+
*/
|
|
2951
|
+
function inferFieldTypes(features, names) {
|
|
2952
|
+
const result = {};
|
|
2953
|
+
for (const name of names) result[name] = "unknown";
|
|
2954
|
+
for (const feature of features) {
|
|
2955
|
+
const properties = feature.properties;
|
|
2956
|
+
if (!properties) continue;
|
|
2957
|
+
for (const name of names) {
|
|
2958
|
+
const value = properties[name];
|
|
2959
|
+
if (value === null || value === void 0) continue;
|
|
2960
|
+
const valueType = typeof value;
|
|
2961
|
+
const fieldType = valueType === "string" || valueType === "number" || valueType === "boolean" ? valueType : "mixed";
|
|
2962
|
+
if (result[name] === "unknown") result[name] = fieldType;
|
|
2963
|
+
else if (result[name] !== fieldType) result[name] = "mixed";
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
return result;
|
|
2967
|
+
}
|
|
2968
|
+
// src/vector-color.ts
|
|
2969
|
+
/** Whether a color value is a data-driven expression rather than a flat color. */
|
|
2970
|
+
function isVectorColorExpression(value) {
|
|
2971
|
+
return Array.isArray(value);
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Whether a string is a 6-digit hex color (`#rrggbb`), the form the vector
|
|
2975
|
+
* renderer treats as valid. Exported so the symbology exporters can apply the
|
|
2976
|
+
* same validity check the live `match`/`case` expressions use rather than
|
|
2977
|
+
* re-deriving the regex.
|
|
2978
|
+
*/
|
|
2979
|
+
function isHexColor(value) {
|
|
2980
|
+
return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value.trim());
|
|
2981
|
+
}
|
|
2982
|
+
/** A 3- or 6-digit hex color, as emitted by the simplestyle spec. */
|
|
2983
|
+
function isSimpleStyleColor(value) {
|
|
2984
|
+
return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim());
|
|
2985
|
+
}
|
|
2986
|
+
/**
|
|
2987
|
+
* simplestyle-spec color and numeric property names. Color keys carry CSS hex
|
|
2988
|
+
* colors; numeric keys carry plain numbers. See
|
|
2989
|
+
* https://github.com/mapbox/simplestyle-spec.
|
|
2990
|
+
*/
|
|
2991
|
+
const SIMPLE_STYLE_COLOR_KEYS = [
|
|
2992
|
+
"fill",
|
|
2993
|
+
"stroke",
|
|
2994
|
+
"marker-color"
|
|
2995
|
+
];
|
|
2996
|
+
const SIMPLE_STYLE_NUMBER_KEYS = [
|
|
2997
|
+
"fill-opacity",
|
|
2998
|
+
"stroke-width",
|
|
2999
|
+
"stroke-opacity",
|
|
3000
|
+
"marker-opacity"
|
|
3001
|
+
];
|
|
3002
|
+
function isSimpleStyleEnabled(style) {
|
|
3003
|
+
return styleValue(style, "simpleStyleEnabled") === true;
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Wrap a resolved color value so a per-feature simplestyle property takes
|
|
3007
|
+
* precedence when {@link LayerStyle.simpleStyleEnabled} is set. Returns the base
|
|
3008
|
+
* value unchanged when the feature lacks the property or the mode is off.
|
|
3009
|
+
*
|
|
3010
|
+
* @param style - The layer style.
|
|
3011
|
+
* @param property - The simplestyle property name (e.g. `fill`, `stroke`).
|
|
3012
|
+
* @param base - The flat color or expression to fall back to.
|
|
3013
|
+
* @returns A `coalesce` expression, or the base value when disabled.
|
|
3014
|
+
*/
|
|
3015
|
+
function withSimpleStyleColor(style, property, base) {
|
|
3016
|
+
if (!isSimpleStyleEnabled(style)) return base;
|
|
3017
|
+
return mapZoomStepOutputs(base, (value) => [
|
|
3018
|
+
"coalesce",
|
|
3019
|
+
["get", property],
|
|
3020
|
+
value
|
|
3021
|
+
]);
|
|
3022
|
+
}
|
|
3023
|
+
/**
|
|
3024
|
+
* Apply a per-feature transform to a value, descending into the outputs of a
|
|
3025
|
+
* top-level `["step", ["zoom"], …]` expression (as produced by the rule-based
|
|
3026
|
+
* per-rule zoom compilation) so the zoom step stays outermost. MapLibre
|
|
3027
|
+
* rejects any expression where `["zoom"]` is not the input of a top-level
|
|
3028
|
+
* `step`/`interpolate`, so wrappers (simplestyle coalesce, opacity folds)
|
|
3029
|
+
* must be pushed inside the step's outputs rather than wrapped around it.
|
|
3030
|
+
* Non-step values are transformed directly.
|
|
3031
|
+
*/
|
|
3032
|
+
function mapZoomStepOutputs(value, transform) {
|
|
3033
|
+
if (Array.isArray(value) && value[0] === "step" && Array.isArray(value[1]) && value[1][0] === "zoom") {
|
|
3034
|
+
const out = [
|
|
3035
|
+
"step",
|
|
3036
|
+
value[1],
|
|
3037
|
+
transform(value[2])
|
|
3038
|
+
];
|
|
3039
|
+
for (let index = 3; index < value.length; index += 2) out.push(value[index], transform(value[index + 1]));
|
|
3040
|
+
return out;
|
|
3041
|
+
}
|
|
3042
|
+
return transform(value);
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* Resolve a numeric paint value, letting a per-feature simplestyle property
|
|
3046
|
+
* override the layer value when {@link LayerStyle.simpleStyleEnabled} is set.
|
|
3047
|
+
*
|
|
3048
|
+
* The lookup is wrapped in a `coalesce` rather than leaning on `to-number`'s
|
|
3049
|
+
* own fallback argument, because that fallback only fires when a value fails
|
|
3050
|
+
* to convert — and a *missing* property does not fail, it converts to 0. Since
|
|
3051
|
+
* simplestyle is enabled for the whole layer as soon as any feature carries any
|
|
3052
|
+
* one of these keys, features that carry only some of them (an ArcGIS KML
|
|
3053
|
+
* export writes `fill`/`fill-opacity` but no `marker-opacity`, for instance)
|
|
3054
|
+
* would otherwise be painted at opacity 0 or width 0 — present in the layer,
|
|
3055
|
+
* invisible on the map (#1552).
|
|
3056
|
+
*
|
|
3057
|
+
* @param style - The layer style.
|
|
3058
|
+
* @param property - The simplestyle property name (e.g. `stroke-width`).
|
|
3059
|
+
* @param base - The layer-level fallback value.
|
|
3060
|
+
* @returns A `to-number` expression, or `base` when disabled.
|
|
3061
|
+
*/
|
|
3062
|
+
function simpleStyleNumberValue(style, property, base) {
|
|
3063
|
+
if (!isSimpleStyleEnabled(style)) return base;
|
|
3064
|
+
return [
|
|
3065
|
+
"to-number",
|
|
3066
|
+
[
|
|
3067
|
+
"coalesce",
|
|
3068
|
+
["get", property],
|
|
3069
|
+
base
|
|
3070
|
+
],
|
|
3071
|
+
base
|
|
3072
|
+
];
|
|
3073
|
+
}
|
|
3074
|
+
/** Whether `node` is `["get", key]` for one of `keys`, returning the key. */
|
|
3075
|
+
function simpleStyleGet(node, keys) {
|
|
3076
|
+
if (!Array.isArray(node) || node.length !== 2 || node[0] !== "get") return null;
|
|
3077
|
+
return keys.includes(node[1]) ? node[1] : null;
|
|
3078
|
+
}
|
|
3079
|
+
/**
|
|
3080
|
+
* Read back the per-feature color lookup {@link withSimpleStyleColor} writes,
|
|
3081
|
+
* `["coalesce", ["get", key], base]` with a simplestyle color key, so a style
|
|
3082
|
+
* importer recognizes exactly what the exporter emits.
|
|
3083
|
+
*
|
|
3084
|
+
* @param node - A paint value or a node inside one.
|
|
3085
|
+
* @returns The simplestyle key and the layer-level base, or `null` when the
|
|
3086
|
+
* node is not that lookup.
|
|
3087
|
+
*/
|
|
3088
|
+
function readSimpleStyleColorLookup(node) {
|
|
3089
|
+
if (!Array.isArray(node) || node.length !== 3 || node[0] !== "coalesce") return null;
|
|
3090
|
+
const property = simpleStyleGet(node[1], SIMPLE_STYLE_COLOR_KEYS);
|
|
3091
|
+
return property ? {
|
|
3092
|
+
property,
|
|
3093
|
+
base: node[2]
|
|
3094
|
+
} : null;
|
|
3095
|
+
}
|
|
3096
|
+
/**
|
|
3097
|
+
* Read back the per-feature number lookup {@link simpleStyleNumberValue}
|
|
3098
|
+
* writes, `["to-number", ["coalesce", ["get", key], base], base]` with a
|
|
3099
|
+
* simplestyle number key and a numeric base.
|
|
3100
|
+
*
|
|
3101
|
+
* @param node - A paint value or a node inside one.
|
|
3102
|
+
* @returns The simplestyle key and the layer-level base, or `null` when the
|
|
3103
|
+
* node is not that lookup.
|
|
3104
|
+
*/
|
|
3105
|
+
function readSimpleStyleNumberLookup(node) {
|
|
3106
|
+
if (!Array.isArray(node) || node.length !== 3 || node[0] !== "to-number") return null;
|
|
3107
|
+
const inner = node[1];
|
|
3108
|
+
if (!Array.isArray(inner) || inner.length !== 3 || inner[0] !== "coalesce") return null;
|
|
3109
|
+
const property = simpleStyleGet(inner[1], SIMPLE_STYLE_NUMBER_KEYS);
|
|
3110
|
+
const base = node[2];
|
|
3111
|
+
if (!property || typeof base !== "number" || !Number.isFinite(base) || inner[2] !== base) return null;
|
|
3112
|
+
return {
|
|
3113
|
+
property,
|
|
3114
|
+
base
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
/**
|
|
3118
|
+
* Whether a paint value reads per-feature simplestyle properties the way the
|
|
3119
|
+
* GeoLibre exporter writes them, anywhere inside it (a lookup can sit inside a
|
|
3120
|
+
* zoom step, a per-rule `case` or an opacity multiplication).
|
|
3121
|
+
*
|
|
3122
|
+
* @param value - A paint value.
|
|
3123
|
+
* @returns `true` when a color or number lookup is found.
|
|
3124
|
+
*/
|
|
3125
|
+
function readsSimpleStyleProperties(value) {
|
|
3126
|
+
if (!Array.isArray(value)) return false;
|
|
3127
|
+
if (readSimpleStyleColorLookup(value) || readSimpleStyleNumberLookup(value)) return true;
|
|
3128
|
+
return value.some(readsSimpleStyleProperties);
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Ground resolution (meters per pixel) at MapLibre zoom 0 on the equator, for
|
|
3132
|
+
* the Web Mercator projection: earth circumference (2*pi*6378137) over the
|
|
3133
|
+
* 512px world at zoom 0. Resolution halves with every zoom level. Exported so
|
|
3134
|
+
* the Mapbox-style importer can reverse {@link metersWidthExpression} without a
|
|
3135
|
+
* second copy of the magic number.
|
|
3136
|
+
*/
|
|
3137
|
+
const MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 = 2 * Math.PI * 6378137 / 512;
|
|
3138
|
+
/**
|
|
3139
|
+
* Same ground resolution as {@link MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0} but for
|
|
3140
|
+
* the project's active body: its circumference over the 512px zoom-0 world. On
|
|
3141
|
+
* Earth this equals the constant; on the Moon/Mars it uses that body's radius so
|
|
3142
|
+
* a stroke width given in ground meters renders at the correct on-screen size.
|
|
3143
|
+
* Exported so the Mapbox-style importer can reverse {@link metersWidthExpression}
|
|
3144
|
+
* with the same ellipsoid-aware factor the exporter used (keeping the round trip
|
|
3145
|
+
* correct on non-Earth projects), rather than the Earth-only constant.
|
|
3146
|
+
*/
|
|
3147
|
+
function mercatorMetersPerPixelAtZoom0() {
|
|
3148
|
+
return 2 * Math.PI * getActiveSemiMajorAxisMeters() / 512;
|
|
3149
|
+
}
|
|
3150
|
+
const MAX_MERCATOR_ZOOM = 24;
|
|
3151
|
+
/**
|
|
3152
|
+
* Build a zoom-driven width expression that keeps a stroke proportional to the
|
|
3153
|
+
* map scale, so a width given in ground meters renders thicker when zoomed in
|
|
3154
|
+
* and thinner when zoomed out (QGIS "map units" behavior).
|
|
3155
|
+
*
|
|
3156
|
+
* In Web Mercator the pixels-per-meter ratio doubles with each zoom level, so
|
|
3157
|
+
* an `["exponential", 2]` interpolation between two stops one zoom apart is
|
|
3158
|
+
* exact across the whole range. The conversion is referenced to the equator;
|
|
3159
|
+
* because Mercator stretches distances toward the poles, the on-screen width at
|
|
3160
|
+
* higher latitudes is correspondingly larger, matching how the underlying map
|
|
3161
|
+
* is itself stretched.
|
|
3162
|
+
*
|
|
3163
|
+
* Typed maplibre-agnostically (`unknown[]`); consumers cast to the concrete
|
|
3164
|
+
* `PropertyValueSpecification<number>` where the MapLibre types are in scope.
|
|
3165
|
+
*
|
|
3166
|
+
* @param meters - The stroke width in ground meters.
|
|
3167
|
+
* @returns A MapLibre `interpolate` expression array.
|
|
3168
|
+
*/
|
|
3169
|
+
function metersWidthExpression(meters) {
|
|
3170
|
+
const widthAtZoom0 = meters / mercatorMetersPerPixelAtZoom0();
|
|
3171
|
+
return [
|
|
3172
|
+
"interpolate",
|
|
3173
|
+
["exponential", 2],
|
|
3174
|
+
["zoom"],
|
|
3175
|
+
0,
|
|
3176
|
+
widthAtZoom0,
|
|
3177
|
+
MAX_MERCATOR_ZOOM,
|
|
3178
|
+
widthAtZoom0 * 2 ** MAX_MERCATOR_ZOOM
|
|
3179
|
+
];
|
|
3180
|
+
}
|
|
3181
|
+
/**
|
|
3182
|
+
* Resolve the `line-width` paint value for a layer style, honoring the
|
|
3183
|
+
* {@link LayerStyle.strokeWidthUnit}:
|
|
3184
|
+
*
|
|
3185
|
+
* - `"meters"`: a zoom-driven {@link metersWidthExpression} from the flat
|
|
3186
|
+
* `strokeWidth`, so the stroke scales with the map. A per-feature pixel
|
|
3187
|
+
* `stroke-width` override no longer applies in this mode.
|
|
3188
|
+
* - `"pixels"` (default): the constant pixel width, still honoring any
|
|
3189
|
+
* per-feature simplestyle `stroke-width`.
|
|
3190
|
+
*
|
|
3191
|
+
* Shared by the map style-mapper and the geo-editor plugin so the Sketches
|
|
3192
|
+
* store layer and Geoman's interaction display layers render an identical
|
|
3193
|
+
* width.
|
|
3194
|
+
*
|
|
3195
|
+
* @param style - The layer style.
|
|
3196
|
+
* @returns A number (constant pixels) or a MapLibre expression array.
|
|
3197
|
+
*/
|
|
3198
|
+
function lineWidthValue(style) {
|
|
3199
|
+
const proportionalWidth = proportionalRadiusExpression(style);
|
|
3200
|
+
if (proportionalWidth) return vectorStrokeWidthValue(style, proportionalWidth);
|
|
3201
|
+
if (styleValue(style, "strokeWidthUnit") === "meters") return metersWidthExpression(styleValue(style, "strokeWidth"));
|
|
3202
|
+
return vectorStrokeWidthValue(style, simpleStyleNumberValue(style, "stroke-width", styleValue(style, "strokeWidth")));
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* The layer's stroke opacity, clamped to [0, 1]. A missing or non-finite value
|
|
3206
|
+
* (an older project, a hand-edited file) reads as fully opaque.
|
|
3207
|
+
*
|
|
3208
|
+
* @param style - The layer style.
|
|
3209
|
+
* @returns The stroke opacity.
|
|
3210
|
+
*/
|
|
3211
|
+
function strokeOpacityValue(style) {
|
|
3212
|
+
const value = styleValue(style, "strokeOpacity");
|
|
3213
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* A stroke opacity paint value: the layer's {@link LayerStyle.strokeOpacity},
|
|
3217
|
+
* which a per-feature simplestyle `stroke-opacity` replaces when
|
|
3218
|
+
* {@link LayerStyle.simpleStyleEnabled} is set.
|
|
3219
|
+
*
|
|
3220
|
+
* @param style - The layer style.
|
|
3221
|
+
* @returns A number, or the simplestyle lookup expression.
|
|
3222
|
+
*/
|
|
3223
|
+
function strokeOpacityPaintValue(style) {
|
|
3224
|
+
return simpleStyleNumberValue(style, "stroke-opacity", strokeOpacityValue(style));
|
|
3225
|
+
}
|
|
3226
|
+
/**
|
|
3227
|
+
* Whether a value is a usable dash pattern: finite, non-negative lengths with
|
|
3228
|
+
* at least one dash longer than zero. Exported so the style sanitizer and the
|
|
3229
|
+
* importers apply the same rule the renderer does.
|
|
3230
|
+
*
|
|
3231
|
+
* @param value - The candidate pattern.
|
|
3232
|
+
* @returns True when the value can be drawn as a dash pattern.
|
|
3233
|
+
*/
|
|
3234
|
+
function isStrokeDashPattern(value) {
|
|
3235
|
+
return Array.isArray(value) && value.length > 0 && value.every((length) => typeof length === "number" && Number.isFinite(length) && length >= 0) && value.some((length) => length > 0);
|
|
3236
|
+
}
|
|
3237
|
+
/**
|
|
3238
|
+
* The `line-dasharray` for the layer, or `null` for a solid line. `null` is
|
|
3239
|
+
* what MapLibre's `setPaintProperty` takes to reset a property, so returning it
|
|
3240
|
+
* (rather than leaving the key out) clears a dash when the user goes back to
|
|
3241
|
+
* solid.
|
|
3242
|
+
*
|
|
3243
|
+
* @param style - The layer style.
|
|
3244
|
+
* @returns The dash pattern, or `null` when the line is solid.
|
|
3245
|
+
*/
|
|
3246
|
+
function strokeDashValue(style) {
|
|
3247
|
+
const dash = styleValue(style, "strokeDash");
|
|
3248
|
+
return isStrokeDashPattern(dash) ? [...dash] : null;
|
|
3249
|
+
}
|
|
3250
|
+
/**
|
|
3251
|
+
* The {@link STROKE_DASH_PRESETS} name matching a pattern, or `"custom"` when
|
|
3252
|
+
* none does. An empty or unusable pattern is `"solid"`.
|
|
3253
|
+
*
|
|
3254
|
+
* @param dash - A dash pattern.
|
|
3255
|
+
* @returns The preset name, or `"custom"`.
|
|
3256
|
+
*/
|
|
3257
|
+
function strokeDashPresetName(dash) {
|
|
3258
|
+
if (!isStrokeDashPattern(dash)) return "solid";
|
|
3259
|
+
for (const [name, pattern] of Object.entries(STROKE_DASH_PRESETS)) if (pattern.length === dash.length && pattern.every((length, index) => length === dash[index])) return name;
|
|
3260
|
+
return "custom";
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* The layer's line blur in pixels; a missing, negative or non-finite value is 0.
|
|
3264
|
+
*
|
|
3265
|
+
* @param style - The layer style.
|
|
3266
|
+
* @returns The blur in pixels.
|
|
3267
|
+
*/
|
|
3268
|
+
function strokeBlurValue(style) {
|
|
3269
|
+
const value = styleValue(style, "strokeBlur");
|
|
3270
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
3271
|
+
}
|
|
3272
|
+
/**
|
|
3273
|
+
* Whether a FeatureCollection carries per-feature simplestyle-spec properties
|
|
3274
|
+
* worth honoring: at least one feature with a valid hex color in a color key
|
|
3275
|
+
* (`fill`/`stroke`/`marker-color`) or a finite number in a numeric key
|
|
3276
|
+
* (`fill-opacity`/`stroke-width`/`stroke-opacity`). The scan is capped so very
|
|
3277
|
+
* large collections do not pay a full pass.
|
|
3278
|
+
*
|
|
3279
|
+
* @param geojson - The collection to inspect (may be undefined).
|
|
3280
|
+
* @returns `true` when simplestyle rendering should be enabled for the layer.
|
|
3281
|
+
*/
|
|
3282
|
+
function hasSimpleStyleProperties(geojson) {
|
|
3283
|
+
const features = geojson?.features;
|
|
3284
|
+
if (!features?.length) return false;
|
|
3285
|
+
const limit = Math.min(features.length, 1e3);
|
|
3286
|
+
for (let index = 0; index < limit; index += 1) {
|
|
3287
|
+
const properties = features[index]?.properties;
|
|
3288
|
+
if (!properties) continue;
|
|
3289
|
+
for (const key of SIMPLE_STYLE_COLOR_KEYS) {
|
|
3290
|
+
const value = properties[key];
|
|
3291
|
+
if (typeof value === "string" && isSimpleStyleColor(value)) return true;
|
|
3292
|
+
}
|
|
3293
|
+
for (const key of SIMPLE_STYLE_NUMBER_KEYS) {
|
|
3294
|
+
const value = properties[key];
|
|
3295
|
+
if (typeof value === "number" && Number.isFinite(value)) return true;
|
|
3296
|
+
}
|
|
3297
|
+
}
|
|
3298
|
+
return false;
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Parses a user-entered MapLibre expression string into an expression array,
|
|
3302
|
+
* tolerating trailing commas. Returns null when the text is empty or not a
|
|
3303
|
+
* JSON array.
|
|
3304
|
+
*/
|
|
3305
|
+
function parseJsonExpression(expression) {
|
|
3306
|
+
const trimmed = expression.trim();
|
|
3307
|
+
if (!trimmed) return null;
|
|
3308
|
+
try {
|
|
3309
|
+
const parsed = JSON.parse(removeTrailingJsonCommas(trimmed));
|
|
3310
|
+
if (!Array.isArray(parsed)) return null;
|
|
3311
|
+
return parsed;
|
|
3312
|
+
} catch {
|
|
3313
|
+
return null;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
3317
|
+
* Builds the data-driven color value for a vector layer's current style mode.
|
|
3318
|
+
* `single` (or any mode that cannot produce a valid expression) returns the
|
|
3319
|
+
* flat fallback color; `categorized` returns a `match` expression, `graduated`
|
|
3320
|
+
* a `step` expression, and `expression` the parsed user expression.
|
|
3321
|
+
*
|
|
3322
|
+
* Graduated stops are class lower bounds (see
|
|
3323
|
+
* {@link createGraduatedClassBreaks}), so the classes are discrete: the map
|
|
3324
|
+
* paints exactly one color per class, which is what the legend (`≥ value`) and
|
|
3325
|
+
* the QML/SLD exporters already describe. Rendering them as a continuous
|
|
3326
|
+
* `interpolate` instead blends a shade per feature, so a 4-class layer draws in
|
|
3327
|
+
* many more than 4 colors and stops matching its own legend (#1384).
|
|
3328
|
+
*
|
|
3329
|
+
* @param style - The layer style.
|
|
3330
|
+
* @param fallbackColor - The flat color used for `single` mode and as the
|
|
3331
|
+
* expression fallback.
|
|
3332
|
+
* @returns A flat color string or a MapLibre color expression.
|
|
3333
|
+
*/
|
|
3334
|
+
function vectorColorExpression(style, fallbackColor) {
|
|
3335
|
+
const mode = styleValue(style, "vectorStyleMode");
|
|
3336
|
+
if (mode === "single") return fallbackColor;
|
|
3337
|
+
if (mode === "expression") return parseJsonExpression(styleValue(style, "vectorStyleExpression")) ?? fallbackColor;
|
|
3338
|
+
if (mode === "rule-based") return ruleBasedColorExpression(style, fallbackColor);
|
|
3339
|
+
const property = styleValue(style, "vectorStyleProperty").trim();
|
|
3340
|
+
if (!property) return fallbackColor;
|
|
3341
|
+
if (mode === "categorized") {
|
|
3342
|
+
const stops = styleValue(style, "vectorStyleStops").filter((stop) => String(stop.value).trim().length > 0 && isHexColor(stop.color));
|
|
3343
|
+
if (stops.length === 0) return fallbackColor;
|
|
3344
|
+
return [
|
|
3345
|
+
"match",
|
|
3346
|
+
["to-string", ["get", property]],
|
|
3347
|
+
...stops.flatMap((stop) => [String(stop.value).trim(), stop.color]),
|
|
3348
|
+
fallbackColor
|
|
3349
|
+
];
|
|
3350
|
+
}
|
|
3351
|
+
const stops = graduatedStops(style);
|
|
3352
|
+
if (stops.length < 2) return fallbackColor;
|
|
3353
|
+
return [
|
|
3354
|
+
"step",
|
|
3355
|
+
[
|
|
3356
|
+
"to-number",
|
|
3357
|
+
["get", property],
|
|
3358
|
+
stops[0].value
|
|
3359
|
+
],
|
|
3360
|
+
stops[0].color,
|
|
3361
|
+
...stops.slice(1).flatMap((stop) => [stop.value, stop.color])
|
|
3362
|
+
];
|
|
3363
|
+
}
|
|
3364
|
+
/**
|
|
3365
|
+
* The graduated stops of a style, cleaned for rendering: non-numeric values and
|
|
3366
|
+
* invalid colors dropped, sorted ascending, and de-duplicated. MapLibre rejects
|
|
3367
|
+
* a `step` expression whose inputs are not strictly ascending, and stops reach
|
|
3368
|
+
* here from hand-edited projects and style imports as well as the Style panel,
|
|
3369
|
+
* so repeats are dropped rather than trusted away.
|
|
3370
|
+
*/
|
|
3371
|
+
function graduatedStops(style) {
|
|
3372
|
+
const sorted = styleValue(style, "vectorStyleStops").map((stop) => ({
|
|
3373
|
+
color: stop.color,
|
|
3374
|
+
value: typeof stop.value === "number" ? stop.value : Number.parseFloat(stop.value)
|
|
3375
|
+
})).filter((stop) => Number.isFinite(stop.value) && isHexColor(stop.color)).sort((a, b) => a.value - b.value);
|
|
3376
|
+
return sorted.filter((stop, index) => index === 0 || stop.value > sorted[index - 1].value);
|
|
3377
|
+
}
|
|
3378
|
+
/** A finite per-rule zoom bound, or undefined when unset/invalid. */
|
|
3379
|
+
function ruleZoom(value) {
|
|
3380
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Resolve the layer's rule tree into the drawable leaf rules (see
|
|
3384
|
+
* {@link EffectiveVectorRule}) plus the catch-all else rule. Disabled rules
|
|
3385
|
+
* (and every descendant of a disabled group), rules with an invalid color or
|
|
3386
|
+
* filter, subtrees under a group whose non-blank filter does not parse, and
|
|
3387
|
+
* rules whose intersected zoom range is empty are all dropped. Cycles in
|
|
3388
|
+
* `parentId` are treated as broken chains and dropped rather than looping.
|
|
3389
|
+
*
|
|
3390
|
+
* @param style - The layer style (reads {@link LayerStyle.vectorRules}).
|
|
3391
|
+
* @returns The ordered drawable rules and the enabled else rule, if any.
|
|
3392
|
+
*/
|
|
3393
|
+
function effectiveVectorRules(style) {
|
|
3394
|
+
const all = styleValue(style, "vectorRules");
|
|
3395
|
+
const elseRule = all.find((rule) => rule.isElse && rule.enabled !== false) ?? null;
|
|
3396
|
+
const byId = /* @__PURE__ */ new Map();
|
|
3397
|
+
for (const rule of all) if (!rule.isElse) byId.set(rule.id, rule);
|
|
3398
|
+
const groupIds = /* @__PURE__ */ new Set();
|
|
3399
|
+
for (const rule of byId.values()) if (rule.parentId && rule.parentId !== rule.id && byId.has(rule.parentId)) groupIds.add(rule.parentId);
|
|
3400
|
+
const rules = [];
|
|
3401
|
+
for (const rule of all) {
|
|
3402
|
+
if (rule.isElse || groupIds.has(rule.id)) continue;
|
|
3403
|
+
if (rule.enabled === false || !isHexColor(rule.color)) continue;
|
|
3404
|
+
const own = parseJsonExpression(rule.filter);
|
|
3405
|
+
if (!own || typeof own[0] !== "string") continue;
|
|
3406
|
+
const filters = [own];
|
|
3407
|
+
let minZoom = ruleZoom(rule.minZoom);
|
|
3408
|
+
let maxZoom = ruleZoom(rule.maxZoom);
|
|
3409
|
+
let dropped = false;
|
|
3410
|
+
const seen = /* @__PURE__ */ new Set([rule.id]);
|
|
3411
|
+
let parent = rule.parentId && rule.parentId !== rule.id ? byId.get(rule.parentId) : void 0;
|
|
3412
|
+
while (parent) {
|
|
3413
|
+
if (seen.has(parent.id) || parent.enabled === false) {
|
|
3414
|
+
dropped = true;
|
|
3415
|
+
break;
|
|
3416
|
+
}
|
|
3417
|
+
seen.add(parent.id);
|
|
3418
|
+
if (parent.filter.trim()) {
|
|
3419
|
+
const parentFilter = parseJsonExpression(parent.filter);
|
|
3420
|
+
if (!parentFilter || typeof parentFilter[0] !== "string") {
|
|
3421
|
+
dropped = true;
|
|
3422
|
+
break;
|
|
3423
|
+
}
|
|
3424
|
+
filters.unshift(parentFilter);
|
|
3425
|
+
}
|
|
3426
|
+
const parentMin = ruleZoom(parent.minZoom);
|
|
3427
|
+
const parentMax = ruleZoom(parent.maxZoom);
|
|
3428
|
+
if (parentMin !== void 0) minZoom = minZoom === void 0 ? parentMin : Math.max(minZoom, parentMin);
|
|
3429
|
+
if (parentMax !== void 0) maxZoom = maxZoom === void 0 ? parentMax : Math.min(maxZoom, parentMax);
|
|
3430
|
+
parent = parent.parentId && parent.parentId !== parent.id ? byId.get(parent.parentId) : void 0;
|
|
3431
|
+
}
|
|
3432
|
+
if (dropped) continue;
|
|
3433
|
+
if (minZoom !== void 0 && maxZoom !== void 0 && minZoom >= maxZoom) continue;
|
|
3434
|
+
rules.push({
|
|
3435
|
+
id: rule.id,
|
|
3436
|
+
label: rule.label,
|
|
3437
|
+
color: rule.color,
|
|
3438
|
+
filter: filters.length === 1 ? filters[0] : ["all", ...filters],
|
|
3439
|
+
...minZoom !== void 0 ? { minZoom } : {},
|
|
3440
|
+
...maxZoom !== void 0 ? { maxZoom } : {},
|
|
3441
|
+
...isHexColor(rule.strokeColor) ? { strokeColor: rule.strokeColor } : {},
|
|
3442
|
+
...typeof rule.strokeWidth === "number" && Number.isFinite(rule.strokeWidth) ? { strokeWidth: rule.strokeWidth } : {},
|
|
3443
|
+
...typeof rule.fillOpacity === "number" && Number.isFinite(rule.fillOpacity) ? { fillOpacity: rule.fillOpacity } : {},
|
|
3444
|
+
...typeof rule.circleRadius === "number" && Number.isFinite(rule.circleRadius) ? { circleRadius: rule.circleRadius } : {}
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
3447
|
+
return {
|
|
3448
|
+
rules,
|
|
3449
|
+
elseRule
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
/**
|
|
3453
|
+
* A rule's match condition as a MapLibre filter: its (ancestor-combined)
|
|
3454
|
+
* attribute filter, with any scale range folded in as `["zoom"]` comparisons.
|
|
3455
|
+
* The zoom window is half-open (`min <= zoom < max`), matching how
|
|
3456
|
+
* {@link zoomWrappedRuleValue} activates rules per zoom segment. MapLibre only
|
|
3457
|
+
* re-evaluates `["zoom"]` in filters at integer zoom levels, so a scale-ranged
|
|
3458
|
+
* rule's features appear/disappear on whole zooms — the paint expressions keep
|
|
3459
|
+
* the fractional-zoom precision.
|
|
3460
|
+
*/
|
|
3461
|
+
function ruleMatchFilter(rule) {
|
|
3462
|
+
const conditions = [rule.filter];
|
|
3463
|
+
if (rule.minZoom !== void 0) conditions.push([
|
|
3464
|
+
">=",
|
|
3465
|
+
["zoom"],
|
|
3466
|
+
rule.minZoom
|
|
3467
|
+
]);
|
|
3468
|
+
if (rule.maxZoom !== void 0) conditions.push([
|
|
3469
|
+
"<",
|
|
3470
|
+
["zoom"],
|
|
3471
|
+
rule.maxZoom
|
|
3472
|
+
]);
|
|
3473
|
+
return conditions.length === 1 ? rule.filter : ["all", ...conditions];
|
|
3474
|
+
}
|
|
3475
|
+
const visibilityFilterCache = /* @__PURE__ */ new WeakMap();
|
|
3476
|
+
/**
|
|
3477
|
+
* The MapLibre filter that hides features matching no rule, for a rule-based
|
|
3478
|
+
* layer whose catch-all else rule has been switched off. Returns `null` — and
|
|
3479
|
+
* callers then leave their geometry filters untouched — unless the layer is in
|
|
3480
|
+
* `"rule-based"` mode AND carries an else record explicitly disabled
|
|
3481
|
+
* (`enabled: false`). An absent else record keeps the historical fallback
|
|
3482
|
+
* rendering, so projects saved before the toggle existed are unaffected.
|
|
3483
|
+
*
|
|
3484
|
+
* With the else rule off, the returned `["any", filter1, filter2, …]` keeps
|
|
3485
|
+
* only features matched by a drawable rule (see {@link effectiveVectorRules});
|
|
3486
|
+
* with no drawable rules it returns the vacuous `["any"]`, which matches
|
|
3487
|
+
* nothing, so every feature hides. Sub-layers combine it with their geometry
|
|
3488
|
+
* filters, which also drops unmatched features from labels and hit-testing —
|
|
3489
|
+
* mirroring QGIS, where features matching no rule are simply not drawn.
|
|
3490
|
+
*
|
|
3491
|
+
* @param style - The layer style (reads {@link LayerStyle.vectorRules}).
|
|
3492
|
+
* @returns The filter to `["all", …]`-combine into each sub-layer, or `null`.
|
|
3493
|
+
*/
|
|
3494
|
+
function ruleBasedVisibilityFilter(style) {
|
|
3495
|
+
if (styleValue(style, "vectorStyleMode") !== "rule-based") return null;
|
|
3496
|
+
const all = styleValue(style, "vectorRules");
|
|
3497
|
+
const cached = visibilityFilterCache.get(all);
|
|
3498
|
+
if (cached !== void 0) return cached;
|
|
3499
|
+
const elseRecord = all.find((rule) => rule.isElse);
|
|
3500
|
+
const filter = !elseRecord || elseRecord.enabled !== false ? null : ["any", ...effectiveVectorRules(style).rules.map(ruleMatchFilter)];
|
|
3501
|
+
visibilityFilterCache.set(all, filter);
|
|
3502
|
+
return filter;
|
|
3503
|
+
}
|
|
3504
|
+
/**
|
|
3505
|
+
* Wrap a per-zoom-segment value builder in a `["step", ["zoom"], …]` expression
|
|
3506
|
+
* when any rule carries a zoom bound. MapLibre only allows `["zoom"]` as the
|
|
3507
|
+
* top-level input of a `step`/`interpolate` in paint properties (not inside a
|
|
3508
|
+
* `case` condition), so per-rule scale ranges compile by splitting the zoom
|
|
3509
|
+
* axis at every rule bound and rebuilding the `case` expression per segment
|
|
3510
|
+
* with only the rules active in that segment. With no zoom bounds the builder
|
|
3511
|
+
* runs once and its plain value is returned unchanged, keeping the historical
|
|
3512
|
+
* `case` shape (which the Mapbox-style importer recognizes).
|
|
3513
|
+
*/
|
|
3514
|
+
function zoomWrappedRuleValue(rules, build) {
|
|
3515
|
+
const bounds = /* @__PURE__ */ new Set();
|
|
3516
|
+
for (const rule of rules) {
|
|
3517
|
+
if (rule.minZoom !== void 0) bounds.add(rule.minZoom);
|
|
3518
|
+
if (rule.maxZoom !== void 0) bounds.add(rule.maxZoom);
|
|
3519
|
+
}
|
|
3520
|
+
if (bounds.size === 0) return build(rules);
|
|
3521
|
+
const breaks = [...bounds].sort((a, b) => a - b);
|
|
3522
|
+
const activeAt = (segmentStart) => rules.filter((rule) => (rule.minZoom ?? -Infinity) <= segmentStart && segmentStart < (rule.maxZoom ?? Infinity));
|
|
3523
|
+
const expression = [
|
|
3524
|
+
"step",
|
|
3525
|
+
["zoom"],
|
|
3526
|
+
build(activeAt(-Infinity))
|
|
3527
|
+
];
|
|
3528
|
+
for (const zoom of breaks) expression.push(zoom, build(activeAt(zoom)));
|
|
3529
|
+
return expression;
|
|
3530
|
+
}
|
|
3531
|
+
/**
|
|
3532
|
+
* Compiles the `"rule-based"` renderer's ordered rules into a MapLibre `case`
|
|
3533
|
+
* color expression: `["case", filter1, color1, filter2, color2, …, elseColor]`.
|
|
3534
|
+
* Rules are evaluated top to bottom; the first matching filter wins. Disabled
|
|
3535
|
+
* rules and rules with an invalid filter JSON or a non-hex color are skipped;
|
|
3536
|
+
* nested rules AND their ancestors' filters (see {@link effectiveVectorRules}).
|
|
3537
|
+
* When any rule carries a zoom range the `case` is rebuilt per zoom segment
|
|
3538
|
+
* inside a `["step", ["zoom"], …]` wrapper. The catch-all (`isElse`) rule
|
|
3539
|
+
* supplies the trailing fallback; when absent or invalid the layer
|
|
3540
|
+
* `fallbackColor` is used. With no usable rules the flat fallback color is
|
|
3541
|
+
* returned.
|
|
3542
|
+
*
|
|
3543
|
+
* @param style - The layer style (reads {@link LayerStyle.vectorRules}).
|
|
3544
|
+
* @param fallbackColor - The color used when no else rule defines one.
|
|
3545
|
+
* @returns A MapLibre expression, or a flat color when no rule applies.
|
|
3546
|
+
*/
|
|
3547
|
+
function ruleBasedColorExpression(style, fallbackColor) {
|
|
3548
|
+
const { rules, elseRule } = effectiveVectorRules(style);
|
|
3549
|
+
const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : fallbackColor;
|
|
3550
|
+
if (rules.length === 0) return elseColor;
|
|
3551
|
+
return zoomWrappedRuleValue(rules, (active) => {
|
|
3552
|
+
if (active.length === 0) return elseColor;
|
|
3553
|
+
return [
|
|
3554
|
+
"case",
|
|
3555
|
+
...active.flatMap((rule) => [rule.filter, rule.color]),
|
|
3556
|
+
elseColor
|
|
3557
|
+
];
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
/**
|
|
3561
|
+
* Build a paint value honoring per-rule symbol overrides for one field: a
|
|
3562
|
+
* `case` expression (zoom-wrapped when rules carry scale ranges) where each
|
|
3563
|
+
* drawable rule's branch yields its override, or the layer fallback when that
|
|
3564
|
+
* rule does not override the field. Returns the fallback unchanged when the
|
|
3565
|
+
* layer is not in rule-based mode or no rule (including the else rule)
|
|
3566
|
+
* overrides the field, so simple styles keep their flat paint values.
|
|
3567
|
+
*
|
|
3568
|
+
* First-match semantics are preserved: every drawable rule contributes a
|
|
3569
|
+
* branch (even without an override) so a feature matched by an earlier rule
|
|
3570
|
+
* never picks up a later rule's override.
|
|
3571
|
+
*/
|
|
3572
|
+
function ruleOverrideValue(style, key, fallback) {
|
|
3573
|
+
if (styleValue(style, "vectorStyleMode") !== "rule-based") return fallback;
|
|
3574
|
+
const { rules, elseRule } = effectiveVectorRules(style);
|
|
3575
|
+
const elseOverride = readRuleOverride(elseRule ?? void 0, key);
|
|
3576
|
+
if (elseOverride === void 0 && !rules.some((rule) => rule[key] !== void 0)) return fallback;
|
|
3577
|
+
const elseValue = elseOverride ?? fallback;
|
|
3578
|
+
return zoomWrappedRuleValue(rules, (active) => {
|
|
3579
|
+
if (active.length === 0) return elseValue;
|
|
3580
|
+
return [
|
|
3581
|
+
"case",
|
|
3582
|
+
...active.flatMap((rule) => [rule.filter, rule[key] ?? fallback]),
|
|
3583
|
+
elseValue
|
|
3584
|
+
];
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
/** A validated override read straight off a (possibly else) {@link VectorRule}. */
|
|
3588
|
+
function readRuleOverride(rule, key) {
|
|
3589
|
+
const value = rule?.[key];
|
|
3590
|
+
if (key === "strokeColor") return isHexColor(value) ? value : void 0;
|
|
3591
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3592
|
+
}
|
|
3593
|
+
/**
|
|
3594
|
+
* The stroke/outline color for polygon outlines and circle strokes, honoring
|
|
3595
|
+
* per-rule {@link VectorRule.strokeColor} overrides in rule-based mode.
|
|
3596
|
+
* Falls back to the flat layer stroke color.
|
|
3597
|
+
*/
|
|
3598
|
+
function vectorOutlineColorValue(style) {
|
|
3599
|
+
return ruleOverrideValue(style, "strokeColor", styleValue(style, "strokeColor"));
|
|
3600
|
+
}
|
|
3601
|
+
/**
|
|
3602
|
+
* A stroke-width paint value honoring per-rule {@link VectorRule.strokeWidth}
|
|
3603
|
+
* overrides in rule-based mode; `fallback` is the layer-level width (a number
|
|
3604
|
+
* or an already-built expression such as the meters-based interpolation).
|
|
3605
|
+
*/
|
|
3606
|
+
function vectorStrokeWidthValue(style, fallback) {
|
|
3607
|
+
return ruleOverrideValue(style, "strokeWidth", fallback);
|
|
3608
|
+
}
|
|
3609
|
+
/**
|
|
3610
|
+
* A fill/circle opacity paint value honoring per-rule
|
|
3611
|
+
* {@link VectorRule.fillOpacity} overrides in rule-based mode; `fallback` is
|
|
3612
|
+
* the layer-level opacity value (number or expression).
|
|
3613
|
+
*/
|
|
3614
|
+
function vectorFillOpacityValue(style, fallback) {
|
|
3615
|
+
return ruleOverrideValue(style, "fillOpacity", fallback);
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* The color-ramp colors a density heatmap paints with.
|
|
3619
|
+
*
|
|
3620
|
+
* {@link getVectorColorRamp} falls back to the *first* ramp ("viridis") for an
|
|
3621
|
+
* unknown name, which is not the heatmap default ("turbo"). Both the map paint
|
|
3622
|
+
* (`heatmapPaint` in `@geolibre/map`) and the auto-legend gradient resolve the
|
|
3623
|
+
* ramp here so a stale or hand-edited `heatmapColorRamp` cannot make the legend
|
|
3624
|
+
* and the rendered heatmap disagree about which ramp is in use.
|
|
3625
|
+
*
|
|
3626
|
+
* @param style - The layer style.
|
|
3627
|
+
* @returns The ramp's colors, or the default ramp's colors for an unknown name.
|
|
3628
|
+
*/
|
|
3629
|
+
function heatmapRampColors(style) {
|
|
3630
|
+
const value = styleValue(style, "heatmapColorRamp");
|
|
3631
|
+
const ramp = getVectorColorRamp(value);
|
|
3632
|
+
return (ramp.value === value ? ramp : getVectorColorRamp(DEFAULT_LAYER_STYLE.heatmapColorRamp)).colors;
|
|
3633
|
+
}
|
|
3634
|
+
/**
|
|
3635
|
+
* The proportional-size configuration, or `null` when proportional sizing
|
|
3636
|
+
* does not apply: disabled, no field chosen, non-finite or degenerate
|
|
3637
|
+
* (`maxValue <= minValue`) value range, or non-finite radii. This is the
|
|
3638
|
+
* single validation chain for every proportional consumer — circle radius,
|
|
3639
|
+
* line width, and marker icon-size (`@geolibre/map`) — so their notion of
|
|
3640
|
+
* "active" can never drift apart.
|
|
3641
|
+
*
|
|
3642
|
+
* @param style - The layer style.
|
|
3643
|
+
* @returns The validated range, or `null` when proportional sizing is off.
|
|
3644
|
+
*/
|
|
3645
|
+
function proportionalSizeRange(style) {
|
|
3646
|
+
if (!styleValue(style, "proportionalSizeEnabled")) return null;
|
|
3647
|
+
return validatedSizeRange(styleValue(style, "proportionalSizeProperty"), styleValue(style, "proportionalSizeMinValue"), styleValue(style, "proportionalSizeMaxValue"), styleValue(style, "proportionalSizeMinRadius"), styleValue(style, "proportionalSizeMaxRadius"));
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* The validation every field-driven size range shares: a chosen field, a
|
|
3651
|
+
* finite non-degenerate value span, and finite radii. Kept in one place so a
|
|
3652
|
+
* future tightening (rejecting negative radii, say) cannot land on the
|
|
3653
|
+
* layer-wide proportional renderer without also landing on the geometry
|
|
3654
|
+
* generator's centroids.
|
|
3655
|
+
*/
|
|
3656
|
+
function validatedSizeRange(rawProperty, minValue, maxValue, minRadius, maxRadius) {
|
|
3657
|
+
const property = rawProperty.trim();
|
|
3658
|
+
if (!property) return null;
|
|
3659
|
+
if (!(Number.isFinite(minValue) && Number.isFinite(maxValue))) return null;
|
|
3660
|
+
if (maxValue <= minValue) return null;
|
|
3661
|
+
if (!(Number.isFinite(minRadius) && Number.isFinite(maxRadius))) return null;
|
|
3662
|
+
return {
|
|
3663
|
+
property,
|
|
3664
|
+
minValue,
|
|
3665
|
+
maxValue,
|
|
3666
|
+
minRadius,
|
|
3667
|
+
maxRadius
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
/**
|
|
3671
|
+
* The proportional `interpolate` expression mapping
|
|
3672
|
+
* `proportionalSizeMinValue..proportionalSizeMaxValue` onto
|
|
3673
|
+
* `proportionalSizeMinRadius..proportionalSizeMaxRadius`, or `null` when
|
|
3674
|
+
* proportional sizing does not apply (see {@link proportionalSizeRange}).
|
|
3675
|
+
*
|
|
3676
|
+
* @param style - The layer style.
|
|
3677
|
+
* @returns The `interpolate` expression, or `null`.
|
|
3678
|
+
*/
|
|
3679
|
+
function proportionalRadiusExpression(style) {
|
|
3680
|
+
const range = proportionalSizeRange(style);
|
|
3681
|
+
return range ? radiusInterpolateExpression(range) : null;
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* The `interpolate` that maps a validated range's value span onto its radius
|
|
3685
|
+
* span. Shared by the layer-wide proportional renderer and the geometry
|
|
3686
|
+
* generator's centroid sizing so both scale identically (linear, with the
|
|
3687
|
+
* value clamped to the range's ends by `interpolate`'s own stop behavior).
|
|
2671
3688
|
*/
|
|
2672
|
-
function
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3689
|
+
function radiusInterpolateExpression(range) {
|
|
3690
|
+
return [
|
|
3691
|
+
"interpolate",
|
|
3692
|
+
["linear"],
|
|
3693
|
+
[
|
|
3694
|
+
"to-number",
|
|
3695
|
+
["get", range.property],
|
|
3696
|
+
range.minValue
|
|
3697
|
+
],
|
|
3698
|
+
range.minValue,
|
|
3699
|
+
Math.max(0, range.minRadius),
|
|
3700
|
+
range.maxValue,
|
|
3701
|
+
Math.max(0, range.maxRadius)
|
|
3702
|
+
];
|
|
2680
3703
|
}
|
|
2681
3704
|
/**
|
|
2682
|
-
*
|
|
2683
|
-
*
|
|
2684
|
-
*
|
|
2685
|
-
*
|
|
2686
|
-
*
|
|
3705
|
+
* The geometry generator's centroid-size configuration, or `null` when the
|
|
3706
|
+
* generated points should all take the flat
|
|
3707
|
+
* {@link LayerStyle.geometryGeneratorCircleRadius}: no field chosen, non-finite
|
|
3708
|
+
* or degenerate (`maxValue <= minValue`) value range, or non-finite radii.
|
|
3709
|
+
* Mirrors {@link proportionalSizeRange} against the generator's own keys.
|
|
3710
|
+
*
|
|
3711
|
+
* @param style - The layer style.
|
|
3712
|
+
* @returns The validated range, or `null` when field-driven sizing is off.
|
|
2687
3713
|
*/
|
|
2688
|
-
function
|
|
2689
|
-
|
|
2690
|
-
if (!trimmed) return { kind: "empty" };
|
|
2691
|
-
const { validation, expression } = compileMapExpression(trimmed, {
|
|
2692
|
-
variables: options.variables,
|
|
2693
|
-
expectedType: options.expectedType
|
|
2694
|
-
});
|
|
2695
|
-
if (!validation.ok || !expression) return {
|
|
2696
|
-
kind: "error",
|
|
2697
|
-
errors: validation.errors
|
|
2698
|
-
};
|
|
2699
|
-
const feature = options.feature ?? null;
|
|
2700
|
-
const geometryType = feature?.geometry?.type ?? "Unknown";
|
|
2701
|
-
try {
|
|
2702
|
-
return {
|
|
2703
|
-
kind: "value",
|
|
2704
|
-
value: expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {
|
|
2705
|
-
type: geometryType,
|
|
2706
|
-
properties: feature?.properties ?? {},
|
|
2707
|
-
...feature?.id !== void 0 ? { id: feature.id } : {},
|
|
2708
|
-
geometry: feature?.geometry
|
|
2709
|
-
})
|
|
2710
|
-
};
|
|
2711
|
-
} catch (error) {
|
|
2712
|
-
return {
|
|
2713
|
-
kind: "error",
|
|
2714
|
-
errors: [error instanceof Error ? error.message : String(error)]
|
|
2715
|
-
};
|
|
2716
|
-
}
|
|
3714
|
+
function generatorSizeRange(style) {
|
|
3715
|
+
return validatedSizeRange(styleValue(style, "geometryGeneratorSizeProperty"), styleValue(style, "geometryGeneratorSizeMinValue"), styleValue(style, "geometryGeneratorSizeMaxValue"), styleValue(style, "geometryGeneratorSizeMinRadius"), styleValue(style, "geometryGeneratorSizeMaxRadius"));
|
|
2717
3716
|
}
|
|
2718
3717
|
/**
|
|
2719
|
-
*
|
|
2720
|
-
*
|
|
2721
|
-
*
|
|
2722
|
-
*
|
|
2723
|
-
*
|
|
3718
|
+
* Builds the `circle-radius` paint value for the geometry generator's derived
|
|
3719
|
+
* centroid points: an `interpolate` over
|
|
3720
|
+
* {@link LayerStyle.geometryGeneratorSizeProperty} when field-driven sizing
|
|
3721
|
+
* applies (see {@link generatorSizeRange}), otherwise the flat
|
|
3722
|
+
* {@link LayerStyle.geometryGeneratorCircleRadius}.
|
|
3723
|
+
*
|
|
3724
|
+
* The flat fallback keeps the 1px floor the generator has always applied; the
|
|
3725
|
+
* interpolate is left as authored, so a 0px min radius still hides the
|
|
3726
|
+
* smallest symbols on purpose.
|
|
3727
|
+
*
|
|
3728
|
+
* @param style - The layer style.
|
|
3729
|
+
* @returns A constant radius (pixels) or a MapLibre `interpolate` expression.
|
|
2724
3730
|
*/
|
|
2725
|
-
function
|
|
2726
|
-
const
|
|
2727
|
-
if (
|
|
2728
|
-
|
|
2729
|
-
errors: []
|
|
2730
|
-
};
|
|
2731
|
-
const { validation, expression } = compileMapExpression(trimmed, {
|
|
2732
|
-
variables: options.variables,
|
|
2733
|
-
expectedType: options.expectedType
|
|
2734
|
-
});
|
|
2735
|
-
if (!validation.ok || !expression) return {
|
|
2736
|
-
ok: false,
|
|
2737
|
-
errors: validation.errors
|
|
2738
|
-
};
|
|
2739
|
-
const compiledZoom = options.zoom ?? 0;
|
|
2740
|
-
return {
|
|
2741
|
-
ok: true,
|
|
2742
|
-
errors: [],
|
|
2743
|
-
evaluate: (feature, zoom = compiledZoom) => expression.evaluateWithoutErrorHandling({ zoom }, {
|
|
2744
|
-
type: feature.geometry?.type ?? "Unknown",
|
|
2745
|
-
properties: feature.properties ?? {},
|
|
2746
|
-
...feature.id !== void 0 ? { id: feature.id } : {},
|
|
2747
|
-
geometry: feature.geometry
|
|
2748
|
-
})
|
|
2749
|
-
};
|
|
3731
|
+
function generatorCircleRadiusValue(style) {
|
|
3732
|
+
const range = generatorSizeRange(style);
|
|
3733
|
+
if (range) return radiusInterpolateExpression(range);
|
|
3734
|
+
return Math.max(1, styleValue(style, "geometryGeneratorCircleRadius"));
|
|
2750
3735
|
}
|
|
2751
3736
|
/**
|
|
2752
|
-
*
|
|
2753
|
-
*
|
|
2754
|
-
*
|
|
2755
|
-
*
|
|
2756
|
-
*
|
|
2757
|
-
*
|
|
3737
|
+
* Builds the `circle-radius` paint value, honoring proportional (graduated)
|
|
3738
|
+
* symbol sizing. When {@link LayerStyle.proportionalSizeEnabled} is set with a
|
|
3739
|
+
* chosen numeric field and a valid value range, returns an `interpolate` that
|
|
3740
|
+
* maps `proportionalSizeMinValue..proportionalSizeMaxValue` onto
|
|
3741
|
+
* `proportionalSizeMinRadius..proportionalSizeMaxRadius`; otherwise the constant
|
|
3742
|
+
* {@link LayerStyle.circleRadius}.
|
|
3743
|
+
*
|
|
3744
|
+
* @param style - The layer style.
|
|
3745
|
+
* @returns A constant radius (pixels) or a MapLibre `interpolate` expression.
|
|
2758
3746
|
*/
|
|
2759
|
-
function
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
expectedType: "boolean"
|
|
2770
|
-
});
|
|
2771
|
-
if (!validation.ok || !expression) return {
|
|
2772
|
-
ok: false,
|
|
2773
|
-
errors: validation.errors,
|
|
2774
|
-
ids: [],
|
|
2775
|
-
errorCount: 0
|
|
2776
|
-
};
|
|
2777
|
-
const ids = [];
|
|
2778
|
-
let errorCount = 0;
|
|
2779
|
-
features.forEach((feature, index) => {
|
|
2780
|
-
try {
|
|
2781
|
-
if (expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {
|
|
2782
|
-
type: feature.geometry?.type ?? "Unknown",
|
|
2783
|
-
properties: feature.properties ?? {},
|
|
2784
|
-
...feature.id !== void 0 ? { id: feature.id } : {},
|
|
2785
|
-
geometry: feature.geometry
|
|
2786
|
-
}) === true) ids.push(featureSelectionId(feature, index));
|
|
2787
|
-
} catch {
|
|
2788
|
-
errorCount += 1;
|
|
2789
|
-
}
|
|
2790
|
-
});
|
|
2791
|
-
return {
|
|
2792
|
-
ok: true,
|
|
2793
|
-
errors: [],
|
|
2794
|
-
ids,
|
|
2795
|
-
errorCount
|
|
2796
|
-
};
|
|
3747
|
+
function circleRadiusValue(style) {
|
|
3748
|
+
return ruleOverrideValue(style, "circleRadius", baseCircleRadiusValue(style));
|
|
3749
|
+
}
|
|
3750
|
+
/** The layer-level circle radius before per-rule overrides. */
|
|
3751
|
+
function baseCircleRadiusValue(style) {
|
|
3752
|
+
return proportionalRadiusExpression(style) ?? styleValue(style, "circleRadius");
|
|
3753
|
+
}
|
|
3754
|
+
/** Fill color value for a polygon layer (fallback: the layer fill color). */
|
|
3755
|
+
function vectorFillColorValue(style) {
|
|
3756
|
+
return withSimpleStyleColor(style, "fill", vectorColorExpression(style, styleValue(style, "fillColor")));
|
|
2797
3757
|
}
|
|
2798
3758
|
/**
|
|
2799
|
-
*
|
|
2800
|
-
*
|
|
2801
|
-
*
|
|
2802
|
-
*
|
|
2803
|
-
*
|
|
2804
|
-
* `["properties"]`) is never misclassified as a color.
|
|
3759
|
+
* Circle color value for a point layer. Intentionally identical to
|
|
3760
|
+
* `vectorFillColorValue`: GeoLibre has no separate point-fill color, so point
|
|
3761
|
+
* circles share the polygon fill color (matching `circlePaint` in the map
|
|
3762
|
+
* package). Kept as its own function so the per-geometry callers read in
|
|
3763
|
+
* parallel and a future dedicated circle color stays a one-line change here.
|
|
2805
3764
|
*/
|
|
2806
|
-
function
|
|
2807
|
-
return
|
|
3765
|
+
function vectorCircleColorValue(style) {
|
|
3766
|
+
return withSimpleStyleColor(style, "marker-color", vectorColorExpression(style, styleValue(style, "fillColor")));
|
|
2808
3767
|
}
|
|
2809
3768
|
/**
|
|
2810
|
-
*
|
|
2811
|
-
*
|
|
2812
|
-
*
|
|
3769
|
+
* Line color value for line geometry and polygon outlines (fallback: the
|
|
3770
|
+
* layer stroke color). For non-`expression` modes the data-driven color is
|
|
3771
|
+
* applied to line geometry only, while polygon outlines keep the flat stroke
|
|
3772
|
+
* color, matching the polygon-fill-only behavior of categorized/graduated
|
|
3773
|
+
* styling.
|
|
2813
3774
|
*/
|
|
2814
|
-
function
|
|
2815
|
-
|
|
2816
|
-
if (
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
3775
|
+
function vectorLineColorValue(style) {
|
|
3776
|
+
const strokeColor = styleValue(style, "strokeColor");
|
|
3777
|
+
if (styleValue(style, "vectorStyleMode") === "rule-based") return withSimpleStyleColor(style, "stroke", ruleBasedLineColorExpression(style));
|
|
3778
|
+
const vectorColor = vectorColorExpression(style, strokeColor);
|
|
3779
|
+
return withSimpleStyleColor(style, "stroke", vectorColor === strokeColor ? strokeColor : styleValue(style, "vectorStyleMode") === "expression" ? vectorColor : [
|
|
3780
|
+
"case",
|
|
3781
|
+
[
|
|
3782
|
+
"==",
|
|
3783
|
+
["geometry-type"],
|
|
3784
|
+
"Polygon"
|
|
3785
|
+
],
|
|
3786
|
+
strokeColor,
|
|
3787
|
+
vectorColor
|
|
3788
|
+
]);
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* The line color for the rule-based renderer: line geometry takes the rule
|
|
3792
|
+
* color, polygon outlines take the per-rule stroke color override (falling
|
|
3793
|
+
* back to the flat layer stroke). Both channels are built inside one shared
|
|
3794
|
+
* zoom wrapper, since composing two independently zoom-stepped expressions in
|
|
3795
|
+
* a single `case` would nest `["zoom"]` below the top level, which MapLibre
|
|
3796
|
+
* rejects.
|
|
3797
|
+
*/
|
|
3798
|
+
function ruleBasedLineColorExpression(style) {
|
|
3799
|
+
const strokeColor = styleValue(style, "strokeColor");
|
|
3800
|
+
const { rules, elseRule } = effectiveVectorRules(style);
|
|
3801
|
+
const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : strokeColor;
|
|
3802
|
+
const elseOutline = readRuleOverride(elseRule ?? void 0, "strokeColor") ?? strokeColor;
|
|
3803
|
+
const hasOutlineOverride = rules.some((rule) => rule.strokeColor !== void 0) || elseOutline !== strokeColor;
|
|
3804
|
+
if (rules.length === 0 && elseColor === strokeColor && !hasOutlineOverride) return strokeColor;
|
|
3805
|
+
return zoomWrappedRuleValue(rules, (active) => {
|
|
3806
|
+
const colorValue = active.length === 0 ? elseColor : [
|
|
3807
|
+
"case",
|
|
3808
|
+
...active.flatMap((rule) => [rule.filter, rule.color]),
|
|
3809
|
+
elseColor
|
|
3810
|
+
];
|
|
3811
|
+
const outlineValue = !hasOutlineOverride ? strokeColor : active.length === 0 ? elseOutline : [
|
|
3812
|
+
"case",
|
|
3813
|
+
...active.flatMap((rule) => [rule.filter, rule.strokeColor ?? strokeColor]),
|
|
3814
|
+
elseOutline
|
|
3815
|
+
];
|
|
3816
|
+
if (colorValue === strokeColor && outlineValue === strokeColor) return strokeColor;
|
|
3817
|
+
return [
|
|
3818
|
+
"case",
|
|
3819
|
+
[
|
|
3820
|
+
"==",
|
|
3821
|
+
["geometry-type"],
|
|
3822
|
+
"Polygon"
|
|
3823
|
+
],
|
|
3824
|
+
outlineValue,
|
|
3825
|
+
colorValue
|
|
3826
|
+
];
|
|
3827
|
+
});
|
|
3828
|
+
}
|
|
3829
|
+
/**
|
|
3830
|
+
* Resolves the 3D-extrusion height (the roof's altitude) for a layer style
|
|
3831
|
+
* into a MapLibre value: a plain meters number, or a data-driven expression.
|
|
3832
|
+
* With a positive `extrusionFloorHeight` the roof sits that far above the
|
|
3833
|
+
* base (`extrusionBaseValue`, clamped to the ground as MapLibre clamps
|
|
3834
|
+
* `fill-extrusion-base`), whatever the height settings say. Otherwise, in
|
|
3835
|
+
* advanced mode a valid `extrusionHeightExpression` wins; else the height is
|
|
3836
|
+
* the chosen property scaled by `extrusionHeightScale` (`["*", ["to-number",
|
|
3837
|
+
* ["get", prop], 0], scale]`), or a flat `0` when no property is set (so the
|
|
3838
|
+
* layer renders flat rather than erroring). Shared by the map's
|
|
3839
|
+
* fill-extrusion paint and the Add Vector Layer control's extrusion mapping
|
|
3840
|
+
* so both extrude identically.
|
|
3841
|
+
*
|
|
3842
|
+
* @param style - The layer style.
|
|
3843
|
+
* @returns The extrusion height as a number or a MapLibre expression array.
|
|
3844
|
+
*/
|
|
3845
|
+
function extrusionHeightValue(style) {
|
|
3846
|
+
const floorHeight = extrusionFloorHeightValue(style);
|
|
3847
|
+
if (floorHeight > 0) {
|
|
3848
|
+
const base = extrusionBaseValue(style);
|
|
3849
|
+
return typeof base === "number" ? Math.max(0, base) + floorHeight : [
|
|
3850
|
+
"+",
|
|
3851
|
+
[
|
|
3852
|
+
"max",
|
|
3853
|
+
0,
|
|
3854
|
+
base
|
|
3855
|
+
],
|
|
3856
|
+
floorHeight
|
|
3857
|
+
];
|
|
2826
3858
|
}
|
|
3859
|
+
const advancedExpression = parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionHeightExpression") : "");
|
|
3860
|
+
if (advancedExpression) return advancedExpression;
|
|
3861
|
+
const property = styleValue(style, "extrusionHeightProperty").trim();
|
|
3862
|
+
if (!property) return 0;
|
|
3863
|
+
const scale = styleValue(style, "extrusionHeightScale");
|
|
3864
|
+
return [
|
|
3865
|
+
"*",
|
|
3866
|
+
[
|
|
3867
|
+
"to-number",
|
|
3868
|
+
["get", property],
|
|
3869
|
+
0
|
|
3870
|
+
],
|
|
3871
|
+
scale
|
|
3872
|
+
];
|
|
2827
3873
|
}
|
|
2828
3874
|
/**
|
|
2829
|
-
*
|
|
2830
|
-
*
|
|
2831
|
-
*
|
|
3875
|
+
* The layer's floor height in metres: a finite `extrusionFloorHeight` above
|
|
3876
|
+
* 0, else 0 (the height is then the roof's altitude). Also 0 for a project
|
|
3877
|
+
* saved before the field existed.
|
|
3878
|
+
*
|
|
3879
|
+
* @param style - The layer style.
|
|
3880
|
+
* @returns The floor height, or 0 when it is off.
|
|
3881
|
+
*/
|
|
3882
|
+
function extrusionFloorHeightValue(style) {
|
|
3883
|
+
const value = styleValue(style, "extrusionFloorHeight");
|
|
3884
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
3885
|
+
}
|
|
3886
|
+
/**
|
|
3887
|
+
* Resolves the 3D-extrusion base for a layer style into a MapLibre value: a
|
|
3888
|
+
* plain meters number, or a data-driven expression. In advanced mode a valid
|
|
3889
|
+
* `extrusionBaseExpression` wins; otherwise, with `extrusionBaseProperty` set,
|
|
3890
|
+
* the base is `extrusionBase` plus the property scaled by `extrusionBaseScale`
|
|
3891
|
+
* (so floors stack from a `level` field), else the flat `extrusionBase`. Like
|
|
3892
|
+
* {@link extrusionHeightValue} it is an altitude: MapLibre draws a feature
|
|
3893
|
+
* only where its height is above its base.
|
|
3894
|
+
*
|
|
3895
|
+
* @param style - The layer style.
|
|
3896
|
+
* @returns The extrusion base as a number or a MapLibre expression array.
|
|
3897
|
+
*/
|
|
3898
|
+
function extrusionBaseValue(style) {
|
|
3899
|
+
const advancedExpression = parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionBaseExpression") : "");
|
|
3900
|
+
if (advancedExpression) return advancedExpression;
|
|
3901
|
+
const rawOffset = styleValue(style, "extrusionBase");
|
|
3902
|
+
const offset = Number.isFinite(rawOffset) ? rawOffset : 0;
|
|
3903
|
+
const property = styleValue(style, "extrusionBaseProperty").trim();
|
|
3904
|
+
if (!property) return offset;
|
|
3905
|
+
const scale = styleValue(style, "extrusionBaseScale");
|
|
3906
|
+
const scaled = [
|
|
3907
|
+
"*",
|
|
3908
|
+
[
|
|
3909
|
+
"to-number",
|
|
3910
|
+
["get", property],
|
|
3911
|
+
0
|
|
3912
|
+
],
|
|
3913
|
+
Number.isFinite(scale) ? scale : 1
|
|
3914
|
+
];
|
|
3915
|
+
return offset === 0 ? scaled : [
|
|
3916
|
+
"+",
|
|
3917
|
+
scaled,
|
|
3918
|
+
offset
|
|
3919
|
+
];
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* Resolves the 3D-extrusion color for a layer style: a data-driven expression
|
|
3923
|
+
* when the layer's symbology mode produces one (categorized/graduated/rule/
|
|
3924
|
+
* expression) or an advanced `extrusionColorExpression` is set, otherwise the
|
|
3925
|
+
* flat `extrusionColor`. Mirrors the fill-color contract so an extruded layer
|
|
3926
|
+
* honors the same attribute-driven styling.
|
|
3927
|
+
*
|
|
3928
|
+
* @param style - The layer style.
|
|
3929
|
+
* @returns A flat color string or a MapLibre color expression array.
|
|
2832
3930
|
*/
|
|
2833
|
-
function
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
if (!properties) continue;
|
|
2839
|
-
for (const name of names) {
|
|
2840
|
-
const value = properties[name];
|
|
2841
|
-
if (value === null || value === void 0) continue;
|
|
2842
|
-
const valueType = typeof value;
|
|
2843
|
-
const fieldType = valueType === "string" || valueType === "number" || valueType === "boolean" ? valueType : "mixed";
|
|
2844
|
-
if (result[name] === "unknown") result[name] = fieldType;
|
|
2845
|
-
else if (result[name] !== fieldType) result[name] = "mixed";
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
return result;
|
|
3931
|
+
function extrusionColorValue(style) {
|
|
3932
|
+
const flat = styleValue(style, "extrusionColor");
|
|
3933
|
+
const vectorExpression = vectorColorExpression(style, flat);
|
|
3934
|
+
if (vectorExpression !== flat) return vectorExpression;
|
|
3935
|
+
return parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionColorExpression") : "") ?? flat;
|
|
2849
3936
|
}
|
|
2850
3937
|
// src/layer-descriptive-metadata.ts
|
|
2851
3938
|
/** STAC specification version the Item export targets. */
|
|
@@ -4669,6 +5756,8 @@ const DEFAULT_PRINT_LAYOUT = {
|
|
|
4669
5756
|
customHeight: 720,
|
|
4670
5757
|
customUnit: "px",
|
|
4671
5758
|
pageMargin: "normal",
|
|
5759
|
+
pageBackground: "#ffffff",
|
|
5760
|
+
panelTheme: "light",
|
|
4672
5761
|
showPageBorder: false,
|
|
4673
5762
|
pageBorderColor: "#111827",
|
|
4674
5763
|
pageBorderWidth: 2,
|
|
@@ -4758,6 +5847,10 @@ function createDefaultPrintLayout() {
|
|
|
4758
5847
|
function str(value, fallback) {
|
|
4759
5848
|
return typeof value === "string" ? value : fallback;
|
|
4760
5849
|
}
|
|
5850
|
+
/** A `#RGB` / `#RRGGBB` colour, or `fallback` for anything else. */
|
|
5851
|
+
function hexColor(value, fallback) {
|
|
5852
|
+
return typeof value === "string" && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim()) ? value.trim() : fallback;
|
|
5853
|
+
}
|
|
4761
5854
|
function bool(value, fallback) {
|
|
4762
5855
|
return typeof value === "boolean" ? value : fallback;
|
|
4763
5856
|
}
|
|
@@ -4859,6 +5952,8 @@ function normalizePrintLayoutConfig(value) {
|
|
|
4859
5952
|
"narrow",
|
|
4860
5953
|
"none"
|
|
4861
5954
|
], d.pageMargin),
|
|
5955
|
+
pageBackground: hexColor(raw.pageBackground, d.pageBackground),
|
|
5956
|
+
panelTheme: oneOf(raw.panelTheme, ["light", "dark"], d.panelTheme),
|
|
4862
5957
|
showPageBorder: bool(raw.showPageBorder, d.showPageBorder),
|
|
4863
5958
|
pageBorderColor: str(raw.pageBorderColor, d.pageBorderColor),
|
|
4864
5959
|
pageBorderWidth: num(raw.pageBorderWidth, d.pageBorderWidth, 1, 20),
|
|
@@ -5047,6 +6142,9 @@ const SYMBOL_STYLE_KEYS = [
|
|
|
5047
6142
|
"strokeColor",
|
|
5048
6143
|
"strokeWidth",
|
|
5049
6144
|
"strokeWidthUnit",
|
|
6145
|
+
"strokeOpacity",
|
|
6146
|
+
"strokeDash",
|
|
6147
|
+
"strokeBlur",
|
|
5050
6148
|
"fillOpacity",
|
|
5051
6149
|
"circleRadius",
|
|
5052
6150
|
"fillPattern",
|
|
@@ -5246,7 +6344,9 @@ function sanitizeLayerStylePatch(value) {
|
|
|
5246
6344
|
...circleRadius !== void 0 ? { circleRadius } : {}
|
|
5247
6345
|
}];
|
|
5248
6346
|
});
|
|
5249
|
-
else
|
|
6347
|
+
else if (key === "strokeDash") {
|
|
6348
|
+
if (given.length === 0 || isStrokeDashPattern(given)) out[key] = given;
|
|
6349
|
+
} else out[key] = given;
|
|
5250
6350
|
continue;
|
|
5251
6351
|
}
|
|
5252
6352
|
if (typeof given !== typeof fallback) continue;
|
|
@@ -6524,6 +7624,7 @@ function redactUrlCredentials(value) {
|
|
|
6524
7624
|
}
|
|
6525
7625
|
const keptQuery = query === void 0 ? void 0 : redactParameterString(query);
|
|
6526
7626
|
const keptFragment = fragment === void 0 || !fragment.includes("=") ? fragment : redactParameterString(fragment);
|
|
7627
|
+
if (base === (queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex)) && keptQuery === query && keptFragment === fragment) return value;
|
|
6527
7628
|
return base + (keptQuery ? `?${keptQuery}` : "") + (keptFragment ? `#${keptFragment}` : "");
|
|
6528
7629
|
}
|
|
6529
7630
|
function isPlainObject$2(value) {
|
|
@@ -6648,13 +7749,17 @@ function redactProjectCredentials(project) {
|
|
|
6648
7749
|
const mapboxStyleUrl = project.preferences?.map?.mapboxStyleUrl;
|
|
6649
7750
|
const redactedMapboxStyleUrl = typeof mapboxStyleUrl === "string" ? redactUrlCredentials(mapboxStyleUrl) : mapboxStyleUrl;
|
|
6650
7751
|
if (redactedMapboxStyleUrl !== mapboxStyleUrl) recordRedaction(accumulator, "preferences.map.mapboxStyleUrl", mapboxStyleUrl);
|
|
7752
|
+
const terrainSource = project.preferences?.map?.terrainSource;
|
|
7753
|
+
const redactedTerrainSource = typeof terrainSource === "string" ? redactUrlCredentials(terrainSource) : terrainSource;
|
|
7754
|
+
if (redactedTerrainSource !== terrainSource) recordRedaction(accumulator, "preferences.map.terrainSource", terrainSource);
|
|
6651
7755
|
const preferences = project.preferences ? {
|
|
6652
7756
|
...project.preferences,
|
|
6653
7757
|
environmentVariables: (project.preferences.environmentVariables ?? []).filter((variable) => variable.secret === false),
|
|
6654
7758
|
geocoding,
|
|
6655
|
-
...redactedMapboxStyleUrl !== mapboxStyleUrl ? { map: {
|
|
7759
|
+
...redactedMapboxStyleUrl !== mapboxStyleUrl || redactedTerrainSource !== terrainSource ? { map: {
|
|
6656
7760
|
...project.preferences.map,
|
|
6657
|
-
mapboxStyleUrl: redactedMapboxStyleUrl
|
|
7761
|
+
mapboxStyleUrl: redactedMapboxStyleUrl,
|
|
7762
|
+
terrainSource: redactedTerrainSource
|
|
6658
7763
|
} } : {}
|
|
6659
7764
|
} : project.preferences;
|
|
6660
7765
|
const populatedEnvironmentVariables = project.preferences?.environmentVariables?.filter((variable) => variable.secret !== false && variable.value !== "") ?? [];
|
|
@@ -6846,6 +7951,50 @@ var GeocoderUnsupportedError = class extends Error {
|
|
|
6846
7951
|
this.name = "GeocoderUnsupportedError";
|
|
6847
7952
|
}
|
|
6848
7953
|
};
|
|
7954
|
+
/**
|
|
7955
|
+
* Whether a raw coordinate is a number or a numeric-looking string
|
|
7956
|
+
* (Nominatim's form). `Number()` would also turn `null`, `true`, a blank string
|
|
7957
|
+
* or a one-element array into a coordinate, so those are rejected first.
|
|
7958
|
+
*/
|
|
7959
|
+
function isCoordinateValue(value) {
|
|
7960
|
+
return typeof value === "number" || typeof value === "string" && value.trim() !== "";
|
|
7961
|
+
}
|
|
7962
|
+
/**
|
|
7963
|
+
* Validate a west/south/east/north extent: every edge finite and on Earth,
|
|
7964
|
+
* south not above north. Returns null otherwise, so a malformed box falls back
|
|
7965
|
+
* to the match's point instead of sending the camera somewhere odd.
|
|
7966
|
+
*
|
|
7967
|
+
* @param west - Western longitude.
|
|
7968
|
+
* @param south - Southern latitude.
|
|
7969
|
+
* @param east - Eastern longitude.
|
|
7970
|
+
* @param north - Northern latitude.
|
|
7971
|
+
* @returns The extent, or null when it is not a usable one.
|
|
7972
|
+
*/
|
|
7973
|
+
function normalizeGeocodeBbox(west, south, east, north) {
|
|
7974
|
+
if (![
|
|
7975
|
+
west,
|
|
7976
|
+
south,
|
|
7977
|
+
east,
|
|
7978
|
+
north
|
|
7979
|
+
].every(isCoordinateValue)) return null;
|
|
7980
|
+
const box = [
|
|
7981
|
+
Number(west),
|
|
7982
|
+
Number(south),
|
|
7983
|
+
Number(east),
|
|
7984
|
+
Number(north)
|
|
7985
|
+
];
|
|
7986
|
+
if (!box.every(Number.isFinite)) return null;
|
|
7987
|
+
const [w, s, e, n] = box;
|
|
7988
|
+
if (w < -180 || w > 180 || e < -180 || e > 180) return null;
|
|
7989
|
+
if (s < -90 || n > 90 || s > n) return null;
|
|
7990
|
+
return box;
|
|
7991
|
+
}
|
|
7992
|
+
/** A GeoJSON feature's own `bbox` member (`[west, south, east, north]`), or null. */
|
|
7993
|
+
function geojsonFeatureBbox(feature) {
|
|
7994
|
+
const bbox = feature.bbox;
|
|
7995
|
+
if (!Array.isArray(bbox) || bbox.length !== 4) return null;
|
|
7996
|
+
return normalizeGeocodeBbox(bbox[0], bbox[1], bbox[2], bbox[3]);
|
|
7997
|
+
}
|
|
6849
7998
|
function coerceScore(value) {
|
|
6850
7999
|
if (value === void 0 || value === null || value === "") return null;
|
|
6851
8000
|
const num = Number(value);
|
|
@@ -6905,11 +8054,14 @@ function nominatimForwardResultToMatch(result) {
|
|
|
6905
8054
|
const lat = Number(result.lat);
|
|
6906
8055
|
const lon = Number(result.lon);
|
|
6907
8056
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
|
8057
|
+
const box = Array.isArray(result.boundingbox) ? result.boundingbox : null;
|
|
8058
|
+
const bbox = box?.length === 4 ? normalizeGeocodeBbox(box[2], box[0], box[3], box[1]) : null;
|
|
6908
8059
|
return {
|
|
6909
8060
|
lat,
|
|
6910
8061
|
lon,
|
|
6911
8062
|
displayName: result.display_name ?? "",
|
|
6912
|
-
score: coerceScore(result.importance)
|
|
8063
|
+
score: coerceScore(result.importance),
|
|
8064
|
+
...bbox ? { bbox } : {}
|
|
6913
8065
|
};
|
|
6914
8066
|
}
|
|
6915
8067
|
/**
|
|
@@ -7049,11 +8201,13 @@ const peliasProvider = {
|
|
|
7049
8201
|
const coords = pointCoords(feature);
|
|
7050
8202
|
if (!coords) return null;
|
|
7051
8203
|
const props = feature.properties ?? {};
|
|
8204
|
+
const bbox = geojsonFeatureBbox(feature);
|
|
7052
8205
|
return {
|
|
7053
8206
|
lon: coords[0],
|
|
7054
8207
|
lat: coords[1],
|
|
7055
8208
|
displayName: readString(props, "label") ?? "",
|
|
7056
|
-
score: coerceScore(props.confidence)
|
|
8209
|
+
score: coerceScore(props.confidence),
|
|
8210
|
+
...bbox ? { bbox } : {}
|
|
7057
8211
|
};
|
|
7058
8212
|
}).filter((m) => m !== null),
|
|
7059
8213
|
buildReverseUrl: (config, lon, lat) => {
|
|
@@ -7155,6 +8309,33 @@ function arcgisCandidatePosition(x, y, wkid) {
|
|
|
7155
8309
|
};
|
|
7156
8310
|
}
|
|
7157
8311
|
/**
|
|
8312
|
+
* A candidate's `extent` (`xmin`/`ymin`/`xmax`/`ymax`, in the response's
|
|
8313
|
+
* spatial reference) as a west/south/east/north box in degrees, or null when it
|
|
8314
|
+
* is missing, malformed, or in a spatial reference that cannot be unprojected.
|
|
8315
|
+
*
|
|
8316
|
+
* @param extent - The candidate's `extent` member.
|
|
8317
|
+
* @param wkid - The response's spatial reference, or null when it reports none.
|
|
8318
|
+
* @returns The extent in degrees, or null.
|
|
8319
|
+
*/
|
|
8320
|
+
function arcgisCandidateExtent(extent, wkid) {
|
|
8321
|
+
if (!extent || typeof extent !== "object") return null;
|
|
8322
|
+
const e = extent;
|
|
8323
|
+
if (![
|
|
8324
|
+
e.xmin,
|
|
8325
|
+
e.ymin,
|
|
8326
|
+
e.xmax,
|
|
8327
|
+
e.ymax
|
|
8328
|
+
].every(isCoordinateValue)) return null;
|
|
8329
|
+
try {
|
|
8330
|
+
const sw = arcgisCandidatePosition(Number(e.xmin), Number(e.ymin), wkid);
|
|
8331
|
+
const ne = arcgisCandidatePosition(Number(e.xmax), Number(e.ymax), wkid);
|
|
8332
|
+
if (!sw || !ne) return null;
|
|
8333
|
+
return normalizeGeocodeBbox(sw.lon, sw.lat, ne.lon, ne.lat);
|
|
8334
|
+
} catch {
|
|
8335
|
+
return null;
|
|
8336
|
+
}
|
|
8337
|
+
}
|
|
8338
|
+
/**
|
|
7158
8339
|
* A candidate's attributes worth keeping: plain values that are not empty.
|
|
7159
8340
|
* The shape and the location fields the match already carries are dropped.
|
|
7160
8341
|
*/
|
|
@@ -7188,11 +8369,13 @@ function parseArcGISCandidates(data) {
|
|
|
7188
8369
|
const position = arcgisCandidatePosition(Number(location?.x), Number(location?.y), wkid);
|
|
7189
8370
|
if (!position) return null;
|
|
7190
8371
|
const attributes = candidateAttributes(candidate.attributes);
|
|
8372
|
+
const bbox = arcgisCandidateExtent(candidate.extent, wkid);
|
|
7191
8373
|
return {
|
|
7192
8374
|
...position,
|
|
7193
8375
|
displayName: readString(candidate, "address") ?? "",
|
|
7194
8376
|
score: coerceScore(candidate.score),
|
|
7195
|
-
...attributes ? { attributes } : {}
|
|
8377
|
+
...attributes ? { attributes } : {},
|
|
8378
|
+
...bbox ? { bbox } : {}
|
|
7196
8379
|
};
|
|
7197
8380
|
}).filter((m) => m !== null);
|
|
7198
8381
|
}
|
|
@@ -7397,11 +8580,13 @@ const mapboxProvider = {
|
|
|
7397
8580
|
}
|
|
7398
8581
|
coords ??= pointCoords(feature);
|
|
7399
8582
|
if (!coords) return null;
|
|
8583
|
+
const bbox = geojsonFeatureBbox(feature);
|
|
7400
8584
|
return {
|
|
7401
8585
|
lon: coords[0],
|
|
7402
8586
|
lat: coords[1],
|
|
7403
8587
|
displayName: readString(feature, "place_name") ?? "",
|
|
7404
|
-
score: coerceScore(feature.relevance)
|
|
8588
|
+
score: coerceScore(feature.relevance),
|
|
8589
|
+
...bbox ? { bbox } : {}
|
|
7405
8590
|
};
|
|
7406
8591
|
}).filter((m) => m !== null),
|
|
7407
8592
|
buildReverseUrl: (config, lon, lat) => {
|
|
@@ -7473,15 +8658,19 @@ const googleProvider = {
|
|
|
7473
8658
|
/** Map a Google Geocoding API `results` array onto normalized matches. */
|
|
7474
8659
|
function parseGoogleResults(data) {
|
|
7475
8660
|
return (data && typeof data === "object" && Array.isArray(data.results) ? data.results : []).map((result) => {
|
|
7476
|
-
const
|
|
8661
|
+
const geometry = result.geometry;
|
|
8662
|
+
const location = geometry?.location;
|
|
7477
8663
|
const lat = Number(location?.lat);
|
|
7478
8664
|
const lon = Number(location?.lng);
|
|
7479
8665
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
|
|
8666
|
+
const viewport = geometry?.viewport;
|
|
8667
|
+
const bbox = viewport ? normalizeGeocodeBbox(viewport.southwest?.lng, viewport.southwest?.lat, viewport.northeast?.lng, viewport.northeast?.lat) : null;
|
|
7480
8668
|
return {
|
|
7481
8669
|
lat,
|
|
7482
8670
|
lon,
|
|
7483
8671
|
displayName: readString(result, "formatted_address") ?? "",
|
|
7484
|
-
score: null
|
|
8672
|
+
score: null,
|
|
8673
|
+
...bbox ? { bbox } : {}
|
|
7485
8674
|
};
|
|
7486
8675
|
}).filter((m) => m !== null);
|
|
7487
8676
|
}
|
|
@@ -9161,6 +10350,8 @@ function normalizeProjectPreferences(preferences) {
|
|
|
9161
10350
|
cesiumBasemap: normalizeCesiumBasemap(map.cesiumBasemap),
|
|
9162
10351
|
terrainEnabled: normalizeBoolean(map.terrainEnabled, DEFAULT_PROJECT_PREFERENCES.map.terrainEnabled),
|
|
9163
10352
|
terrainIonAssetId: parseCesiumIonAssetId(map.terrainIonAssetId) ?? void 0,
|
|
10353
|
+
terrainExaggeration: normalizeTerrainExaggeration(map.terrainExaggeration),
|
|
10354
|
+
terrainSource: normalizeTerrainSource(map.terrainSource),
|
|
9164
10355
|
coordinateFormat: typeof map.coordinateFormat === "string" ? map.coordinateFormat : DEFAULT_PROJECT_PREFERENCES.map.coordinateFormat,
|
|
9165
10356
|
coordinateEpsgCode: normalizeEpsgCode(map.coordinateEpsgCode),
|
|
9166
10357
|
zoomToNewLayers: normalizeBoolean(map.zoomToNewLayers, DEFAULT_PROJECT_PREFERENCES.map.zoomToNewLayers)
|
|
@@ -9239,6 +10430,34 @@ function normalizeBounds(bounds) {
|
|
|
9239
10430
|
}
|
|
9240
10431
|
return DEFAULT_PROJECT_PREFERENCES.map.bounds;
|
|
9241
10432
|
}
|
|
10433
|
+
/**
|
|
10434
|
+
* Keep a saved terrain exaggeration only when it is a finite, non-negative
|
|
10435
|
+
* number; anything else falls back to the renderer default.
|
|
10436
|
+
*
|
|
10437
|
+
* Args:
|
|
10438
|
+
* value: The raw `terrainExaggeration` from a project file.
|
|
10439
|
+
*
|
|
10440
|
+
* Returns:
|
|
10441
|
+
* The exaggeration, or `undefined` for the default.
|
|
10442
|
+
*/
|
|
10443
|
+
function normalizeTerrainExaggeration(value) {
|
|
10444
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
10445
|
+
}
|
|
10446
|
+
/**
|
|
10447
|
+
* Keep a saved terrain DEM only when it is an HTTP(S) URL, the one kind of
|
|
10448
|
+
* source a project can reopen on another device.
|
|
10449
|
+
*
|
|
10450
|
+
* Args:
|
|
10451
|
+
* value: The raw `terrainSource` from a project file.
|
|
10452
|
+
*
|
|
10453
|
+
* Returns:
|
|
10454
|
+
* The trimmed URL, or `undefined` for the built-in global terrain.
|
|
10455
|
+
*/
|
|
10456
|
+
function normalizeTerrainSource(value) {
|
|
10457
|
+
if (typeof value !== "string") return void 0;
|
|
10458
|
+
const trimmed = value.trim();
|
|
10459
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : void 0;
|
|
10460
|
+
}
|
|
9242
10461
|
function normalizeNumber(value, fallback) {
|
|
9243
10462
|
return Number.isFinite(value) ? Number(value) : fallback;
|
|
9244
10463
|
}
|
|
@@ -9375,8 +10594,63 @@ function withoutRuntimeGeoArrow(layer) {
|
|
|
9375
10594
|
function portableLayer(layer) {
|
|
9376
10595
|
return withoutRuntimeGeoArrow(withoutZarrHeaders(withoutLocalRasterBytes(layer)));
|
|
9377
10596
|
}
|
|
10597
|
+
/**
|
|
10598
|
+
* The path of a URL, or `""` when it does not parse (not a loadable URL). A
|
|
10599
|
+
* protocol-relative URL (`//host/...`) is read against https.
|
|
10600
|
+
*/
|
|
10601
|
+
function urlPath(url) {
|
|
10602
|
+
try {
|
|
10603
|
+
return new URL(url.trim(), "https://geolibre.invalid").pathname;
|
|
10604
|
+
} catch {
|
|
10605
|
+
return "";
|
|
10606
|
+
}
|
|
10607
|
+
}
|
|
10608
|
+
/** Source kind of an ArcGIS I3S scene layer (`arcgis-i3s-tiles.ts` in the plugins). */
|
|
10609
|
+
const ARCGIS_I3S_SOURCE_KIND = "arcgis-i3s";
|
|
10610
|
+
/**
|
|
10611
|
+
* The path of an ArcGIS scene service or one of its layers:
|
|
10612
|
+
* `.../SceneServer`, `.../SceneServer/layers/<n>`, or a Building Scene Layer's
|
|
10613
|
+
* `.../layers/<n>/sublayers/<m>`. Narrower than the 3D Tiles
|
|
10614
|
+
* panel's `isArcgisI3sSceneLayerUrl` on purpose: a `tileset.json` stored under
|
|
10615
|
+
* a `SceneServer` folder, or a query that mentions one, stays a tileset.
|
|
10616
|
+
*/
|
|
10617
|
+
const I3S_SCENE_SERVER_PATH_RE = /\/SceneServer(\/layers\/\d+(\/sublayers\/\d+)?)?\/?$/i;
|
|
10618
|
+
/**
|
|
10619
|
+
* Re-file a 3D Tiles record whose URL is an ArcGIS scene service as an I3S
|
|
10620
|
+
* scene layer (issue #3377).
|
|
10621
|
+
*
|
|
10622
|
+
* The app draws a scene layer only when its source kind is `arcgis-i3s`. The
|
|
10623
|
+
* Python `add_3d_tiles()` used to write a SceneServer URL as a plain tileset
|
|
10624
|
+
* (`3d-tiles-url`), which the 3D Tiles control then loads as a `tileset.json`,
|
|
10625
|
+
* so nothing drew. This gives such a record the shape the app's 3D Tiles panel
|
|
10626
|
+
* writes (`createArcgisI3sStoreLayer`), keeping its id, name and settings.
|
|
10627
|
+
*
|
|
10628
|
+
* @param layer - A layer from a project file.
|
|
10629
|
+
* @returns The layer, re-filed as an I3S scene layer when it is a misfiled one.
|
|
10630
|
+
*/
|
|
10631
|
+
function withI3sSceneLayerKind(layer) {
|
|
10632
|
+
const url = layer.source?.url;
|
|
10633
|
+
if (layer.type !== "3d-tiles" || layer.metadata?.sourceKind !== "3d-tiles-url" || typeof url !== "string" || !I3S_SCENE_SERVER_PATH_RE.test(urlPath(url))) return layer;
|
|
10634
|
+
const { status: _status, panelCollapsed: _panelCollapsed, ...metadata } = layer.metadata;
|
|
10635
|
+
return {
|
|
10636
|
+
...layer,
|
|
10637
|
+
source: {
|
|
10638
|
+
...layer.source,
|
|
10639
|
+
type: ARCGIS_I3S_SOURCE_KIND
|
|
10640
|
+
},
|
|
10641
|
+
metadata: {
|
|
10642
|
+
...metadata,
|
|
10643
|
+
customLayerType: ARCGIS_I3S_SOURCE_KIND,
|
|
10644
|
+
externalDeckLayer: true,
|
|
10645
|
+
externalNativeLayer: true,
|
|
10646
|
+
identifiable: false,
|
|
10647
|
+
nativeLayerIds: [`${layer.id}-deck`],
|
|
10648
|
+
sourceKind: ARCGIS_I3S_SOURCE_KIND
|
|
10649
|
+
}
|
|
10650
|
+
};
|
|
10651
|
+
}
|
|
9378
10652
|
function normalizeLayer(layer) {
|
|
9379
|
-
layer = withRasterSourceDefaults(withoutLocalRasterBytes(layer));
|
|
10653
|
+
layer = withI3sSceneLayerKind(withRasterSourceDefaults(withoutLocalRasterBytes(layer)));
|
|
9380
10654
|
const { capabilities: rawCapabilities, filterExpression: rawFilterExpression, descriptiveMetadata: rawDescriptiveMetadata, geoarrow: _rawGeoarrow, ...rest } = layer;
|
|
9381
10655
|
const capabilities = normalizeLayerCapabilities(rawCapabilities);
|
|
9382
10656
|
const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);
|
|
@@ -9809,6 +11083,7 @@ const LAYER_GROUPED_KEYS = /* @__PURE__ */ new Set([
|
|
|
9809
11083
|
"geojson",
|
|
9810
11084
|
"filterExpression",
|
|
9811
11085
|
"quickFilters",
|
|
11086
|
+
"levelFilter",
|
|
9812
11087
|
"source",
|
|
9813
11088
|
"sourcePath",
|
|
9814
11089
|
"beforeId"
|
|
@@ -10159,10 +11434,12 @@ function diffLayer(beforeProject, afterProject, a, b, maxLength) {
|
|
|
10159
11434
|
}
|
|
10160
11435
|
diffRecords({
|
|
10161
11436
|
filterExpression: a.filterExpression,
|
|
10162
|
-
quickFilters: a.quickFilters
|
|
11437
|
+
quickFilters: a.quickFilters,
|
|
11438
|
+
levelFilter: a.levelFilter
|
|
10163
11439
|
}, {
|
|
10164
11440
|
filterExpression: b.filterExpression,
|
|
10165
|
-
quickFilters: b.quickFilters
|
|
11441
|
+
quickFilters: b.quickFilters,
|
|
11442
|
+
levelFilter: b.levelFilter
|
|
10166
11443
|
}, {
|
|
10167
11444
|
depth: 0,
|
|
10168
11445
|
maxLength
|
|
@@ -10426,6 +11703,6 @@ function metadataRank(path) {
|
|
|
10426
11703
|
if (path === "description") return 1;
|
|
10427
11704
|
return 2;
|
|
10428
11705
|
}
|
|
10429
|
-
export {
|
|
11706
|
+
export { GEOCODE_STATUS_KEY as $, LAYER_TYPES as $a, unescapePolyline as $i, buildLayerTree as $n, isVectorColorExpression as $r, isHeaderReferenceOnly as $t, normalizeSecondaryMapViews as A, CHINA_BASEMAPS as Aa, matchFeaturesByExpression as Ai, STYLE_LIBRARY_BUNDLE_VERSION as An, STAC_VERSION as Ar, resolveGeocoderConfig as At, storyMapHasContent as B, BLEND_MODES as Ba, getCesiumIonToken as Bi, createDefaultPrintLayout as Bn, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 as Br, normalizeStoryChapterDurationSeconds as Bt, normalizeModelGraph as C, ARCGIS_PROJECTION_PRESETS as Ca, vectorStrokeWidthValue as Ci, featureCollectionToGeoArrow as Cn, collectTransitiveJoinSourceIds as Cr, nominatimReverseResultToDisplay as Ct, normalizeProcessingHistory as D, basemapToCesiumImagery as Da, formatExpressionPreviewValue as Di, BUILT_IN_STYLE_PRESETS as Dn, applyLayerVirtualFields as Dr, normalizeGeocodingProviderId as Dt, normalizePrimaryRenderer as E, availableCesiumBasemap as Ea, evaluateMapExpression as Ei, withRasterSourceDefaults as En, stripJoinFields as Er, normalizeGeocodingLocator as Et, projectFromStore as F, getRegionalBasemapById as Fa, applySelectionMode as Fi, normalizeStyleLibraryEntries as Fn, metadataDateToRfc3339 as Fr, shouldThrottle as Ft, DEFAULT_GEOCODING_PROVIDER_ID as G, DEFAULT_LEGEND_CONFIG as Ga, getRuntimeEnvironment as Gi, scrubPrintLayoutForRemovedLayers as Gn, extrusionFloorHeightValue as Gr, isCredentialUrlParam as Gt, CARTOCIUDAD_MIN_INTERVAL_MS as H, DEFAULT_BLEND_MODE as Ha, getMapboxAccessToken as Hi, normalizePrintLayoutConfig as Hn, effectiveVectorRules as Hr, PROJECT_CREDENTIAL_FIELDS as Ht, resolveMapGrid as I, getRegionalBasemapByStyleUrl as Ia, featureSelectionId as Ii, parseStyleLibrary as In, normalizeLayerDescriptiveMetadata as Ir, unmatchedGeocodeFeature as It, GEOCODE_DISPLAY_NAME_KEY as J, DEFAULT_PROJECT_PREFERENCES as Ja, decodePolyline as Ji, scrubLegendForRemovedLayers as Jn, generatorSizeRange as Jr, redactProjectCredentials as Jt, DEFAULT_REVERSE_GEOCODE_ENDPOINT as K, DEFAULT_MAP_GRID_LAYOUT as Ka, getSpatialExtensionPath as Ki, removedLayerIdSet as Kn, extrusionHeightValue as Kr, redactConfigurationCredentials as Kt, serializeProject as L, isRegionalBasemapSentinel as La, invertSelection as Li, sanitizeLayerStylePatch as Ln, parseMetadataKeywords as Lr, DEFAULT_STORY_CHAPTER_DURATION_SECONDS as Lt, normalizeWidgets as M, REGIONAL_BASEMAP_GROUPS as Ma, substituteExpressionVariables as Mi, createStyleLibraryEntryId as Mn, isValidMetadataDate as Mr, selectedGeocodingLocator as Mt, parseProject as N, REGIONAL_BASEMAP_SENTINEL_PREFIX as Na, validateMapExpression as Ni, extractStyleLibraryStyle as Nn, isValidMetadataEmail as Nr, setGeocodingFetch as Nt, normalizeProjectComments as O, normalizeCesiumBasemap as Oa, inferFieldTypes as Oi, RAMP_STYLE_KEYS as On, stripVirtualFieldColumns as Or, parseArcGISLocatorInfo as Ot, portableWmsTileUrl as P, gateRegionalBasemap as Pa, SELECTION_MODES as Pi, isStyleLibraryTargetLayer as Pn, isValidMetadataUrl as Pr, setGeocodingTokenProvider as Pt, GEOCODE_SCORE_KEY as Q, LARGE_VECTOR_FEATURE_THRESHOLD as Qa, polylineStrToGeoJSON as Qi, buildLayerPanelUnits as Qn, isStrokeDashPattern as Qr, hasHeaderReferences as Qt, serializeProjectWithLayerCache as R, ARCGIS_IMAGE_SERVICE_SOURCE_KIND as Ra, getArcgisApiKey as Ri, serializeStyleLibrary as Rn, stacAssetMediaType as Rr, MAX_STORY_CHAPTER_DURATION_SECONDS as Rt, normalizeMapViewState as S, parseMapCrsEpsgCode as Sa, vectorOutlineColorValue as Si, computeGeoArrowBBox as Sn, cascadeLayerJoinRefresh as Sr, nominatimResultToFeature as St, normalizeObiaWorkbench as T, CESIUM_BASEMAPS as Ta, compileFeatureExpression as Ti, isGeoArrowBatch as Tn, reapplyLayerJoins as Tr, normalizeGeocodeBbox as Tt, CARTOCIUDAD_PUBLIC_HOST as U, DEFAULT_DASHBOARD_COLUMNS as Ua, getProtomapsApiKey as Ui, printLayoutConfigsEqual as Un, extrusionBaseValue as Ur, PUBLISHABLE_PLUGIN_SETTINGS as Ut, withI3sSceneLayerKind as V, DEFAULT_BASEMAP as Va, getGoogleMapsApiKey as Vi, isDefaultPrintLayout as Vn, circleRadiusValue as Vr, MAX_REDACT_DEPTH as Vt, DEFAULT_FORWARD_GEOCODE_ENDPOINT as W, DEFAULT_LAYER_STYLE as Wa, getProtomapsStyleUrl as Wi, scrubPrintLayoutForLayers as Wn, extrusionColorValue as Wr, isCredentialFieldName as Wt, GEOCODE_LON_KEY as X, DUCKDB_VECTOR_FEATURE_WARN_COUNT as Xa, encodePolyline as Xi, DEFAULT_LAYER_GROUP_OPACITY as Xn, heatmapRampColors as Xr, setRegistryPublishableSettings as Xt, GEOCODE_LAT_KEY as Y, DEFAULT_STORY_MAP as Ya, decodePolylineDetailed as Yi, scrubWidgetsForRemovedLayers as Yn, hasSimpleStyleProperties as Yr, redactUrlCredentials as Yt, GEOCODE_PROVIDER_KEY as Z, DUCKDB_VECTOR_ROUTE_BYTES as Za, geoJSONToPolylineStr as Zi, applyGroupEffects as Zn, isHexColor as Zr, allowsCredentialHeaders as Zt, normalizeBookmarkGroups as _, adaptBboxTemplateToCrs as _a, vectorCircleColorValue as _i, CZML_SOURCE_KIND as _n, normalizeLayerCapabilities as _r, geocoderSupportsReverse as _t, DEFAULT_PROJECT_NAME as a, interpolateColors as aa, proportionalRadiusExpression as ai, CESIUM_OSM_BUILDINGS_ASSET_ID as an, OPENFREEMAP_BASEMAPS as ao, layerPanelGroupHeaders as ar, PUBLIC_GEOCODE_ROW_CAP as at, normalizeLegendConfig as b, mapCrsPreferencePatch as ba, vectorFillOpacityValue as bi, isCzmlLayer as bn, applyJoinsToLayer as br, getGeocodingProvider as bt, createDefaultMapView as c, normalizeHexColor as ca, readsSimpleStyleProperties as ci, createCesiumIonLayer as cn, PROTOMAPS_BASEMAPS as co, sortLayerGroupInPanel as cr, buildReverseGeocodeUrl as ct, createProjectTemplate as d, rgbToHex as da, simpleStyleNumberValue as di, parseCesiumIonAssetId as dn, detectNonGeographicCoordinates as do, appPrivilegeReason as dr, geocodeForward as dt, VECTOR_COLOR_RAMPS as ea, lineWidthValue as ei, resolveHeaderReferences as en, MAX_DASHBOARD_COLUMNS as eo, effectiveLayerRenderState as er, GEOCODING_PROVIDERS as et, detachProjectCopy as f, geojsonHasZCoordinates as fa, strokeBlurValue as fi, CESIUM_KML_SOURCE_KIND as fn, isDuckDBQueryLayer as fo, createDefaultAppCapabilities as fr, geocodeMatchToFeature as ft, normalizeBlankBackgroundColor as g, MAP_CRS_PRESETS as ga, strokeOpacityValue as gi, CZML_QUICK_PICKS as gn, normalizeAppPrivileges as gr, geocoderNeedsApiKey as gt, isSessionOnlyLayer as h, MAP_CRS_CHANGE_EVENT as ha, strokeOpacityPaintValue as hi, isCesiumKmlLayer as hn, styleValue as ho, intersectPrivileges as hr, geocoderMinIntervalMs as ht, previewValue as i, getVectorColorRamp as ia, parseJsonExpression as ii, CESIUM_ION_SOURCE_KIND as in, NETCDF_IMAGE_SOURCE_KIND as io, layerGroupSortability as ir, NOMINATIM_PUBLIC_HOST as it, normalizeStoryMap as j, REGIONAL_BASEMAPS as ja, removeTrailingJsonCommas as ji, SYMBOL_STYLE_KEYS as jn, buildLayerStacItem as jr, rowCap as jt, normalizeProjectInteraction as k, sameCesiumImagery as ka, isStyleSpecColor as ki, STYLE_LIBRARY_BUNDLE_TYPE as kn, STAC_PROCESSING_EXTENSION as kr, parseArcGISSuggestions as kt, createEmptyProject as l, parseHexColor as la, ruleBasedColorExpression as li, isCesiumIonLayer as ln, SQL_QUERY_SOURCE_KIND as lo, ALL_APP_PRIVILEGES as lr, csvRowsToGeocodeRequests as lt, isAllowedPluginManifestUrl as m, transformGeojsonElevation as ma, strokeDashValue as mi, createCesiumKmlLayer as mn, shouldUseTiledRendering as mo, inferLayerCapabilities as mr, geocodeSuggest as mt, diffProjects as n, createGraduatedClassBreaks as na, mercatorMetersPerPixelAtZoom0 as ni, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID as nn, MAX_PROCESSING_HISTORY as no, layerGroupDepth as nr, GeocoderUnsupportedError as nt, MAX_PROJECT_BOOKMARKS as o, interpolateHexColor as oa, proportionalSizeRange as oi, cesiumIonAssetId as on, PLANETARY_COMPUTER_SOURCE_KIND as oo, normalizeGroupContiguity as or, arcgisLocatorInputField as ot, hydrateProjectLayer as p, horizontalBbox as pa, strokeDashPresetName as pi, cesiumKmlSource as pn, isPlanetaryComputerLayer as po, hasAppPrivilege as pr, geocodeReverse as pt, GEOCODE_ATTRIBUTE_PREFIX as q, DEFAULT_PRIMARY_RENDERER as qa, batchDecodePolylines as qi, scrubCommentsForRemovedLayers as qn, generatorCircleRadiusValue as qr, redactCredentials as qt, jsonValuesEqual as r, createQuantileBreaks as ra, metersWidthExpression as ri, CESIUM_ION_QUICK_PICKS as rn, MIN_DASHBOARD_COLUMNS as ro, layerGroupMoveability as rr, NOMINATIM_MIN_INTERVAL_MS as rt, applyProjectToStore as s, interpolateRampColors as sa, readSimpleStyleNumberLookup as si, cesiumIonAssetKind as sn, PROJECT_VERSION as so, reorderLayerGroupInPanel as sr, buildForwardGeocodeUrl as st, diffFeatureCollections as t, createEqualIntervalBreaks as ta, mapZoomStepOutputs as ti, CESIUM_BING_AERIAL_ASSET_ID as tn, MAX_MAP_GRID_DIM as to, groupMemberIndices as tr, GeocoderServiceError as tt, createProjectLayerSerializationCache as u, parseHexColorList as ua, ruleBasedVisibilityFilter as ui, isCesiumOnlyLayer as un, STROKE_DASH_PRESETS as uo, ROLE_PRIVILEGES as ur, fetchArcGISLocatorInfo as ut, normalizeBookmarks as v, deriveTileMatrixSet as va, vectorColorExpression as vi, createCzmlLayer as vn, resolveLayerCapabilities as vr, geocoderSupportsSuggest as vt, normalizeModels as w, normalizeArcgisWkid as wa, EXPRESSION_FUNCTION_CATEGORIES as wi, geoArrowToFeatureCollection as wn, layerJoinKey as wr, normalizeArcGISLocatorUrl as wt, normalizeMapLayout as x, normalizeMapCrs as xa, vectorLineColorValue as xi, parseCzml as xn, applyLayerJoins as xr, nextDelayMs as xt, normalizeDashboardColumns as y, getMapCrsPreset as ya, vectorFillColorValue as yi, czmlSource as yn, resolveRolePrivileges as yr, getGeocoderConfig as yt, serializeProjectWithLayerCacheAsync as z, BLANK_BASEMAP as za, getBuildEnvironment as zi, DEFAULT_PRINT_LAYOUT as zn, validateLayerDescriptiveMetadata as zr, MIN_STORY_CHAPTER_DURATION_SECONDS as zt };
|
|
10430
11707
|
|
|
10431
|
-
//# sourceMappingURL=project-diff-
|
|
11708
|
+
//# sourceMappingURL=project-diff-DJkOd8ff.mjs.map
|