@geolibre/core 2.8.0 → 3.0.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/index.mjs CHANGED
@@ -169,6 +169,9 @@ const DEFAULT_LAYER_STYLE = {
169
169
  rotation: 0,
170
170
  maxWidth: 10,
171
171
  transform: "none",
172
+ numberFormatEnabled: false,
173
+ numberDecimals: 0,
174
+ numberLocale: "",
172
175
  dedupe: "off",
173
176
  sizeExpression: "",
174
177
  colorExpression: "",
@@ -227,6 +230,8 @@ const DEFAULT_LAYER_STYLE = {
227
230
  pointRenderer: "single",
228
231
  heatmapRadius: 30,
229
232
  heatmapIntensity: 1,
233
+ heatmapColorRamp: "turbo",
234
+ heatmapWeightProperty: "",
230
235
  clusterRadius: 50,
231
236
  clusterMaxZoom: 14,
232
237
  invertedFillEnabled: false,
@@ -306,6 +311,12 @@ const NETCDF_IMAGE_SOURCE_KIND = "netcdf-image";
306
311
  function isDuckDBQueryLayer(layer) {
307
312
  return layer?.type === "duckdb-query" && layer.metadata.sourceKind === "duckdb-query" && layer.metadata.externalDeckLayer === true;
308
313
  }
314
+ /**
315
+ * The engine that draws the primary map area when a project says nothing. The
316
+ * 2D map: it is the renderer every tool, plugin, and panel is wired to, so an
317
+ * existing project (and a new one) opens exactly as it always did.
318
+ */
319
+ const DEFAULT_PRIMARY_RENDERER = "maplibre";
309
320
  /** The default single-map grid (one pane, sync enabled so it turns on cleanly). */
310
321
  const DEFAULT_MAP_GRID_LAYOUT = {
311
322
  rows: 1,
@@ -331,6 +342,7 @@ const DEFAULT_PROJECT_PREFERENCES = {
331
342
  ellipsoidId: "earth",
332
343
  scaleUnit: "metric",
333
344
  showPointerElevation: false,
345
+ terrainEnabled: false,
334
346
  coordinateFormat: "dd"
335
347
  },
336
348
  environmentVariables: [],
@@ -1400,14 +1412,393 @@ function isRegionalBasemapSentinel(styleUrl) {
1400
1412
  return Boolean(styleUrl?.startsWith(REGIONAL_BASEMAP_SENTINEL_PREFIX));
1401
1413
  }
1402
1414
  //#endregion
1415
+ //#region src/cesium-imagery.ts
1416
+ /**
1417
+ * Maps the project's basemap onto imagery the Cesium globe can render.
1418
+ *
1419
+ * The 2D panes render `basemapStyleUrl` through MapLibre, which understands
1420
+ * vector GL styles and GeoLibre's `geolibre://` sentinels. Cesium understands
1421
+ * neither: it draws imagery from tile providers only. So a globe pane that
1422
+ * simply mounted the store basemap would show nothing, which is why the pane
1423
+ * used to hardcode Ion World Imagery (with a token) or OpenStreetMap (without)
1424
+ * and ignore the user's choice entirely — leaving a bright globe beside dark 2D
1425
+ * panes and breaking the "same map, different projection" promise the multi-map
1426
+ * grid makes.
1427
+ *
1428
+ * This module is the translation. It is deliberately **engine-free** — it
1429
+ * returns a plain descriptor and never imports Cesium — so it lives in
1430
+ * `@geolibre/core` beside the basemap catalogs it reads, the 2D and 3D
1431
+ * renderers share one source of truth, and the Cesium chunk boundary in
1432
+ * `vite.config.ts` stays intact.
1433
+ *
1434
+ * Three outcomes are possible, and the caller has to handle all three (see
1435
+ * {@link CesiumBasemapImagery}): tiles to draw, nothing to draw, or "no raster
1436
+ * equivalent exists — pick your own default".
1437
+ */
1438
+ /** Stable project IDs and the corresponding Cesium bundled thumbnails / ion assets. */
1439
+ const CESIUM_BASEMAPS = [
1440
+ {
1441
+ id: "project",
1442
+ name: "Project basemap",
1443
+ icon: "naturalEarthII.png"
1444
+ },
1445
+ {
1446
+ id: "natural-earth",
1447
+ name: "Natural Earth II",
1448
+ icon: "naturalEarthII.png"
1449
+ },
1450
+ {
1451
+ id: "bing-aerial",
1452
+ name: "Bing Maps Aerial",
1453
+ icon: "bingAerial.png",
1454
+ assetId: 2
1455
+ },
1456
+ {
1457
+ id: "bing-labels",
1458
+ name: "Bing Maps Aerial with Labels",
1459
+ icon: "bingAerialLabels.png",
1460
+ assetId: 3
1461
+ },
1462
+ {
1463
+ id: "bing-roads",
1464
+ name: "Bing Maps Roads",
1465
+ icon: "bingRoads.png",
1466
+ assetId: 4
1467
+ },
1468
+ {
1469
+ id: "sentinel-2",
1470
+ name: "Sentinel-2",
1471
+ icon: "sentinel-2.png",
1472
+ assetId: 3954
1473
+ },
1474
+ {
1475
+ id: "blue-marble",
1476
+ name: "Blue Marble",
1477
+ icon: "blueMarble.png",
1478
+ assetId: 3845
1479
+ },
1480
+ {
1481
+ id: "earth-at-night",
1482
+ name: "Earth at night",
1483
+ icon: "earthAtNight.png",
1484
+ assetId: 3812
1485
+ },
1486
+ {
1487
+ id: "google-satellite",
1488
+ name: "Google Maps Satellite",
1489
+ icon: "googleSatellite.png",
1490
+ assetId: 3830182
1491
+ },
1492
+ {
1493
+ id: "google-labels",
1494
+ name: "Google Maps Satellite with Labels",
1495
+ icon: "googleSatelliteLabels.png",
1496
+ assetId: 3830183
1497
+ },
1498
+ {
1499
+ id: "google-roads",
1500
+ name: "Google Maps Roadmap",
1501
+ icon: "googleRoadmap.png",
1502
+ assetId: 3830184
1503
+ },
1504
+ {
1505
+ id: "google-contour",
1506
+ name: "Google Maps Contour",
1507
+ icon: "googleContour.png",
1508
+ assetId: 3830186
1509
+ },
1510
+ {
1511
+ id: "azure-aerial",
1512
+ name: "Azure Maps Aerial",
1513
+ icon: "azureAerial.png",
1514
+ assetId: 3891168
1515
+ },
1516
+ {
1517
+ id: "azure-roads",
1518
+ name: "Azure Maps Roads",
1519
+ icon: "azureRoads.png",
1520
+ assetId: 3891169
1521
+ },
1522
+ {
1523
+ id: "esri-imagery",
1524
+ name: "ArcGIS World Imagery",
1525
+ icon: "ArcGisMapServiceWorldImagery.png",
1526
+ category: "Other",
1527
+ service: "World_Imagery"
1528
+ },
1529
+ {
1530
+ id: "esri-hillshade",
1531
+ name: "ArcGIS World Hillshade",
1532
+ icon: "ArcGisMapServiceWorldHillshade.png",
1533
+ category: "Other",
1534
+ service: "Elevation/World_Hillshade"
1535
+ },
1536
+ {
1537
+ id: "esri-ocean",
1538
+ name: "Esri World Ocean",
1539
+ icon: "ArcGisMapServiceWorldOcean.png",
1540
+ category: "Other",
1541
+ service: "Ocean/World_Ocean_Base"
1542
+ },
1543
+ {
1544
+ id: "osm",
1545
+ name: "OpenStreetMap",
1546
+ icon: "openStreetMap.png",
1547
+ category: "Other"
1548
+ },
1549
+ {
1550
+ id: "stadia-watercolor",
1551
+ name: "Stadia x Stamen Watercolor",
1552
+ icon: "stamenWatercolor.png",
1553
+ category: "Other",
1554
+ stadiaStyle: "stamen_watercolor",
1555
+ maximumLevel: 16,
1556
+ extension: "jpg"
1557
+ },
1558
+ {
1559
+ id: "stadia-toner",
1560
+ name: "Stadia x Stamen Toner",
1561
+ icon: "stamenToner.png",
1562
+ category: "Other",
1563
+ stadiaStyle: "stamen_toner",
1564
+ maximumLevel: 20,
1565
+ extension: "png"
1566
+ },
1567
+ {
1568
+ id: "stadia-smooth",
1569
+ name: "Stadia Alidade Smooth",
1570
+ icon: "stadiaAlidadeSmooth.png",
1571
+ category: "Other",
1572
+ stadiaStyle: "alidade_smooth",
1573
+ maximumLevel: 20,
1574
+ extension: "png"
1575
+ },
1576
+ {
1577
+ id: "stadia-dark",
1578
+ name: "Stadia Alidade Smooth Dark",
1579
+ icon: "stadiaAlidadeSmoothDark.png",
1580
+ category: "Other",
1581
+ stadiaStyle: "alidade_smooth_dark",
1582
+ maximumLevel: 20,
1583
+ extension: "png"
1584
+ }
1585
+ ];
1586
+ function normalizeCesiumBasemap(value) {
1587
+ return CESIUM_BASEMAPS.find((entry) => entry.id === value)?.id ?? "project";
1588
+ }
1589
+ /** Missing credentials fall back to the project background without changing the saved choice. */
1590
+ function availableCesiumBasemap(value, hasIonToken) {
1591
+ const entry = CESIUM_BASEMAPS.find((entry) => entry.id === value);
1592
+ return entry && (!("assetId" in entry) || hasIonToken) ? entry.id : "project";
1593
+ }
1594
+ const OSM_CREDIT = "© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors";
1595
+ /** EOX Maps serves these layers keyless under CC BY-SA 4.0. */
1596
+ const EOX_CREDIT = "<a href=\"https://maps.eox.at\">EOX Maps</a> (CC BY-SA 4.0)";
1597
+ const eoxLayer = (layer, extension) => `https://tiles.maps.eox.at/wmts/1.0.0/${layer}/default/g/{z}/{y}/{x}.${extension}`;
1598
+ /** Colourful streets — the tone of Liberty and Bright. */
1599
+ const STREETS = {
1600
+ template: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
1601
+ attribution: OSM_CREDIT,
1602
+ maximumLevel: 19
1603
+ };
1604
+ /** Light and muted — the tone of Positron and the pale Protomaps flavors. */
1605
+ const LIGHT = {
1606
+ template: eoxLayer("terrain-light_3857", "jpg"),
1607
+ attribution: `Terrain Light · ${OSM_CREDIT} · ${EOX_CREDIT}`,
1608
+ maximumLevel: 16
1609
+ };
1610
+ /**
1611
+ * Dark — the tone of Dark, Fiord, and the dark Protomaps flavors. NASA's Black
1612
+ * Marble night lights is the one keyless dark raster basemap left, and it suits
1613
+ * a globe: the point of matching here is that a dark project does not end up
1614
+ * with a glaring bright globe in the corner.
1615
+ */
1616
+ const DARK = {
1617
+ template: eoxLayer("blackmarble_3857", "jpg"),
1618
+ attribution: `Black Marble © NASA Earth Observatory · ${EOX_CREDIT}`,
1619
+ maximumLevel: 8
1620
+ };
1621
+ /** Raster analogue per OpenFreeMap style name (the last path segment). */
1622
+ const OPENFREEMAP_ANALOGUES = {
1623
+ liberty: STREETS,
1624
+ bright: STREETS,
1625
+ positron: LIGHT,
1626
+ dark: DARK,
1627
+ fiord: DARK
1628
+ };
1629
+ /** Raster analogue per Protomaps v5 flavor. */
1630
+ const PROTOMAPS_ANALOGUES = {
1631
+ light: LIGHT,
1632
+ white: LIGHT,
1633
+ grayscale: LIGHT,
1634
+ dark: DARK,
1635
+ black: DARK
1636
+ };
1637
+ function toImagery(analogue) {
1638
+ return {
1639
+ kind: "xyz",
1640
+ template: analogue.template,
1641
+ attribution: analogue.attribution,
1642
+ maximumLevel: analogue.maximumLevel
1643
+ };
1644
+ }
1645
+ /**
1646
+ * Whether two descriptors would draw the same thing.
1647
+ *
1648
+ * Descriptors are built fresh on every call, so two basemaps resolving to the
1649
+ * same analogue — OpenFreeMap `liberty` and `bright` are both the streets
1650
+ * tone — yield equal but distinct objects. A renderer that remembers the
1651
+ * descriptor it drew (to skip redundant work) must compare by value: a
1652
+ * reference check would miss that case and tear down and re-add an identical
1653
+ * tile provider, costing a flash and a round of re-requested tiles for no
1654
+ * visible change.
1655
+ */
1656
+ function sameCesiumImagery(a, b) {
1657
+ if (a.kind !== b.kind) return false;
1658
+ if (a.kind === "arcgis" && b.kind === "arcgis") return a.url === b.url;
1659
+ if (a.kind === "ion" && b.kind === "ion") return a.assetId === b.assetId;
1660
+ if (a.kind !== "xyz" || b.kind !== "xyz") return true;
1661
+ return a.template === b.template && a.apiKeyProvider === b.apiKeyProvider && a.attribution === b.attribution && a.maximumLevel === b.maximumLevel && a.scheme === b.scheme && a.overlayTemplate === b.overlayTemplate;
1662
+ }
1663
+ /**
1664
+ * `decodeURIComponent` that reports failure instead of throwing. A URL keeps a
1665
+ * malformed escape (`%E0%A4`) in its pathname quite happily, but decoding one
1666
+ * throws `URIError` — and this runs inside a React render, so an unhandled throw
1667
+ * would take the globe pane down instead of falling through to the default.
1668
+ */
1669
+ function safeDecode(value) {
1670
+ try {
1671
+ return decodeURIComponent(value);
1672
+ } catch {
1673
+ return;
1674
+ }
1675
+ }
1676
+ /**
1677
+ * The raster analogue for a vector GL style URL, or undefined when the style is
1678
+ * not one GeoLibre ships (a provider style, a user's own URL, a Mapbox style).
1679
+ * Matching on host + path rather than searching the URL for "dark"/"light"
1680
+ * keeps an unrelated style whose *key* happens to contain one of those words
1681
+ * from being mapped to the wrong tone.
1682
+ */
1683
+ function vectorStyleAnalogue(styleUrl) {
1684
+ let parsed;
1685
+ try {
1686
+ parsed = new URL(styleUrl);
1687
+ } catch {
1688
+ return;
1689
+ }
1690
+ if (parsed.hostname === "tiles.openfreemap.org") {
1691
+ const name = parsed.pathname.split("/").filter(Boolean).at(-1);
1692
+ return name ? OPENFREEMAP_ANALOGUES[name] : void 0;
1693
+ }
1694
+ if (parsed.hostname === "api.protomaps.com") {
1695
+ const segments = parsed.pathname.split("/").filter(Boolean);
1696
+ const flavor = segments[0] === "styles" ? segments[2] : void 0;
1697
+ const decoded = flavor ? safeDecode(flavor) : void 0;
1698
+ return decoded ? PROTOMAPS_ANALOGUES[decoded] : void 0;
1699
+ }
1700
+ }
1701
+ /**
1702
+ * Translate a basemap style URL into imagery for the Cesium globe.
1703
+ *
1704
+ * Planetary and regional basemaps are already raster tile sets, so they carry
1705
+ * straight over — the globe shows the same Mars/Moon/Amap tiles the 2D panes
1706
+ * do. GeoLibre's own vector styles map to a keyless raster basemap of matching
1707
+ * tone, so switching the project to a dark basemap darkens the globe too.
1708
+ * Everything else reports `default`, leaving the choice of fallback imagery to
1709
+ * the renderer (which knows whether an Ion token is configured).
1710
+ *
1711
+ * A `geolibre://` sentinel that no longer resolves is treated as the default
1712
+ * basemap, mirroring what `resolveMapStyle` does for the 2D map rather than
1713
+ * handing the sentinel onward as if it were fetchable.
1714
+ *
1715
+ * @param styleUrl - The project's `basemapStyleUrl`.
1716
+ * @returns What the globe should draw beneath the data layers.
1717
+ */
1718
+ function basemapToCesiumImagery(styleUrl, cesiumBasemap = "project") {
1719
+ const entry = CESIUM_BASEMAPS.find((entry) => entry.id === cesiumBasemap);
1720
+ if (entry && "service" in entry) return {
1721
+ kind: "arcgis",
1722
+ url: `https://services.arcgisonline.com/ArcGIS/rest/services/${entry.service}/MapServer`
1723
+ };
1724
+ if (entry && "stadiaStyle" in entry) return {
1725
+ kind: "xyz",
1726
+ template: `https://tiles.stadiamaps.com/tiles/${entry.stadiaStyle}/{z}/{x}/{y}.${entry.extension}`,
1727
+ maximumLevel: entry.maximumLevel,
1728
+ apiKeyProvider: "stadia",
1729
+ attribution: "<a href=\"https://stadiamaps.com/\">© Stadia Maps</a> " + (entry.stadiaStyle.startsWith("stamen_") ? "<a href=\"https://stamen.com/\">© Stamen Design</a> " : "") + "<a href=\"https://openmaptiles.org/\">© OpenMapTiles</a> <a href=\"https://www.openstreetmap.org/copyright\">© OpenStreetMap contributors</a>"
1730
+ };
1731
+ if (entry && "assetId" in entry) return {
1732
+ kind: "ion",
1733
+ assetId: entry.assetId
1734
+ };
1735
+ if (cesiumBasemap === "natural-earth") return { kind: "natural-earth" };
1736
+ if (cesiumBasemap === "osm") return toImagery(STREETS);
1737
+ if (styleUrl === "") return { kind: "none" };
1738
+ const url = styleUrl ?? "https://tiles.openfreemap.org/styles/liberty";
1739
+ const planetary = getPlanetaryBasemapByStyleUrl(url);
1740
+ if (planetary) return {
1741
+ kind: "xyz",
1742
+ template: planetary.tileUrl,
1743
+ attribution: planetary.attribution,
1744
+ maximumLevel: planetary.maxZoom,
1745
+ ...planetary.scheme ? { scheme: planetary.scheme } : {}
1746
+ };
1747
+ const regional = getRegionalBasemapByStyleUrl(url);
1748
+ if (regional) return {
1749
+ kind: "xyz",
1750
+ template: regional.tileUrl,
1751
+ attribution: regional.attribution,
1752
+ maximumLevel: regional.maxZoom,
1753
+ ...regional.scheme ? { scheme: regional.scheme } : {},
1754
+ ...regional.overlayTileUrl ? { overlayTemplate: regional.overlayTileUrl } : {}
1755
+ };
1756
+ const analogue = vectorStyleAnalogue(url.startsWith("geolibre://") ? DEFAULT_BASEMAP : url);
1757
+ return analogue ? toImagery(analogue) : { kind: "default" };
1758
+ }
1759
+ //#endregion
1403
1760
  //#region src/geojson-z.ts
1404
1761
  /**
1405
1762
  * Helpers for vector data whose coordinates carry a Z value (elevation), e.g.
1406
1763
  * GPX tracks with `<ele>` readings or LineStringZ/PointZ geometries. MapLibre's
1407
1764
  * 2D style layers ignore the third coordinate, so layers that want to render
1408
1765
  * their Z values use these helpers to detect and rescale elevations before
1409
- * handing the data to a deck.gl overlay.
1410
- */
1766
+ * handing the data to a deck.gl overlay. The same data can carry its elevation
1767
+ * in a `bbox` member too, which `horizontalBbox` trims back to two dimensions.
1768
+ */
1769
+ /**
1770
+ * Reduces a bounding box to its horizontal extent, `[west, south, east, north]`.
1771
+ *
1772
+ * RFC 7946 §5 lets a `bbox` member carry elevation, in which case it holds six
1773
+ * values — `[west, south, minAltitude, east, north, maxAltitude]` — and
1774
+ * `@turf/bbox` returns a collection's own member verbatim rather than
1775
+ * recomputing it. A consumer reading such a box as four values takes the
1776
+ * altitude for a longitude and the east edge for a latitude; that is how a 3D
1777
+ * feed reached MapLibre as an out-of-range latitude (#2358).
1778
+ *
1779
+ * Returns null when there is no usable horizontal extent: a box of any other
1780
+ * length, or one carrying a non-finite value anywhere — an empty collection, or
1781
+ * one whose features all have a null geometry, bboxes to ±Infinity, and a box
1782
+ * whose altitudes are not finite is not one to trust the rest of.
1783
+ *
1784
+ * @param box - A GeoJSON or Turf bounding box, of four or six values.
1785
+ */
1786
+ function horizontalBbox(box) {
1787
+ if (!box) return null;
1788
+ if (box.length !== 4 && box.length !== 6) return null;
1789
+ if (!box.every((value) => Number.isFinite(value))) return null;
1790
+ return box.length === 6 ? [
1791
+ box[0],
1792
+ box[1],
1793
+ box[3],
1794
+ box[4]
1795
+ ] : [
1796
+ box[0],
1797
+ box[1],
1798
+ box[2],
1799
+ box[3]
1800
+ ];
1801
+ }
1411
1802
  const hasZCache = /* @__PURE__ */ new WeakMap();
1412
1803
  /**
1413
1804
  * Returns true when any coordinate in the GeoJSON carries a finite, non-zero
@@ -2383,13 +2774,18 @@ function getMapboxAccessToken(env) {
2383
2774
  /**
2384
2775
  * Resolves the Cesium Ion access token from the runtime environment.
2385
2776
  *
2386
- * The 3D-globe view's world imagery and terrain need a Cesium Ion token. It is
2777
+ * Cesium World Terrain and Ion World Imagery need a Cesium Ion token. It is
2387
2778
  * supplied via `VITE_CESIUM_TOKEN` (baked in at build time from the bare
2388
2779
  * `CESIUM_TOKEN` env var, which `vite.config.ts` copies into the prefixed name)
2389
2780
  * or set at runtime through Settings → Environment variables
2390
2781
  * (`window.__GEOLIBRE_RUNTIME_ENV__`, which bypasses Vite's envPrefix allowlist,
2391
- * so a bare `CESIUM_TOKEN` entry works there too). When unset, the globe cannot
2392
- * render and the 3D view is not offered.
2782
+ * so a bare `CESIUM_TOKEN` entry works there too).
2783
+ *
2784
+ * The 3D-globe view itself does **not** need one: it draws the project basemap
2785
+ * as its base imagery whenever that basemap has a raster form (see
2786
+ * {@link basemapToCesiumImagery}), and falls back to Ion World Imagery — or, with
2787
+ * no token, to OpenStreetMap — for the ones that do not. So an absent token
2788
+ * costs terrain and the quality of that fallback, not the view.
2393
2789
  *
2394
2790
  * @param env - Environment record (defaults to the runtime environment);
2395
2791
  * injectable for testing.
@@ -2842,7 +3238,8 @@ const EXPRESSION_FUNCTION_CATEGORIES = [
2842
3238
  fn("concat", "[\"concat\", [\"get\", \"field\"], \" suffix\"]", "concat"),
2843
3239
  fn("upcase", "[\"upcase\", [\"get\", \"field\"]]", "upcase"),
2844
3240
  fn("downcase", "[\"downcase\", [\"get\", \"field\"]]", "downcase"),
2845
- fn("slice", "[\"slice\", [\"get\", \"field\"], 0, 3]", "slice")
3241
+ fn("slice", "[\"slice\", [\"get\", \"field\"], 0, 3]", "slice"),
3242
+ fn("number-format", "[\"number-format\", [\"get\", \"field\"], {\"locale\": \"en-US\", \"max-fraction-digits\": 2}]", "numberFormat")
2846
3243
  ]
2847
3244
  },
2848
3245
  {
@@ -3060,11 +3457,11 @@ function compileFeatureExpression(source, options = {}) {
3060
3457
  ok: false,
3061
3458
  errors: validation.errors
3062
3459
  };
3063
- const zoom = options.zoom ?? 0;
3460
+ const compiledZoom = options.zoom ?? 0;
3064
3461
  return {
3065
3462
  ok: true,
3066
3463
  errors: [],
3067
- evaluate: (feature) => expression.evaluateWithoutErrorHandling({ zoom }, {
3464
+ evaluate: (feature, zoom = compiledZoom) => expression.evaluateWithoutErrorHandling({ zoom }, {
3068
3465
  type: feature.geometry?.type ?? "Unknown",
3069
3466
  properties: feature.properties ?? {},
3070
3467
  ...feature.id !== void 0 ? { id: feature.id } : {},
@@ -3698,6 +4095,23 @@ function vectorFillOpacityValue(style, fallback) {
3698
4095
  return ruleOverrideValue(style, "fillOpacity", fallback);
3699
4096
  }
3700
4097
  /**
4098
+ * The color-ramp colors a density heatmap paints with.
4099
+ *
4100
+ * {@link getVectorColorRamp} falls back to the *first* ramp ("viridis") for an
4101
+ * unknown name, which is not the heatmap default ("turbo"). Both the map paint
4102
+ * (`heatmapPaint` in `@geolibre/map`) and the auto-legend gradient resolve the
4103
+ * ramp here so a stale or hand-edited `heatmapColorRamp` cannot make the legend
4104
+ * and the rendered heatmap disagree about which ramp is in use.
4105
+ *
4106
+ * @param style - The layer style.
4107
+ * @returns The ramp's colors, or the default ramp's colors for an unknown name.
4108
+ */
4109
+ function heatmapRampColors(style) {
4110
+ const value = styleValue(style, "heatmapColorRamp");
4111
+ const ramp = getVectorColorRamp(value);
4112
+ return (ramp.value === value ? ramp : getVectorColorRamp(DEFAULT_LAYER_STYLE.heatmapColorRamp)).colors;
4113
+ }
4114
+ /**
3701
4115
  * The proportional-size configuration, or `null` when proportional sizing
3702
4116
  * does not apply: disabled, no field chosen, non-finite or degenerate
3703
4117
  * (`maxValue <= minValue`) value range, or non-finite radii. This is the
@@ -3937,6 +4351,236 @@ function extrusionColorValue(style) {
3937
4351
  return parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionColorExpression") : "") ?? flat;
3938
4352
  }
3939
4353
  //#endregion
4354
+ //#region src/document-locale.ts
4355
+ /**
4356
+ * The document language, so formatted numbers and dates follow the UI locale.
4357
+ *
4358
+ * Shared by the Identify popup's value formatting and by label number
4359
+ * formatting, which both fall back to it when the author has not pinned an
4360
+ * explicit locale. Returns `undefined` outside a browser (tests, the headless
4361
+ * project authoring paths), which leaves `Intl` on its own default.
4362
+ */
4363
+ function documentLocale() {
4364
+ return (typeof document !== "undefined" ? document.documentElement.lang.trim() : "") || void 0;
4365
+ }
4366
+ //#endregion
4367
+ //#region src/label-number-format.ts
4368
+ /**
4369
+ * Locale-aware number formatting for map labels (issue #2336).
4370
+ *
4371
+ * A numeric attribute rendered straight into a label reads as `1234567.5`,
4372
+ * which is hard to compare at a glance. QGIS solves this with
4373
+ * `format_number(field, places, language)`; the equivalent here is three
4374
+ * {@link LabelStyle} fields — {@link LabelStyle.numberFormatEnabled},
4375
+ * {@link LabelStyle.numberDecimals} and {@link LabelStyle.numberLocale} —
4376
+ * whose effect this module renders two ways:
4377
+ *
4378
+ * - {@link labelFieldTextField} builds the MapLibre `text-field` expression the
4379
+ * 2D map and the Mapbox/MapLibre style export both use, so the map and the
4380
+ * exported style agree.
4381
+ * - {@link formatLabelNumber} does the same in JavaScript for the paths that
4382
+ * resolve label text themselves: the Cesium globe and the duplicate-label
4383
+ * collapsing modes.
4384
+ *
4385
+ * Formatting applies to {@link LabelStyle.field} only. A label
4386
+ * {@link LabelStyle.expression} formats its own output (with `number-format`,
4387
+ * offered in the Expression Builder), so wrapping it here would fight the
4388
+ * author.
4389
+ */
4390
+ /**
4391
+ * Locales offered for {@link LabelStyle.numberLocale}, alongside the default
4392
+ * empty value that follows the app's own language.
4393
+ *
4394
+ * Deliberately short, and every entry groups with a character the map's glyph
4395
+ * stack can draw: ASCII `,`/`.` or U+00A0. Locales whose CLDR grouping uses
4396
+ * the narrow no-break space U+202F (`fr-FR`) or non-Latin digits (`ar-EG`)
4397
+ * are left out because a missing glyph renders as a blank box on the map
4398
+ * rather than as a separator. An author who needs one of those can still
4399
+ * write `["number-format", …]` in the label expression.
4400
+ */
4401
+ const LABEL_NUMBER_LOCALES = [
4402
+ "en-US",
4403
+ "de-DE",
4404
+ "ru-RU",
4405
+ "hi-IN"
4406
+ ];
4407
+ /**
4408
+ * Magnitude at which the text-field stops treating a value as a finite number.
4409
+ *
4410
+ * JSON carries no `Infinity`, so the expression cannot test against it
4411
+ * directly; this is the largest power of ten a double holds, which doubles as
4412
+ * the `to-number` fallback so an unconvertible value fails the same test. A
4413
+ * real attribute at or above it labels as plain text instead of a formatted
4414
+ * number -- a trade worth making for values already past 1e308.
4415
+ */
4416
+ const NON_FINITE_SENTINEL = 1e308;
4417
+ /** Sample value used to preview a locale's separators in the Style panel. */
4418
+ const LABEL_NUMBER_SAMPLE = 1234567.5;
4419
+ /**
4420
+ * Cached `Intl.NumberFormat`s, keyed by locale + decimals. `formatLabelNumber`
4421
+ * runs once per labeled feature (per Cesium entity, per deduplicated point), so
4422
+ * constructing a formatter on every call is the wrong order of magnitude.
4423
+ *
4424
+ * The key space is small: {@link LABEL_NUMBER_LOCALES} plus the app's own
4425
+ * language, times eleven decimal settings, times whatever a hand-edited project
4426
+ * pins.
4427
+ */
4428
+ const formatterCache = /* @__PURE__ */ new Map();
4429
+ /** Locale tags already accepted or rejected, so each is tested once. */
4430
+ const localeSupport = /* @__PURE__ */ new Map();
4431
+ /**
4432
+ * A formatter for this locale/decimals pair. Grouping is always on: it is the
4433
+ * whole point of the setting. The locale must already have been vetted by
4434
+ * {@link resolveLabelNumberLocale}, so this never throws.
4435
+ */
4436
+ function labelNumberFormatter(locale, decimals) {
4437
+ const digits = clampLabelDecimals(decimals);
4438
+ const key = `${locale ?? ""}\u0000${digits}`;
4439
+ const cached = formatterCache.get(key);
4440
+ if (cached) return cached;
4441
+ const formatter = new Intl.NumberFormat(locale, {
4442
+ useGrouping: true,
4443
+ minimumFractionDigits: digits,
4444
+ maximumFractionDigits: digits
4445
+ });
4446
+ formatterCache.set(key, formatter);
4447
+ return formatter;
4448
+ }
4449
+ /**
4450
+ * Whether every character a locale's grouping produces is one the map's glyph
4451
+ * stack can draw: ASCII, or the no-break space U+00A0.
4452
+ *
4453
+ * {@link LABEL_NUMBER_LOCALES} is curated on exactly this basis, but the picker
4454
+ * is not the only way a locale reaches the renderer. A hand-edited project can
4455
+ * pin any tag, and the default (`numberLocale: ""`) follows the app's own
4456
+ * language, so a French or Arabic UI would otherwise put U+202F or Arabic-Indic
4457
+ * digits into the `text-field` and label the map with blank boxes. Testing the
4458
+ * rendered sample rather than the tag covers both routes and needs no list to
4459
+ * keep in sync.
4460
+ */
4461
+ function glyphSafeLocale(tag) {
4462
+ try {
4463
+ const format = new Intl.NumberFormat(tag, {
4464
+ useGrouping: true,
4465
+ minimumFractionDigits: 2,
4466
+ maximumFractionDigits: 2
4467
+ });
4468
+ for (const sample of [format.format(LABEL_NUMBER_SAMPLE), format.format(-1234567.5)]) for (const char of sample) {
4469
+ const code = char.codePointAt(0) ?? 0;
4470
+ if (code >= 128 && code !== 160) return false;
4471
+ }
4472
+ return true;
4473
+ } catch {
4474
+ return false;
4475
+ }
4476
+ }
4477
+ /** Clamp a decimal-places setting to the range the UI and `Intl` both accept. */
4478
+ function clampLabelDecimals(decimals) {
4479
+ if (!Number.isFinite(decimals)) return 0;
4480
+ return Math.max(0, Math.min(10, Math.trunc(decimals)));
4481
+ }
4482
+ /**
4483
+ * The first locale tag that both `Intl` accepts and the map can draw, or
4484
+ * `undefined` to leave `Intl` and MapLibre on the runtime default.
4485
+ *
4486
+ * Every path resolves its locale through here -- the JavaScript formatter, the
4487
+ * MapLibre expression, and the dedup cache key -- so all three agree on which
4488
+ * locale is actually in effect.
4489
+ */
4490
+ function resolveLabelNumberLocale(...tags) {
4491
+ for (const tag of tags) {
4492
+ if (!tag) continue;
4493
+ let usable = localeSupport.get(tag);
4494
+ if (usable === void 0) {
4495
+ usable = glyphSafeLocale(tag);
4496
+ localeSupport.set(tag, usable);
4497
+ }
4498
+ if (usable) return tag;
4499
+ }
4500
+ }
4501
+ /**
4502
+ * Format one label value as a number, or `null` when the setting is off or the
4503
+ * value is not a finite number (a text attribute keeps its own rendering).
4504
+ *
4505
+ * @param value - The raw attribute value.
4506
+ * @param labels - The layer's label configuration.
4507
+ * @param fallbackLocale - Locale used when {@link LabelStyle.numberLocale} is
4508
+ * empty; the caller passes the app's language. Omitted means the runtime
4509
+ * default, matching how popups format their numbers.
4510
+ */
4511
+ function formatLabelNumber(value, labels, fallbackLocale) {
4512
+ if (!labels.numberFormatEnabled) return null;
4513
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
4514
+ return labelNumberFormatter(resolveLabelNumberLocale(labels.numberLocale, fallbackLocale), labels.numberDecimals).format(value);
4515
+ }
4516
+ /**
4517
+ * The MapLibre expression that renders {@link LabelStyle.field} as text, with
4518
+ * number formatting applied when it is on. Returns `""` when no field is set,
4519
+ * which callers treat as "no label text".
4520
+ *
4521
+ * Number formatting is guarded by a `typeof` test rather than applied blindly
4522
+ * so a mixed or text column still labels normally; only real numbers take the
4523
+ * `number-format` branch.
4524
+ */
4525
+ function labelFieldTextField(labels, fallbackLocale) {
4526
+ const field = labels.field;
4527
+ if (!field) return "";
4528
+ const asText = ["to-string", [
4529
+ "coalesce",
4530
+ ["get", field],
4531
+ ""
4532
+ ]];
4533
+ if (!labels.numberFormatEnabled) return asText;
4534
+ const locale = resolveLabelNumberLocale(labels.numberLocale, fallbackLocale);
4535
+ const digits = clampLabelDecimals(labels.numberDecimals);
4536
+ const options = {};
4537
+ if (locale) options.locale = locale;
4538
+ const numeric = [
4539
+ "to-number",
4540
+ ["get", field],
4541
+ NON_FINITE_SENTINEL
4542
+ ];
4543
+ const number = digits > 0 ? numeric : ["round", numeric];
4544
+ if (digits > 0) {
4545
+ options["min-fraction-digits"] = digits;
4546
+ options["max-fraction-digits"] = digits;
4547
+ }
4548
+ return [
4549
+ "case",
4550
+ [
4551
+ "all",
4552
+ [
4553
+ "==",
4554
+ ["typeof", ["get", field]],
4555
+ "number"
4556
+ ],
4557
+ [
4558
+ "<",
4559
+ ["abs", numeric],
4560
+ NON_FINITE_SENTINEL
4561
+ ]
4562
+ ],
4563
+ [
4564
+ "number-format",
4565
+ number,
4566
+ options
4567
+ ],
4568
+ asText
4569
+ ];
4570
+ }
4571
+ /**
4572
+ * Render {@link LABEL_NUMBER_SAMPLE} the way a locale/decimals pair would, so
4573
+ * the Style panel can name each choice by the separators it actually produces
4574
+ * ("1,234,567.50") instead of by an opaque language tag.
4575
+ *
4576
+ * A tag `Intl` rejects (or one the map cannot draw) falls back to the runtime
4577
+ * locale's formatting rather than to an unformatted number, matching what
4578
+ * {@link formatLabelNumber} and the emitted expression do with the same tag.
4579
+ */
4580
+ function formatLabelNumberSample(locale, decimals) {
4581
+ return labelNumberFormatter(resolveLabelNumberLocale(locale), decimals).format(LABEL_NUMBER_SAMPLE);
4582
+ }
4583
+ //#endregion
3940
4584
  //#region src/external-native-paint.ts
3941
4585
  /**
3942
4586
  * Whether a layer is drawn by a **custom render layer** its control created — a
@@ -4630,6 +5274,10 @@ function valueToString(value) {
4630
5274
  function layerJoinKey(value) {
4631
5275
  return value === null || value === void 0 || typeof value === "number" && Number.isNaN(value) || valueToString(value) === "" ? null : valueToString(value);
4632
5276
  }
5277
+ /** GeoJSON columns are own properties; inherited object members are not data. */
5278
+ function fieldValue(properties, field) {
5279
+ return properties && Object.hasOwn(properties, field) ? properties[field] : void 0;
5280
+ }
4633
5281
  /**
4634
5282
  * Remove every column previously added by `joins` (per their `addedFields`
4635
5283
  * bookkeeping) from a copy of `features`, restoring the pre-join properties.
@@ -4722,7 +5370,7 @@ function applyLayerJoins(features, joins, resolveSource) {
4722
5370
  }
4723
5371
  const lookup = /* @__PURE__ */ new Map();
4724
5372
  for (const jf of joinFeatures) {
4725
- const key = layerJoinKey(jf.properties?.[join.joinField]);
5373
+ const key = layerJoinKey(fieldValue(jf.properties, join.joinField));
4726
5374
  if (key === null || lookup.has(key)) continue;
4727
5375
  lookup.set(key, jf.properties ?? {});
4728
5376
  }
@@ -4730,17 +5378,17 @@ function applyLayerJoins(features, joins, resolveSource) {
4730
5378
  const targetKeys = /* @__PURE__ */ new Set();
4731
5379
  for (const feature of out) {
4732
5380
  const props = feature.properties;
4733
- const key = layerJoinKey(props[join.targetField]);
5381
+ const key = layerJoinKey(fieldValue(props, join.targetField));
4734
5382
  if (key !== null) targetKeys.add(key);
4735
5383
  const row = key === null ? void 0 : lookup.get(key);
4736
5384
  if (row) {
4737
5385
  matched += 1;
4738
- for (const [field, outputName] of fieldPairs) props[outputName] = row[field] !== void 0 ? row[field] : null;
5386
+ for (const [field, outputName] of fieldPairs) props[outputName] = fieldValue(row, field) ?? null;
4739
5387
  } else for (const [, outputName] of fieldPairs) props[outputName] = null;
4740
5388
  }
4741
5389
  let unmatchedJoin = 0;
4742
5390
  for (const jf of joinFeatures) {
4743
- const key = layerJoinKey(jf.properties?.[join.joinField]);
5391
+ const key = layerJoinKey(fieldValue(jf.properties, join.joinField));
4744
5392
  if (key !== null && !targetKeys.has(key)) unmatchedJoin += 1;
4745
5393
  }
4746
5394
  return {
@@ -5150,6 +5798,42 @@ function clearQuickFilterValues(filters) {
5150
5798
  }));
5151
5799
  }
5152
5800
  //#endregion
5801
+ //#region src/layer-filters.ts
5802
+ /**
5803
+ * Return a layer's persisted expression filter when it has a usable array
5804
+ * shape, otherwise return null.
5805
+ *
5806
+ * @param layer - The layer whose expression filter should be read.
5807
+ * @returns The active expression filter, or null when none is configured.
5808
+ */
5809
+ function activeLayerFilterExpression(layer) {
5810
+ const expression = layer.filterExpression;
5811
+ return Array.isArray(expression) && expression.length > 0 ? expression : null;
5812
+ }
5813
+ /**
5814
+ * Compile all project-authored filters for a layer into one MapLibre boolean
5815
+ * expression. An explicit expression and Quick Filter controls narrow the
5816
+ * layer together.
5817
+ *
5818
+ * @param layer - The layer whose authored filters should be composed.
5819
+ * @returns A MapLibre filter expression, or null when no authored filter is active.
5820
+ */
5821
+ function compileLayerFilters(layer) {
5822
+ const filters = [activeLayerFilterExpression(layer), compileQuickFilters(layer.quickFilters)].filter((filter) => filter !== null);
5823
+ if (filters.length === 0) return null;
5824
+ return filters.length === 1 ? filters[0] : ["all", ...filters];
5825
+ }
5826
+ /**
5827
+ * Whether a project-authored expression or Quick Filter currently narrows a
5828
+ * layer. This drives the filtered indicator and clear action in the layer UI.
5829
+ *
5830
+ * @param layer - The layer to inspect.
5831
+ * @returns True when at least one persisted filter is active.
5832
+ */
5833
+ function hasActiveLayerFilter(layer) {
5834
+ return compileLayerFilters(layer) !== null;
5835
+ }
5836
+ //#endregion
5153
5837
  //#region src/capabilities.ts
5154
5838
  /**
5155
5839
  * Default inferred capabilities for a layer based on its type and metadata.
@@ -5228,6 +5912,210 @@ function normalizeLayerCapabilities(raw) {
5228
5912
  }
5229
5913
  return hasAny ? caps : void 0;
5230
5914
  }
5915
+ /**
5916
+ * All supported application privilege identifiers in GeoLibre.
5917
+ */
5918
+ const ALL_APP_PRIVILEGES = [
5919
+ "layers:edit",
5920
+ "layers:add-remote",
5921
+ "layers:add-local",
5922
+ "processing:run",
5923
+ "processing:sidecar",
5924
+ "project:save",
5925
+ "project:share",
5926
+ "project:share-public",
5927
+ "plugins:install",
5928
+ "assistant:use",
5929
+ "connections:manage",
5930
+ "export:data",
5931
+ "export:image",
5932
+ "settings:manage"
5933
+ ];
5934
+ /**
5935
+ * Standard privilege bundles for named application roles.
5936
+ */
5937
+ const ROLE_PRIVILEGES = {
5938
+ viewer: ["export:image", "export:data"],
5939
+ editor: [
5940
+ "export:image",
5941
+ "export:data",
5942
+ "layers:edit",
5943
+ "layers:add-local",
5944
+ "layers:add-remote",
5945
+ "processing:run",
5946
+ "project:save"
5947
+ ],
5948
+ publisher: [
5949
+ "export:image",
5950
+ "export:data",
5951
+ "layers:edit",
5952
+ "layers:add-local",
5953
+ "layers:add-remote",
5954
+ "processing:run",
5955
+ "project:save",
5956
+ "project:share",
5957
+ "project:share-public",
5958
+ "processing:sidecar",
5959
+ "assistant:use"
5960
+ ],
5961
+ administrator: ALL_APP_PRIVILEGES
5962
+ };
5963
+ /**
5964
+ * Resolves the effective privilege list for a given role, applying custom overrides if specified.
5965
+ */
5966
+ function resolveRolePrivileges(role, customPrivileges) {
5967
+ if (role === "custom") {
5968
+ if (!customPrivileges || customPrivileges.length === 0) return [];
5969
+ const validSet = new Set(ALL_APP_PRIVILEGES);
5970
+ return [...new Set(customPrivileges.filter((p) => validSet.has(p)))];
5971
+ }
5972
+ return [...ROLE_PRIVILEGES[role]];
5973
+ }
5974
+ /**
5975
+ * Intersects multiple sets of privileges to derive the effective permissions when multiple
5976
+ * policies (e.g. deployment, organization, and share link) apply simultaneously.
5977
+ */
5978
+ function intersectPrivileges(...privilegeSets) {
5979
+ if (privilegeSets.length === 0) return [];
5980
+ if (privilegeSets.length === 1) return [...new Set(privilegeSets[0])];
5981
+ let current = new Set(privilegeSets[0]);
5982
+ for (let i = 1; i < privilegeSets.length; i++) {
5983
+ const nextSet = new Set(privilegeSets[i]);
5984
+ current = new Set([...current].filter((p) => nextSet.has(p)));
5985
+ }
5986
+ return [...current];
5987
+ }
5988
+ /**
5989
+ * Evaluates whether an application capability set grants a specific privilege.
5990
+ */
5991
+ function hasAppPrivilege(capabilities, privilege) {
5992
+ if (!capabilities) return true;
5993
+ return capabilities.privileges.includes(privilege);
5994
+ }
5995
+ /**
5996
+ * Resolves why a privilege is withheld: its own reason if it has one, otherwise
5997
+ * the reason recorded for the capability set as a whole.
5998
+ *
5999
+ * @param capabilities - The active capability set, if any.
6000
+ * @param privilege - The privilege being explained.
6001
+ * @returns The reason, or undefined when none was recorded.
6002
+ */
6003
+ function appPrivilegeReason(capabilities, privilege) {
6004
+ if (!capabilities) return void 0;
6005
+ return capabilities.privilegeReasons?.[privilege] ?? capabilities.reason;
6006
+ }
6007
+ /**
6008
+ * Creates the default unconstrained application capabilities (Administrator role).
6009
+ */
6010
+ function createDefaultAppCapabilities() {
6011
+ return {
6012
+ role: "administrator",
6013
+ privileges: [...ALL_APP_PRIVILEGES]
6014
+ };
6015
+ }
6016
+ /**
6017
+ * Normalizes an untrusted array of privilege strings.
6018
+ */
6019
+ function normalizeAppPrivileges(raw) {
6020
+ if (!Array.isArray(raw)) return void 0;
6021
+ const validSet = new Set(ALL_APP_PRIVILEGES);
6022
+ const result = [];
6023
+ for (const item of raw) if (typeof item === "string" && validSet.has(item)) result.push(item);
6024
+ return result.length > 0 ? [...new Set(result)] : [];
6025
+ }
6026
+ //#endregion
6027
+ //#region src/deployment-capabilities.ts
6028
+ /** Every capability, in a stable order for docs and tests. */
6029
+ const DEPLOYMENT_CAPABILITIES = [
6030
+ "project:edit",
6031
+ "data:add",
6032
+ "processing:run",
6033
+ "export:data",
6034
+ "plugins:install",
6035
+ "settings:manage"
6036
+ ];
6037
+ /**
6038
+ * The default grant: everything. `full` must stay the default so an existing
6039
+ * deployment that configures nothing behaves exactly as it did before.
6040
+ */
6041
+ const ALL_DEPLOYMENT_CAPABILITIES = new Set(DEPLOYMENT_CAPABILITIES);
6042
+ /** Whether an arbitrary string names a capability we know how to enforce. */
6043
+ function isDeploymentCapability(value) {
6044
+ return DEPLOYMENT_CAPABILITIES.includes(value);
6045
+ }
6046
+ /**
6047
+ * The reserved token that grants nothing, for a deployment that wants the map
6048
+ * and nothing else.
6049
+ *
6050
+ * Needed because a blank value cannot mean it: the deployment env reader treats
6051
+ * an empty string as unset (a `-e VAR=` produces one), and unset has to keep
6052
+ * meaning "full" so existing deployments are unchanged. Without a spelling for
6053
+ * "none", the most locked-down configuration would be the one you cannot write.
6054
+ */
6055
+ const NO_DEPLOYMENT_CAPABILITIES = "none";
6056
+ /**
6057
+ * Parse a comma-separated capability list from deployment configuration.
6058
+ *
6059
+ * Unknown tokens are dropped rather than throwing: a newer container image may
6060
+ * be configured with a capability this build does not know about, and the safe
6061
+ * reading of an unrecognized grant is "not granted".
6062
+ *
6063
+ * Fails closed on purpose. An operator who set the variable at all meant to
6064
+ * restrict something, so a list that parses to nothing grants nothing — only an
6065
+ * absent (or blank) value falls back to the full set, which is the caller's job
6066
+ * to detect.
6067
+ *
6068
+ * @param raw - The configured value, e.g. `"data:add, export:data"`.
6069
+ * @returns The granted capabilities, with duplicates and unknown names removed.
6070
+ */
6071
+ function parseDeploymentCapabilities(raw) {
6072
+ const granted = /* @__PURE__ */ new Set();
6073
+ if (raw.trim().toLowerCase() === "none") return granted;
6074
+ for (const token of raw.split(",")) {
6075
+ const name = token.trim();
6076
+ if (isDeploymentCapability(name)) granted.add(name);
6077
+ }
6078
+ return granted;
6079
+ }
6080
+ //#endregion
6081
+ //#region src/file-name.ts
6082
+ /**
6083
+ * A bare Android SAF document ID, as the browser `File.name` carries after a
6084
+ * native import: a known volume prefix (`primary:`, `home:`, `raw:`, `msf:`, or a
6085
+ * removable `XXXX-XXXX:` volume), or any provider prefix followed by a folder
6086
+ * separator. Ordinary encoded filenames such as `Report%3AQ1.geojson` do not match.
6087
+ */
6088
+ const SAF_DOCUMENT_ID = /^(?:(?:primary|home|raw|msf|[0-9a-f]{4}-[0-9a-f]{4})%3a|[^%/\\:]+%3a.*%2f)/i;
6089
+ /** Extract a display filename without changing the source used for file access. */
6090
+ function localFileName(path) {
6091
+ const isUri = /^(?:content|file):\/\//i.test(path);
6092
+ const value = isUri ? path.split(/[?#]/)[0] : path;
6093
+ const segment = value.split(/[/\\]/).pop() ?? value;
6094
+ if (!isUri && (!SAF_DOCUMENT_ID.test(segment) || /[/\\]/.test(path))) return segment;
6095
+ let decoded = segment;
6096
+ try {
6097
+ decoded = decodeURIComponent(segment);
6098
+ } catch {
6099
+ return segment;
6100
+ }
6101
+ return decoded.replace(/^[^/\\:]+:/, "").split(/[/\\]/).pop() ?? decoded;
6102
+ }
6103
+ /**
6104
+ * Whether a layer name was derived from its local source filename (with or
6105
+ * without the extension) rather than supplied explicitly by a caller.
6106
+ */
6107
+ function isSourceDerivedLayerName(name, sourcePath) {
6108
+ const fileName = localFileName(sourcePath);
6109
+ return name === fileName || name === fileName.replace(/\.[^.]+$/, "");
6110
+ }
6111
+ /** Keep imported layers distinguishable, including within a multi-file import. */
6112
+ function uniqueImportedLayerName(name, names) {
6113
+ const existing = new Set(names);
6114
+ let candidate = name;
6115
+ let suffix = 2;
6116
+ while (existing.has(candidate)) candidate = `${name}_${suffix++}`;
6117
+ return candidate;
6118
+ }
5231
6119
  //#endregion
5232
6120
  //#region src/history.ts
5233
6121
  /**
@@ -6833,6 +7721,9 @@ function scrubPrintLayoutForRemovedLayers(config, layerIds) {
6833
7721
  //#region src/project.ts
6834
7722
  /** Placeholder name a project carries before the user names it. */
6835
7723
  const DEFAULT_PROJECT_NAME = "Untitled Project";
7724
+ function normalizeBlankBackgroundColor(value) {
7725
+ return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value) ? value : null;
7726
+ }
6836
7727
  function createDefaultMapView() {
6837
7728
  return {
6838
7729
  center: [-100, 40],
@@ -6849,6 +7740,7 @@ function createEmptyProject(name = DEFAULT_PROJECT_NAME, options = {}) {
6849
7740
  basemapStyleUrl: options.basemapStyleUrl ?? "https://tiles.openfreemap.org/styles/liberty",
6850
7741
  basemapVisible: true,
6851
7742
  basemapOpacity: 1,
7743
+ blankBackgroundColor: null,
6852
7744
  layers: [],
6853
7745
  layerGroups: [],
6854
7746
  styles: {},
@@ -6970,6 +7862,7 @@ function parseProject(json) {
6970
7862
  const basemapStyleUrl = data.basemapStyleUrl ?? "https://tiles.openfreemap.org/styles/liberty";
6971
7863
  const basemapVisible = data.basemapVisible ?? true;
6972
7864
  const basemapOpacity = data.basemapOpacity ?? 1;
7865
+ const blankBackgroundColor = normalizeBlankBackgroundColor(data.blankBackgroundColor);
6973
7866
  const mapView = normalizeMapViewState(data.mapView);
6974
7867
  const { mapLayout, secondaryMapViews } = resolveMapGrid(normalizeMapLayout(data.mapLayout), normalizeSecondaryMapViews(data.secondaryMapViews), { mapView });
6975
7868
  const styleLibrary = normalizeStyleLibraryEntries(data.styleLibrary);
@@ -6981,6 +7874,7 @@ function parseProject(json) {
6981
7874
  basemapStyleUrl,
6982
7875
  basemapVisible,
6983
7876
  basemapOpacity,
7877
+ blankBackgroundColor,
6984
7878
  layers,
6985
7879
  ...selectedLayerId !== void 0 ? { selectedLayerId } : {},
6986
7880
  ...layerGroups.length > 0 ? { layerGroups } : {},
@@ -6999,6 +7893,7 @@ function parseProject(json) {
6999
7893
  secondaryMapViews,
7000
7894
  ...normalizeString(data.primaryMapLabel) ? { primaryMapLabel: normalizeString(data.primaryMapLabel) } : {}
7001
7895
  } : {},
7896
+ ...normalizePrimaryRenderer(data.primaryRenderer) ? { primaryRenderer: "cesium" } : {},
7002
7897
  ...styleLibrary.length > 0 ? { styleLibrary } : {},
7003
7898
  ...parsedComments.length > 0 ? { comments: parsedComments } : {},
7004
7899
  metadata: data.metadata ?? {}
@@ -7464,6 +8359,18 @@ function normalizeMapLayout(value) {
7464
8359
  };
7465
8360
  }
7466
8361
  /**
8362
+ * Coerce an untrusted `primaryRenderer` into a known engine id (issue #2217).
8363
+ *
8364
+ * Returns null for the default 2D map — absent, unknown, or an explicit
8365
+ * `"maplibre"` — because the field is only written when it is not the default,
8366
+ * so a MapLibre project serializes byte-identically to before this existed.
8367
+ * The return type is narrowed to `"cesium" | null` rather than the full
8368
+ * {@link MapRendererKind} for that reason: `"maplibre"` is never a result.
8369
+ */
8370
+ function normalizePrimaryRenderer(value) {
8371
+ return value === "cesium" ? "cesium" : null;
8372
+ }
8373
+ /**
7467
8374
  * Coerce an untrusted `secondaryMapViews` array into valid
7468
8375
  * {@link SecondaryMapView} records, dropping entries without a usable id and
7469
8376
  * de-duplicating by id. Returns null when none are valid.
@@ -7665,6 +8572,8 @@ function normalizeProjectPreferences(preferences) {
7665
8572
  ellipsoidId: getEllipsoid(map.ellipsoidId).id,
7666
8573
  scaleUnit: normalizeScaleUnit(map.scaleUnit),
7667
8574
  showPointerElevation: normalizeBoolean(map.showPointerElevation, DEFAULT_PROJECT_PREFERENCES.map.showPointerElevation),
8575
+ cesiumBasemap: normalizeCesiumBasemap(map.cesiumBasemap),
8576
+ terrainEnabled: normalizeBoolean(map.terrainEnabled, DEFAULT_PROJECT_PREFERENCES.map.terrainEnabled),
7668
8577
  coordinateFormat: typeof map.coordinateFormat === "string" ? map.coordinateFormat : DEFAULT_PROJECT_PREFERENCES.map.coordinateFormat
7669
8578
  },
7670
8579
  environmentVariables: Array.isArray(candidate.environmentVariables) ? candidate.environmentVariables.map(normalizeEnvironmentVariable).filter((variable) => Boolean(variable)) : [],
@@ -7801,8 +8710,9 @@ function isPlainObject$2(value) {
7801
8710
  return prototype === Object.prototype || prototype === null;
7802
8711
  }
7803
8712
  function normalizeLayer(layer) {
7804
- const { capabilities: rawCapabilities, ...rest } = layer;
8713
+ const { capabilities: rawCapabilities, filterExpression: rawFilterExpression, ...rest } = layer;
7805
8714
  const capabilities = normalizeLayerCapabilities(rawCapabilities);
8715
+ const filterExpression = Array.isArray(rawFilterExpression) && rawFilterExpression.length > 0 && validateMapExpression(JSON.stringify(rawFilterExpression), { expectedType: "boolean" }).ok ? rawFilterExpression : void 0;
7806
8716
  return {
7807
8717
  ...rest,
7808
8718
  style: {
@@ -7813,7 +8723,8 @@ function normalizeLayer(layer) {
7813
8723
  opacity: layer.opacity ?? 1,
7814
8724
  metadata: layer.metadata ?? {},
7815
8725
  source: layer.source ?? {},
7816
- ...capabilities ? { capabilities } : {}
8726
+ ...capabilities ? { capabilities } : {},
8727
+ ...filterExpression ? { filterExpression } : {}
7817
8728
  };
7818
8729
  }
7819
8730
  function normalizeProjectComments(rawComments) {
@@ -7897,6 +8808,7 @@ function projectFromStore(state) {
7897
8808
  basemapStyleUrl: state.basemapStyleUrl,
7898
8809
  basemapVisible: state.basemapVisible,
7899
8810
  basemapOpacity: state.basemapOpacity,
8811
+ ...state.blankBackgroundColor ? { blankBackgroundColor: state.blankBackgroundColor } : {},
7900
8812
  layers: state.layers.map(prepareLayerForSave),
7901
8813
  ...selectedLayerId !== void 0 ? { selectedLayerId } : {},
7902
8814
  ...layerGroups.length > 0 ? { layerGroups } : {},
@@ -7915,6 +8827,7 @@ function projectFromStore(state) {
7915
8827
  secondaryMapViews,
7916
8828
  ...normalizeString(state.primaryMapLabel) ? { primaryMapLabel: normalizeString(state.primaryMapLabel) } : {}
7917
8829
  } : {},
8830
+ ...normalizePrimaryRenderer(state.primaryRenderer) ? { primaryRenderer: "cesium" } : {},
7918
8831
  ...styleLibrary.length > 0 ? { styleLibrary } : {},
7919
8832
  ...comments.length > 0 ? { comments } : {},
7920
8833
  metadata: state.metadata
@@ -7926,6 +8839,13 @@ function hasRestorableSourceUrl(layer) {
7926
8839
  return typeof sourceUrl === "string" && sourceUrl.trim() !== "" || typeof originalUrl === "string" && originalUrl.trim() !== "";
7927
8840
  }
7928
8841
  function prepareLayerForSave(layer) {
8842
+ if (layer.metadata.geometryEdited !== void 0) {
8843
+ const { geometryEdited: _geometryEdited, ...metadata } = layer.metadata;
8844
+ layer = {
8845
+ ...layer,
8846
+ metadata
8847
+ };
8848
+ }
7929
8849
  if (layer.timeFilter !== void 0) {
7930
8850
  const { timeFilter: _timeFilter, ...rest } = layer;
7931
8851
  layer = rest;
@@ -7943,6 +8863,18 @@ function prepareLayerForSave(layer) {
7943
8863
  const { geojson: _geojson, ...rest } = layer;
7944
8864
  layer = rest;
7945
8865
  }
8866
+ if (layer.type === "wms") {
8867
+ const tiles = layer.source.tiles;
8868
+ if (!Array.isArray(tiles)) return layer;
8869
+ const portableTiles = tiles.map((tile) => portableWmsTileUrl(tile));
8870
+ return portableTiles.some((tile, index) => tile !== tiles[index]) ? {
8871
+ ...layer,
8872
+ source: {
8873
+ ...layer.source,
8874
+ tiles: portableTiles
8875
+ }
8876
+ } : layer;
8877
+ }
7946
8878
  if (layer.type !== "xyz") return layer;
7947
8879
  const originalUrl = typeof layer.metadata.originalUrl === "string" && layer.metadata.originalUrl.trim() ? layer.metadata.originalUrl : typeof layer.source.url === "string" && layer.source.url.trim() ? layer.source.url : null;
7948
8880
  if (!originalUrl) return layer;
@@ -7958,11 +8890,23 @@ function prepareLayerForSave(layer) {
7958
8890
  metadata
7959
8891
  };
7960
8892
  }
8893
+ function portableWmsTileUrl(tile) {
8894
+ if (typeof tile !== "string" || !tile.startsWith("geolibre-wms://")) return tile;
8895
+ try {
8896
+ const url = new URL(tile).searchParams.get("url");
8897
+ if (!url) return tile;
8898
+ const parsed = new URL(url);
8899
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? url : tile;
8900
+ } catch {
8901
+ return tile;
8902
+ }
8903
+ }
7961
8904
  function applyProjectToStore(project) {
7962
8905
  const layers = project.layers.map((layer) => ({
7963
8906
  ...layer,
7964
8907
  style: project.styles[layer.id] ? {
7965
8908
  ...DEFAULT_LAYER_STYLE,
8909
+ ...layer.style,
7966
8910
  ...project.styles[layer.id]
7967
8911
  } : {
7968
8912
  ...DEFAULT_LAYER_STYLE,
@@ -7978,6 +8922,7 @@ function applyProjectToStore(project) {
7978
8922
  const basemapStyleUrl = project.basemapStyleUrl;
7979
8923
  const basemapVisible = project.basemapVisible ?? true;
7980
8924
  const basemapOpacity = project.basemapOpacity ?? 1;
8925
+ const blankBackgroundColor = normalizeBlankBackgroundColor(project.blankBackgroundColor);
7981
8926
  const mapView = normalizeMapViewState(project.mapView);
7982
8927
  const { mapLayout, secondaryMapViews } = resolveMapGrid(normalizeMapLayout(project.mapLayout), normalizeSecondaryMapViews(project.secondaryMapViews), { mapView });
7983
8928
  const existingLayerIds = new Set(normalizedLayers.map((l) => l.id));
@@ -8006,6 +8951,7 @@ function applyProjectToStore(project) {
8006
8951
  basemapStyleUrl,
8007
8952
  basemapVisible,
8008
8953
  basemapOpacity,
8954
+ blankBackgroundColor,
8009
8955
  layers: normalizedLayers,
8010
8956
  layerGroups,
8011
8957
  preferences: normalizeProjectPreferences(project.preferences),
@@ -8020,6 +8966,7 @@ function applyProjectToStore(project) {
8020
8966
  mapLayout,
8021
8967
  secondaryMapViews,
8022
8968
  primaryMapLabel: normalizeString(project.primaryMapLabel),
8969
+ primaryRenderer: normalizePrimaryRenderer(project.primaryRenderer) ?? "maplibre",
8023
8970
  projectStyleLibrary: normalizeStyleLibraryEntries(project.styleLibrary),
8024
8971
  comments: scrubbedComments,
8025
8972
  metadata: project.metadata
@@ -8364,6 +9311,313 @@ function rescaleEntryCount(state) {
8364
9311
  return 1;
8365
9312
  }
8366
9313
  //#endregion
9314
+ //#region src/czml.ts
9315
+ /**
9316
+ * CZML (Cesium Language) domain models and layer authoring (issue #2290).
9317
+ *
9318
+ * CZML is a JSON-based format for describing dynamic, time-varying 3D geospatial
9319
+ * scenes on the Cesium globe (trajectories, satellites, vehicles, sensor
9320
+ * networks, moving entities with orientations and paths). The globe loads CZML
9321
+ * natively through `CzmlDataSource`; the 2D map has no equivalent, so CZML
9322
+ * layers are badged "3D only" there.
9323
+ */
9324
+ /** `metadata.sourceKind` of a layer that carries or references CZML data. */
9325
+ const CZML_SOURCE_KIND = "czml";
9326
+ /** One-click CZML samples offered in the Add Data dialog. */
9327
+ const CZML_QUICK_PICKS = [{
9328
+ name: "CZML Point",
9329
+ data: [{
9330
+ id: "document",
9331
+ name: "CZML Point Sample",
9332
+ version: "1.0"
9333
+ }, {
9334
+ id: "point 1",
9335
+ name: "Extruded Point",
9336
+ position: { cartographicDegrees: [
9337
+ -75.59777,
9338
+ 40.03883,
9339
+ 1e3
9340
+ ] },
9341
+ point: {
9342
+ color: { rgba: [
9343
+ 255,
9344
+ 128,
9345
+ 0,
9346
+ 255
9347
+ ] },
9348
+ pixelSize: 14,
9349
+ outlineColor: { rgba: [
9350
+ 255,
9351
+ 255,
9352
+ 255,
9353
+ 255
9354
+ ] },
9355
+ outlineWidth: 2
9356
+ }
9357
+ }]
9358
+ }, {
9359
+ name: "CZML Dynamic Trajectory",
9360
+ data: [{
9361
+ id: "document",
9362
+ name: "CZML Trajectory Sample",
9363
+ version: "1.0",
9364
+ clock: {
9365
+ interval: "2026-09-09T00:00:00Z/2026-09-09T02:00:00Z",
9366
+ currentTime: "2026-09-09T00:00:00Z",
9367
+ multiplier: 60,
9368
+ range: "LOOP_STOP"
9369
+ }
9370
+ }, {
9371
+ id: "satellite",
9372
+ name: "Satellite Track",
9373
+ availability: "2026-09-09T00:00:00Z/2026-09-09T02:00:00Z",
9374
+ path: {
9375
+ material: { solidColor: { color: { rgba: [
9376
+ 0,
9377
+ 200,
9378
+ 255,
9379
+ 255
9380
+ ] } } },
9381
+ width: 2,
9382
+ leadTime: 1800,
9383
+ trailTime: 1800
9384
+ },
9385
+ position: {
9386
+ epoch: "2026-09-09T00:00:00Z",
9387
+ cartographicDegrees: [
9388
+ 0,
9389
+ -75,
9390
+ 40,
9391
+ 25e4,
9392
+ 1800,
9393
+ -30,
9394
+ 20,
9395
+ 25e4,
9396
+ 3600,
9397
+ 20,
9398
+ 0,
9399
+ 25e4,
9400
+ 5400,
9401
+ 70,
9402
+ -20,
9403
+ 25e4,
9404
+ 7200,
9405
+ 120,
9406
+ -40,
9407
+ 25e4
9408
+ ]
9409
+ },
9410
+ point: {
9411
+ color: { rgba: [
9412
+ 255,
9413
+ 255,
9414
+ 255,
9415
+ 255
9416
+ ] },
9417
+ pixelSize: 10
9418
+ }
9419
+ }]
9420
+ }];
9421
+ /**
9422
+ * Parse and validate a CZML payload from a JSON string or raw object.
9423
+ * Returns an array of CZML packets or null when the input is malformed.
9424
+ */
9425
+ function parseCzml(value) {
9426
+ if (!value) return null;
9427
+ let parsed = value;
9428
+ if (typeof value === "string") {
9429
+ const trimmed = value.trim();
9430
+ if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) return null;
9431
+ try {
9432
+ parsed = JSON.parse(trimmed);
9433
+ } catch {
9434
+ return null;
9435
+ }
9436
+ }
9437
+ if (Array.isArray(parsed)) return parsed.length > 0 && parsed.every((item) => item && typeof item === "object") ? parsed : null;
9438
+ if (parsed && typeof parsed === "object") return [parsed];
9439
+ return null;
9440
+ }
9441
+ /**
9442
+ * Whether `layer` represents a CZML dynamic 3D scene.
9443
+ */
9444
+ function isCzmlLayer(layer) {
9445
+ return layer.metadata?.sourceKind === CZML_SOURCE_KIND;
9446
+ }
9447
+ /**
9448
+ * Extract the CZML content and/or URL from a layer, or null if not a CZML layer.
9449
+ */
9450
+ function czmlSource(layer) {
9451
+ if (!isCzmlLayer(layer)) return null;
9452
+ const raw = layer.source?.czmlData;
9453
+ const data = Array.isArray(raw) ? raw.length > 0 ? raw : void 0 : raw && typeof raw === "object" ? [raw] : raw || void 0;
9454
+ const rawUrl = layer.source?.url;
9455
+ const url = typeof rawUrl === "string" && rawUrl.trim() ? rawUrl.trim() : void 0;
9456
+ if (!data && !url) return null;
9457
+ return {
9458
+ url,
9459
+ data
9460
+ };
9461
+ }
9462
+ /** Generate a unique identifier for a newly created layer. */
9463
+ function newLayerId$1() {
9464
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
9465
+ }
9466
+ /**
9467
+ * Build a typed GeoLibreLayer representing a CZML dynamic scene on the Cesium globe.
9468
+ */
9469
+ function createCzmlLayer(options) {
9470
+ const id = options.id ?? newLayerId$1();
9471
+ const data = options.data;
9472
+ const url = options.url?.trim() || void 0;
9473
+ const sourcePath = options.sourcePath?.trim() || void 0;
9474
+ return {
9475
+ id,
9476
+ name: options.name,
9477
+ type: "3d-tiles",
9478
+ ...sourcePath ? { sourcePath } : {},
9479
+ source: {
9480
+ type: "3d-tiles",
9481
+ sourceId: id,
9482
+ ...data !== void 0 ? { czmlData: data } : {},
9483
+ ...url ? { url } : {},
9484
+ ...sourcePath ? { sourcePath } : {}
9485
+ },
9486
+ visible: true,
9487
+ opacity: 1,
9488
+ style: { ...DEFAULT_LAYER_STYLE },
9489
+ metadata: {
9490
+ sourceKind: CZML_SOURCE_KIND,
9491
+ externalNativeLayer: true,
9492
+ identifiable: false,
9493
+ sourceId: id,
9494
+ nativeLayerIds: [id]
9495
+ }
9496
+ };
9497
+ }
9498
+ //#endregion
9499
+ //#region src/cesium-kml.ts
9500
+ /** Native KML/KMZ documents retain their own styling on the globe. */
9501
+ const CESIUM_KML_SOURCE_KIND = "cesium-kml";
9502
+ function isCesiumKmlLayer(layer) {
9503
+ return layer.metadata?.sourceKind === CESIUM_KML_SOURCE_KIND;
9504
+ }
9505
+ function cesiumKmlSource(layer) {
9506
+ if (!isCesiumKmlLayer(layer)) return null;
9507
+ for (const value of [layer.source.kmlData, layer.source.url]) if (typeof value === "string" && value.trim()) return value.trim();
9508
+ return null;
9509
+ }
9510
+ function createCesiumKmlLayer(options) {
9511
+ const data = options.data?.trim();
9512
+ const url = options.url?.trim();
9513
+ if (!data && !url) throw new Error("Provide a KML/KMZ document or URL.");
9514
+ const id = options.id ?? crypto.randomUUID();
9515
+ return {
9516
+ id,
9517
+ name: options.name,
9518
+ type: "3d-tiles",
9519
+ ...options.sourcePath ? { sourcePath: options.sourcePath } : {},
9520
+ source: {
9521
+ type: "3d-tiles",
9522
+ sourceId: id,
9523
+ ...data ? { kmlData: data } : { url }
9524
+ },
9525
+ visible: true,
9526
+ opacity: 1,
9527
+ style: { ...DEFAULT_LAYER_STYLE },
9528
+ metadata: {
9529
+ sourceKind: CESIUM_KML_SOURCE_KIND,
9530
+ externalNativeLayer: true,
9531
+ identifiable: false
9532
+ }
9533
+ };
9534
+ }
9535
+ //#endregion
9536
+ //#region src/cesium-ion.ts
9537
+ /** `metadata.sourceKind` of a layer that references a Cesium Ion asset. */
9538
+ const CESIUM_ION_SOURCE_KIND = "cesium-ion";
9539
+ /** Cesium OSM Buildings, the one-click 3D Tiles asset every Ion account can use. */
9540
+ const CESIUM_OSM_BUILDINGS_ASSET_ID = 96188;
9541
+ /** Bing Maps Aerial through Ion, the imagery asset every Ion account can use. */
9542
+ const CESIUM_BING_AERIAL_ASSET_ID = 2;
9543
+ /** Ion assets offered as one-click picks in the Add Data dialog. */
9544
+ const CESIUM_ION_QUICK_PICKS = [{
9545
+ assetId: CESIUM_OSM_BUILDINGS_ASSET_ID,
9546
+ kind: "3d-tiles",
9547
+ name: "Cesium OSM Buildings"
9548
+ }, {
9549
+ assetId: 2,
9550
+ kind: "imagery",
9551
+ name: "Bing Maps Aerial (Ion)"
9552
+ }];
9553
+ /** Parse an Ion asset id the way the Add Data form and the project file carry it. */
9554
+ function parseCesiumIonAssetId(value) {
9555
+ const id = typeof value === "string" ? Number(value.trim()) : value;
9556
+ return typeof id === "number" && Number.isInteger(id) && id > 0 ? id : null;
9557
+ }
9558
+ /**
9559
+ * The Ion asset id a layer references, or null when the layer is not an Ion
9560
+ * asset layer (or carries no usable id).
9561
+ */
9562
+ function cesiumIonAssetId(layer) {
9563
+ if (layer.metadata?.sourceKind !== "cesium-ion") return null;
9564
+ return parseCesiumIonAssetId(layer.source?.ionAssetId);
9565
+ }
9566
+ /** Whether `layer` references a Cesium Ion asset. */
9567
+ function isCesiumIonLayer(layer) {
9568
+ return cesiumIonAssetId(layer) !== null;
9569
+ }
9570
+ /** What the Ion asset draws as: a tileset primitive or an imagery layer. */
9571
+ function cesiumIonAssetKind(layer) {
9572
+ return layer.type === "3d-tiles" ? "3d-tiles" : "imagery";
9573
+ }
9574
+ /**
9575
+ * Whether only the 3D globe can render `layer`: the mirror of the globe's
9576
+ * `isCesiumSupportedLayerType`, for the Layers panel to badge on the 2D map.
9577
+ */
9578
+ function isCesiumOnlyLayer(layer) {
9579
+ return isCesiumIonLayer(layer) || isCzmlLayer(layer) || isCesiumKmlLayer(layer);
9580
+ }
9581
+ function newLayerId() {
9582
+ return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
9583
+ }
9584
+ /**
9585
+ * Build the store layer for a Cesium Ion asset. A tileset is a `3d-tiles`
9586
+ * layer whose `sourceKind` keeps it out of the 2D deck.gl control; imagery is
9587
+ * a `raster` layer with no tiles, marked external so the 2D sync leaves it
9588
+ * alone. Both persist only the asset id: the token is runtime configuration.
9589
+ */
9590
+ function createCesiumIonLayer(options) {
9591
+ const id = options.id ?? newLayerId();
9592
+ const isTileset = options.kind === "3d-tiles";
9593
+ const altitudeOffset = Number.isFinite(options.altitudeOffset) ? options.altitudeOffset : 0;
9594
+ return {
9595
+ id,
9596
+ name: options.name,
9597
+ type: isTileset ? "3d-tiles" : "raster",
9598
+ source: {
9599
+ type: isTileset ? "3d-tiles" : "raster",
9600
+ ionAssetId: options.assetId,
9601
+ sourceId: id,
9602
+ ...isTileset ? { altitudeOffset } : {}
9603
+ },
9604
+ visible: true,
9605
+ opacity: 1,
9606
+ style: { ...DEFAULT_LAYER_STYLE },
9607
+ metadata: {
9608
+ sourceKind: CESIUM_ION_SOURCE_KIND,
9609
+ externalNativeLayer: true,
9610
+ identifiable: false,
9611
+ sourceId: id,
9612
+ nativeLayerIds: [id],
9613
+ ...isTileset ? {
9614
+ customLayerType: "3d-tiles",
9615
+ altitudeOffset
9616
+ } : {}
9617
+ }
9618
+ };
9619
+ }
9620
+ //#endregion
8367
9621
  //#region src/credentials.ts
8368
9622
  /**
8369
9623
  * Project fields whose values are credentials. Keep this registry as the
@@ -8414,7 +9668,8 @@ const PROJECT_CREDENTIAL_FIELDS = {
8414
9668
  */
8415
9669
  const PUBLISHABLE_PLUGIN_SETTINGS = {
8416
9670
  "maplibre-gl-swipe": null,
8417
- "maplibre-gl-components": ["legend", "colorbar"]
9671
+ "maplibre-gl-components": ["legend", "colorbar"],
9672
+ "maplibre-gl-time-slider": null
8418
9673
  };
8419
9674
  /**
8420
9675
  * Hash a credential value to a short, stable 64-bit token: two independent
@@ -8709,8 +9964,9 @@ const MAX_LAYER_LIBRARY_ENTRIES = 500;
8709
9964
  * Metadata keys dropped when capturing an entry. `resolvedUrl` is the dev-server
8710
9965
  * proxy rewrite of an XYZ template — a per-session artifact that must not be
8711
9966
  * baked into a saved source (the same reason `prepareLayerForSave` strips it).
9967
+ * `geometryEdited` tracks changes to the live layer, not a library source.
8712
9968
  */
8713
- const TRANSIENT_METADATA_KEYS = ["resolvedUrl"];
9969
+ const TRANSIENT_METADATA_KEYS = ["resolvedUrl", "geometryEdited"];
8714
9970
  function nonEmptyString(value) {
8715
9971
  return typeof value === "string" && value.trim() !== "";
8716
9972
  }
@@ -8729,6 +9985,7 @@ function hasRestorableLayerSource(layer) {
8729
9985
  if (nonEmptyString(source.url)) return true;
8730
9986
  if (nonEmptyString(source.data)) return true;
8731
9987
  if (Array.isArray(source.tiles) && source.tiles.some(nonEmptyString)) return true;
9988
+ if (cesiumIonAssetId(layer) !== null) return true;
8732
9989
  return nonEmptyString((layer.metadata ?? {}).originalUrl);
8733
9990
  }
8734
9991
  /**
@@ -9333,6 +10590,8 @@ function createLayerLibraryEntryId() {
9333
10590
  }
9334
10591
  //#endregion
9335
10592
  //#region src/store.ts
10593
+ /** Reserved Identify target for querying every visible queryable layer at once. */
10594
+ const IDENTIFY_ALL_LAYERS_ID = "__geolibre_identify_all_layers__";
9336
10595
  const MAX_RECENT_PROJECTS = 10;
9337
10596
  /**
9338
10597
  * A fresh, inactive collaboration slice (no live session). Frozen (like
@@ -9571,6 +10830,7 @@ const useAppStore = create()(temporal((set, get) => ({
9571
10830
  basemapStyleUrl: DEFAULT_BASEMAP,
9572
10831
  basemapVisible: true,
9573
10832
  basemapOpacity: 1,
10833
+ blankBackgroundColor: null,
9574
10834
  layers: [],
9575
10835
  layerGroups: [],
9576
10836
  preferences: DEFAULT_PROJECT_PREFERENCES,
@@ -9589,6 +10849,7 @@ const useAppStore = create()(temporal((set, get) => ({
9589
10849
  mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT },
9590
10850
  secondaryMapViews: [],
9591
10851
  primaryMapLabel: "",
10852
+ primaryRenderer: DEFAULT_PRIMARY_RENDERER,
9592
10853
  copiedLayerStyle: null,
9593
10854
  selectedLayerId: null,
9594
10855
  selectedFeatureId: null,
@@ -9602,7 +10863,9 @@ const useAppStore = create()(temporal((set, get) => ({
9602
10863
  metadata: {},
9603
10864
  recentProjects: [],
9604
10865
  attributeFilter: "",
10866
+ deploymentCapabilities: ALL_DEPLOYMENT_CAPABILITIES,
9605
10867
  collaboration: DEFAULT_COLLABORATION_STATE,
10868
+ capabilities: createDefaultAppCapabilities(),
9606
10869
  ui: {
9607
10870
  processingOpen: false,
9608
10871
  processingInitialTool: null,
@@ -9627,6 +10890,7 @@ const useAppStore = create()(temporal((set, get) => ({
9627
10890
  storymapPanelOpen: false,
9628
10891
  storymapPresenting: false,
9629
10892
  storymapReturnToEditor: false,
10893
+ storymapLayerOpacity: {},
9630
10894
  storymapComposingId: null,
9631
10895
  batchToolsOpen: false,
9632
10896
  modelBuilderOpen: false,
@@ -9799,6 +11063,10 @@ const useAppStore = create()(temporal((set, get) => ({
9799
11063
  primaryMapLabel: label,
9800
11064
  isDirty: true
9801
11065
  }),
11066
+ setPrimaryRenderer: (renderer) => set((s) => s.primaryRenderer === renderer ? s : {
11067
+ primaryRenderer: renderer,
11068
+ isDirty: true
11069
+ }),
9802
11070
  setSecondaryMapLabel: (id, label) => set((s) => {
9803
11071
  let changed = false;
9804
11072
  const secondaryMapViews = s.secondaryMapViews.map((pane) => {
@@ -9880,6 +11148,10 @@ const useAppStore = create()(temporal((set, get) => ({
9880
11148
  basemapOpacity: opacity,
9881
11149
  isDirty: true
9882
11150
  }),
11151
+ setBlankBackgroundColor: (color) => set({
11152
+ blankBackgroundColor: normalizeBlankBackgroundColor(color),
11153
+ isDirty: true
11154
+ }),
9883
11155
  setPreferences: (preferences) => set({
9884
11156
  preferences,
9885
11157
  isDirty: true
@@ -9996,8 +11268,23 @@ const useAppStore = create()(temporal((set, get) => ({
9996
11268
  setStorymapPresenting: (presenting, returnToEditor = false) => set((s) => ({ ui: {
9997
11269
  ...s.ui,
9998
11270
  storymapPresenting: presenting,
9999
- storymapReturnToEditor: presenting ? returnToEditor : false
11271
+ storymapReturnToEditor: presenting ? returnToEditor : false,
11272
+ storymapLayerOpacity: {}
10000
11273
  } })),
11274
+ setStorymapLayerOpacity: (changes) => set((s) => {
11275
+ const next = { ...s.ui.storymapLayerOpacity };
11276
+ let changed = false;
11277
+ for (const [layerId, opacity] of Object.entries(changes)) {
11278
+ const clamped = Math.min(1, Math.max(0, opacity));
11279
+ if (next[layerId] === clamped) continue;
11280
+ next[layerId] = clamped;
11281
+ changed = true;
11282
+ }
11283
+ return changed ? { ui: {
11284
+ ...s.ui,
11285
+ storymapLayerOpacity: next
11286
+ } } : s;
11287
+ }),
10001
11288
  setStorymapComposing: (chapterId) => set((s) => ({ ui: {
10002
11289
  ...s.ui,
10003
11290
  storymapComposingId: chapterId
@@ -10229,6 +11516,7 @@ const useAppStore = create()(temporal((set, get) => ({
10229
11516
  projectName: name,
10230
11517
  isDirty: true
10231
11518
  }),
11519
+ setDeploymentCapabilities: (capabilities) => set({ deploymentCapabilities: new Set(capabilities) }),
10232
11520
  setRecentProjects: (projects) => set({ recentProjects: normalizeRecentProjects(projects) }),
10233
11521
  rememberRecentProject: (entry) => set((s) => ({ recentProjects: normalizeRecentProjects([entry, ...s.recentProjects]) })),
10234
11522
  forgetRecentProject: (path) => {
@@ -10239,6 +11527,11 @@ const useAppStore = create()(temporal((set, get) => ({
10239
11527
  markSaved: () => set({ isDirty: false }),
10240
11528
  addLayer: (layer, beforeLayerId = null) => set((s) => {
10241
11529
  const layers = [...s.layers];
11530
+ const { sourcePath } = layer;
11531
+ if (sourcePath && (!/^[a-z][a-z0-9+.-]*:\/\//i.test(sourcePath) || /^(content|file):\/\//i.test(sourcePath)) && isSourceDerivedLayerName(layer.name, sourcePath)) layer = {
11532
+ ...layer,
11533
+ name: uniqueImportedLayerName(layer.name, layers.map((item) => item.name))
11534
+ };
10242
11535
  const beforeIndex = beforeLayerId ? layers.findIndex((l) => l.id === beforeLayerId) : -1;
10243
11536
  const layerWithBeforeId = beforeLayerId && beforeIndex < 0 ? {
10244
11537
  ...layer,
@@ -10309,6 +11602,7 @@ const useAppStore = create()(temporal((set, get) => ({
10309
11602
  };
10310
11603
  }),
10311
11604
  setLayerQuickFilters: (id, filters) => get().updateLayer(id, { quickFilters: filters.length > 0 ? filters : void 0 }),
11605
+ setLayerFilterExpression: (id, expression) => get().updateLayer(id, { filterExpression: expression ?? void 0 }),
10312
11606
  setLayerVisibility: (id, visible) => get().updateLayer(id, { visible }),
10313
11607
  setLayerOpacity: (id, opacity) => get().updateLayer(id, { opacity }),
10314
11608
  setLayerStyle: (id, style) => set((s) => ({
@@ -10641,6 +11935,7 @@ const useAppStore = create()(temporal((set, get) => ({
10641
11935
  ...s.ui,
10642
11936
  storymapPresenting: false,
10643
11937
  storymapReturnToEditor: false,
11938
+ storymapLayerOpacity: {},
10644
11939
  storymapPanelOpen: false,
10645
11940
  storymapComposingId: null,
10646
11941
  selectByExpressionOpen: false,
@@ -10676,6 +11971,7 @@ const useAppStore = create()(temporal((set, get) => ({
10676
11971
  ...s.ui,
10677
11972
  storymapPresenting: presentStory,
10678
11973
  storymapReturnToEditor: false,
11974
+ storymapLayerOpacity: {},
10679
11975
  storymapPanelOpen: false,
10680
11976
  storymapComposingId: null,
10681
11977
  selectByExpressionOpen: false,
@@ -10693,6 +11989,51 @@ const useAppStore = create()(temporal((set, get) => ({
10693
11989
  name: project.name,
10694
11990
  openedAt: (/* @__PURE__ */ new Date()).toISOString()
10695
11991
  });
11992
+ },
11993
+ setAppRole: (role, options) => {
11994
+ set({ capabilities: {
11995
+ role,
11996
+ privileges: resolveRolePrivileges(role, options?.customPrivileges),
11997
+ reason: options?.reason
11998
+ } });
11999
+ },
12000
+ setAppPrivileges: (privileges, reason) => {
12001
+ set({ capabilities: {
12002
+ role: "custom",
12003
+ privileges: normalizeAppPrivileges(privileges) ?? [],
12004
+ reason
12005
+ } });
12006
+ },
12007
+ grantAppPrivilege: (privilege) => {
12008
+ const current = get().capabilities;
12009
+ if (current.privileges.includes(privilege)) return;
12010
+ const { [privilege]: _granted, ...privilegeReasons } = current.privilegeReasons ?? {};
12011
+ set({ capabilities: {
12012
+ ...current,
12013
+ role: "custom",
12014
+ privileges: [...current.privileges, privilege],
12015
+ privilegeReasons
12016
+ } });
12017
+ },
12018
+ revokeAppPrivilege: (privilege, reason) => {
12019
+ const current = get().capabilities;
12020
+ const held = current.privileges.includes(privilege);
12021
+ if (!held && !reason) return;
12022
+ set({ capabilities: {
12023
+ ...current,
12024
+ role: held ? "custom" : current.role,
12025
+ privileges: held ? current.privileges.filter((p) => p !== privilege) : current.privileges,
12026
+ privilegeReasons: reason ? {
12027
+ ...current.privilegeReasons,
12028
+ [privilege]: reason
12029
+ } : current.privilegeReasons
12030
+ } });
12031
+ },
12032
+ resetAppCapabilities: () => {
12033
+ set({ capabilities: createDefaultAppCapabilities() });
12034
+ },
12035
+ hasAppPrivilege: (privilege) => {
12036
+ return hasAppPrivilege(get().capabilities, privilege);
10696
12037
  }
10697
12038
  }), {
10698
12039
  partialize: (s) => ({
@@ -10701,10 +12042,11 @@ const useAppStore = create()(temporal((set, get) => ({
10701
12042
  basemapStyleUrl: s.basemapStyleUrl,
10702
12043
  basemapVisible: s.basemapVisible,
10703
12044
  basemapOpacity: s.basemapOpacity,
12045
+ blankBackgroundColor: s.blankBackgroundColor,
10704
12046
  storymap: s.storymap,
10705
12047
  comments: s.comments
10706
12048
  }),
10707
- equality: (a, b) => a.basemapStyleUrl === b.basemapStyleUrl && a.basemapVisible === b.basemapVisible && a.basemapOpacity === b.basemapOpacity && a.storymap === b.storymap && shallow(a.layers, b.layers) && shallow(a.comments, b.comments) && layerGroupsEqualForHistory(a.layerGroups, b.layerGroups),
12049
+ equality: (a, b) => a.basemapStyleUrl === b.basemapStyleUrl && a.basemapVisible === b.basemapVisible && a.basemapOpacity === b.basemapOpacity && a.blankBackgroundColor === b.blankBackgroundColor && a.storymap === b.storymap && shallow(a.layers, b.layers) && shallow(a.comments, b.comments) && layerGroupsEqualForHistory(a.layerGroups, b.layerGroups),
10708
12050
  limit: 100,
10709
12051
  handleSet: (baseHandleSet) => {
10710
12052
  const debounced = leadingDebounce(baseHandleSet, getHistoryCoalesceMs);
@@ -10829,6 +12171,19 @@ function clearHistory() {
10829
12171
  notifyProjectRestoreHistory();
10830
12172
  }
10831
12173
  }
12174
+ /**
12175
+ * React hook for consuming application capability state for a specific privilege.
12176
+ *
12177
+ * @param privilege - The privilege to check.
12178
+ * @returns `{ granted: boolean, reason?: string }`
12179
+ */
12180
+ function useAppCapability(privilege) {
12181
+ const capabilities = useAppStore((state) => state.capabilities);
12182
+ return {
12183
+ granted: capabilities.privileges.includes(privilege),
12184
+ reason: appPrivilegeReason(capabilities, privilege)
12185
+ };
12186
+ }
10832
12187
  //#endregion
10833
12188
  //#region src/selection-actions.ts
10834
12189
  /**
@@ -11154,6 +12509,23 @@ function formatCameraAltitude(meters, unit) {
11154
12509
  })} ${label}`;
11155
12510
  }
11156
12511
  //#endregion
12512
+ //#region src/pixel-format.ts
12513
+ /**
12514
+ * Trim a float sample to something readable. A 32-bit raster value decoded to a
12515
+ * JS double prints all 17 digits of its binary representation
12516
+ * (`48.11851119995117`), which is noise past the sensor's precision — six
12517
+ * significant digits is more than any COG carries. Integers are left alone so a
12518
+ * classification code is never shown in exponential form.
12519
+ *
12520
+ * @param value A decoded raster sample.
12521
+ * @returns The sample rendered for display.
12522
+ */
12523
+ function formatPixelValue(value) {
12524
+ if (!Number.isFinite(value)) return String(value);
12525
+ if (Number.isInteger(value)) return String(value);
12526
+ return String(Number(value.toPrecision(6)));
12527
+ }
12528
+ //#endregion
11157
12529
  //#region src/storymap-sample.ts
11158
12530
  const SAMPLE_ASSET_BASE = "https://opengeos.org/maplibre-gl-storymaps/assets";
11159
12531
  /**
@@ -11274,6 +12646,72 @@ function createSampleStoryMap() {
11274
12646
  };
11275
12647
  }
11276
12648
  //#endregion
12649
+ //#region src/storymap-playback.ts
12650
+ /** The clamped opacity recorded for a layer, or undefined when untouched. */
12651
+ function recordedStoryOpacity(opacities, layerId) {
12652
+ const value = opacities?.[layerId];
12653
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
12654
+ return Math.min(1, Math.max(0, value));
12655
+ }
12656
+ /**
12657
+ * The opacity a story presentation has applied to a layer.
12658
+ *
12659
+ * @param opacities The store's `ui.storymapLayerOpacity` record.
12660
+ * @param layerId Store layer id.
12661
+ * @returns The recorded opacity in `[0, 1]`, or `1` when the presentation has
12662
+ * not touched the layer (or nothing is presenting).
12663
+ */
12664
+ function storyLayerOpacityFactor(opacities, layerId) {
12665
+ return recordedStoryOpacity(opacities, layerId) ?? 1;
12666
+ }
12667
+ /**
12668
+ * A layer whose opacity reflects the story presentation's fade.
12669
+ *
12670
+ * A chapter's opacity *replaces* the layer's own opacity rather than scaling
12671
+ * it, matching `MapController.setStoryLayerOpacity`, which writes the chapter
12672
+ * value as the absolute paint opacity; otherwise a layer set to 0.7 in the
12673
+ * Layers panel and faded to 0.4 by a chapter would draw its diagrams at 0.28
12674
+ * while its MapLibre layers draw at 0.4.
12675
+ *
12676
+ * Returns the same object when the presentation has not touched the layer (or
12677
+ * recorded the opacity it already has), so identity-keyed caches downstream
12678
+ * keep hitting.
12679
+ *
12680
+ * @param layer Store layer.
12681
+ * @param opacities The store's `ui.storymapLayerOpacity` record.
12682
+ */
12683
+ function applyStoryLayerOpacity(layer, opacities) {
12684
+ const opacity = recordedStoryOpacity(opacities, layer.id);
12685
+ if (opacity === void 0 || opacity === layer.opacity) return layer;
12686
+ return {
12687
+ ...layer,
12688
+ opacity
12689
+ };
12690
+ }
12691
+ /**
12692
+ * Whether a story presentation has faded a layer fully out.
12693
+ *
12694
+ * @param opacities The store's `ui.storymapLayerOpacity` record.
12695
+ * @param layerId Store layer id.
12696
+ */
12697
+ function isStoryHiddenLayer(opacities, layerId) {
12698
+ return storyLayerOpacityFactor(opacities, layerId) === 0;
12699
+ }
12700
+ /**
12701
+ * The layers a reader can currently see during a story presentation: the
12702
+ * input minus those the active chapter has faded fully out. Outside a
12703
+ * presentation the input is returned as-is (same array identity).
12704
+ *
12705
+ * @param layers Store layers in panel order.
12706
+ * @param presenting The store's `ui.storymapPresenting` flag.
12707
+ * @param opacities The store's `ui.storymapLayerOpacity` record.
12708
+ */
12709
+ function storyVisibleLayers(layers, presenting, opacities) {
12710
+ if (!presenting) return layers;
12711
+ if (!layers.some((layer) => isStoryHiddenLayer(opacities, layer.id))) return layers;
12712
+ return layers.filter((layer) => !isStoryHiddenLayer(opacities, layer.id));
12713
+ }
12714
+ //#endregion
11277
12715
  //#region src/storymap-io.ts
11278
12716
  /**
11279
12717
  * Serialize a story map to pretty-printed JSON for export.
@@ -12631,6 +14069,6 @@ function currentEditorIdentity() {
12631
14069
  return pickEditorIdentity(isActive ? selfName : void 0, readStoredAuthorName());
12632
14070
  }
12633
14071
  //#endregion
12634
- export { BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CHINA_BASEMAPS, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, RoutingRequestError, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeQuickFilters, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, attributeFormFieldLabel, attributeLinkUrl, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, circleRadiusValue, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileQuickFilter, compileQuickFilters, controlRendersLayer, copyableLayerStyleKind, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, ensureEditorTrackingFields, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, extractCopiedLayerStyle, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, formatCameraAltitude, formatExpressionPreviewValue, formatPopupValue, formatRoundNum, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveQuickFilter, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, invertSelection, isAbsoluteFilesystemPath, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCredentialFieldName, isDefaultPrintLayout, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isPopupClickEnabled, isPopupHoverEnabled, isRegionalBasemapSentinel, isSafePopupUrl, isStyleLibraryTargetLayer, isStyleSpecColor, isVectorColorExpression, isochroneResponseToFeatures, layerGroupDepth, layerGroupMoveability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, mapZoomStepOutputs, matchFeaturesByExpression, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerLibraryEntries, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, parseContours, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseProject, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, removeTrailingJsonCommas, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveLayerCapabilities, resolveMapGrid, resolvePopupBody, resolvePopupRows, resolvePopupTitle, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, serializeLayerLibrary, serializeProject, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, simpleStyleNumberValue, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyMapHasContent, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, useAppStore, validateAttributeFormField, validateAttributeFormValues, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties };
14072
+ export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CESIUM_BASEMAPS, CESIUM_BING_AERIAL_ASSET_ID, CESIUM_ION_QUICK_PICKS, CESIUM_ION_SOURCE_KIND, CESIUM_KML_SOURCE_KIND, CESIUM_OSM_BUILDINGS_ASSET_ID, CHINA_BASEMAPS, CZML_QUICK_PICKS, CZML_SOURCE_KIND, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRIMARY_RENDERER, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, DEPLOYMENT_CAPABILITIES, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, IDENTIFY_ALL_LAYERS_ID, LABEL_NUMBER_LOCALES, LABEL_NUMBER_SAMPLE, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeLayerFilterExpression, activeQuickFilters, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, applyStoryLayerOpacity, attributeFormFieldLabel, attributeLinkUrl, availableCesiumBasemap, basemapToCesiumImagery, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, cesiumIonAssetId, cesiumIonAssetKind, cesiumKmlSource, circleRadiusValue, clampLabelDecimals, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileLayerFilters, compileQuickFilter, compileQuickFilters, controlRendersLayer, copyableLayerStyleKind, createCesiumIonLayer, createCesiumKmlLayer, createCzmlLayer, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, czmlSource, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, documentLocale, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, ensureEditorTrackingFields, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, extractCopiedLayerStyle, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, formatCameraAltitude, formatExpressionPreviewValue, formatLabelNumber, formatLabelNumberSample, formatPixelValue, formatPopupValue, formatRoundNum, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveLayerFilter, hasActiveQuickFilter, hasAppPrivilege, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, heatmapRampColors, horizontalBbox, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbsoluteFilesystemPath, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCesiumIonLayer, isCesiumKmlLayer, isCesiumOnlyLayer, isCredentialFieldName, isCzmlLayer, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isPopupClickEnabled, isPopupHoverEnabled, isRegionalBasemapSentinel, isSafePopupUrl, isStoryHiddenLayer, isStyleLibraryTargetLayer, isStyleSpecColor, isVectorColorExpression, isochroneResponseToFeatures, labelFieldTextField, layerGroupDepth, layerGroupMoveability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, localFileName, mapZoomStepOutputs, matchFeaturesByExpression, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeBlankBackgroundColor, normalizeCesiumBasemap, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerLibraryEntries, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrimaryRenderer, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, parseCesiumIonAssetId, parseContours, parseCzml, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseProject, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, removeTrailingJsonCommas, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveLabelNumberLocale, resolveLayerCapabilities, resolveMapGrid, resolvePopupBody, resolvePopupRows, resolvePopupTitle, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, sameCesiumImagery, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, serializeLayerLibrary, serializeProject, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, simpleStyleNumberValue, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyLayerOpacityFactor, storyMapHasContent, storyVisibleLayers, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, uniqueImportedLayerName, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, useAppCapability, useAppStore, validateAttributeFormField, validateAttributeFormValues, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties };
12635
14073
 
12636
14074
  //# sourceMappingURL=index.mjs.map