@geolibre/core 3.4.0 → 3.5.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.
@@ -273,15 +273,18 @@ function styleValue(style, key) {
273
273
  */
274
274
  const LARGE_VECTOR_FEATURE_THRESHOLD = 5e4;
275
275
  /**
276
- * Decide whether a GeoJSON layer should use the tiled rendering path.
276
+ * Decide whether a vector layer should use the tiled rendering path.
277
277
  *
278
- * @param geojson - The layer's feature collection (may be undefined for
278
+ * @param data - The layer's feature collection or GeoArrow batch (may be undefined for
279
279
  * non-vector layers).
280
- * @returns `true` when the collection exceeds
281
- * {@link LARGE_VECTOR_FEATURE_THRESHOLD} features.
280
+ * @returns `true` when the feature count exceeds
281
+ * {@link LARGE_VECTOR_FEATURE_THRESHOLD}.
282
282
  */
283
- function shouldUseTiledRendering(geojson) {
284
- return (geojson?.features.length ?? 0) > LARGE_VECTOR_FEATURE_THRESHOLD;
283
+ function shouldUseTiledRendering(data) {
284
+ if (!data) return false;
285
+ if ("featureCount" in data && typeof data.featureCount === "number") return data.featureCount > LARGE_VECTOR_FEATURE_THRESHOLD;
286
+ if ("features" in data && Array.isArray(data.features)) return data.features.length > LARGE_VECTOR_FEATURE_THRESHOLD;
287
+ return false;
285
288
  }
286
289
  /**
287
290
  * Metadata `sourceKind` marking a live SQL query layer: a GeoJSON-backed layer
@@ -1360,7 +1363,7 @@ function basemapToCesiumImagery(styleUrl, cesiumBasemap = "project") {
1360
1363
  //#endregion
1361
1364
  //#region src/arcgis-projection.ts
1362
1365
  /** WKIDs that are Web Mercator, i.e. the default map. */
1363
- const WEB_MERCATOR_WKIDS = /* @__PURE__ */ new Set([
1366
+ const WEB_MERCATOR_WKIDS$1 = /* @__PURE__ */ new Set([
1364
1367
  3857,
1365
1368
  102100,
1366
1369
  102113,
@@ -1422,9 +1425,394 @@ const ARCGIS_PROJECTION_PRESETS = [
1422
1425
  function normalizeArcgisWkid(value) {
1423
1426
  const wkid = typeof value === "string" ? /^\s*\d+\s*$/.test(value) ? Number(value) : void 0 : value;
1424
1427
  if (typeof wkid !== "number" || !Number.isSafeInteger(wkid) || wkid <= 0) return void 0;
1425
- return WEB_MERCATOR_WKIDS.has(wkid) ? void 0 : wkid;
1428
+ return WEB_MERCATOR_WKIDS$1.has(wkid) ? void 0 : wkid;
1426
1429
  }
1427
1430
  //#endregion
1431
+ //#region src/map-crs.ts
1432
+ /** Codes that are the default Web Mercator map rather than a custom CRS. */
1433
+ const WEB_MERCATOR_CODES = /* @__PURE__ */ new Set([
1434
+ 3857,
1435
+ 900913,
1436
+ 102100,
1437
+ 102113
1438
+ ]);
1439
+ const USGS_WMS = "https://planetarymaps.usgs.gov/cgi-bin/mapserv";
1440
+ const USGS_ATTRIBUTION = "<a href=\"https://astrogeology.usgs.gov/\">USGS Astrogeology</a>";
1441
+ const GIBS_ATTRIBUTION = "Imagery: <a href=\"https://earthdata.nasa.gov/gibs\">NASA GIBS</a> / Blue Marble";
1442
+ /**
1443
+ * A WMS 1.1.1 GetMap template. 1.1.1 because its BBOX is always x,y (east,
1444
+ * north) order, which is what `{bbox}` fills in; 1.3.0 flips it for
1445
+ * geographic CRSs.
1446
+ */
1447
+ function getMapTemplate(base, srs, layer, format) {
1448
+ return `${base}${base.includes("?") ? "&" : "?"}SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&SRS=${srs}&STYLES=&LAYERS=${layer}&WIDTH=512&HEIGHT=512&FORMAT=${format}&TRANSPARENT=TRUE&BBOX={bbox}`;
1449
+ }
1450
+ /** A USGS Astrogeology MapServer layer in the CRS of its `.map` file. */
1451
+ function usgsBasemap(mapFile, srs, layer, maxZoom) {
1452
+ return {
1453
+ tiles: getMapTemplate(`${USGS_WMS}?map=${mapFile}`, srs, layer, "image/png"),
1454
+ attribution: USGS_ATTRIBUTION,
1455
+ maxZoom
1456
+ };
1457
+ }
1458
+ /** A NASA GIBS Blue Marble layer from the WMS endpoint of one of its grids. */
1459
+ function gibsBasemap(epsg) {
1460
+ return {
1461
+ tiles: getMapTemplate(`https://gibs.earthdata.nasa.gov/wms/epsg${epsg}/best/wms.cgi`, `EPSG:${epsg}`, "BlueMarble_ShadedRelief_Bathymetry", "image/jpeg"),
1462
+ attribution: GIBS_ATTRIBUTION,
1463
+ maxZoom: 7
1464
+ };
1465
+ }
1466
+ /** Lng/lat degrees on the whole world, centered so MapLibre can find its edges. */
1467
+ const GEOGRAPHIC_GRID = {
1468
+ origin: [-180, 180],
1469
+ extentAtZoom0: 360
1470
+ };
1471
+ /** A square centered on a pole, `halfWidth` CRS units to each side. */
1472
+ function polarGrid(halfWidth) {
1473
+ return {
1474
+ origin: [-halfWidth, halfWidth],
1475
+ extentAtZoom0: 2 * halfWidth
1476
+ };
1477
+ }
1478
+ const sphere = (radius) => `+R=${radius}`;
1479
+ const longlat = (radius) => `+proj=longlat ${sphere(radius)} +no_defs`;
1480
+ const polarStereographic = (north, radius) => `+proj=stere +lat_0=${north ? 90 : -90} +lon_0=0 +k=1 +x_0=0 +y_0=0 ${sphere(radius)} +units=m +no_defs`;
1481
+ const MOON_RADIUS = 1737400;
1482
+ const MARS_RADIUS = 3396190;
1483
+ /**
1484
+ * A simple-cylindrical (lng/lat degrees) preset for one body's USGS map file.
1485
+ *
1486
+ * Args:
1487
+ * body: The body, which is also the ellipsoid id.
1488
+ * name: English name of the preset.
1489
+ * naifCode: The body's IAU 2015 code (NAIF id × 100), e.g. `49900` for Mars.
1490
+ * radius: Sphere radius in metres.
1491
+ * mapFile: The USGS `.map` file path.
1492
+ * layer: The WMS layer drawn as the basemap.
1493
+ *
1494
+ * Returns:
1495
+ * The preset.
1496
+ */
1497
+ function cylindrical(body, name, naifCode, radius, mapFile, layer) {
1498
+ return {
1499
+ id: `${body}-cylindrical`,
1500
+ name,
1501
+ code: `IAU_2015:${naifCode}`,
1502
+ wmsSrs: "EPSG:4326",
1503
+ definition: longlat(radius),
1504
+ ellipsoidId: body,
1505
+ tileMatrixSet: GEOGRAPHIC_GRID,
1506
+ bounds: [
1507
+ -180,
1508
+ -90,
1509
+ 180,
1510
+ 90
1511
+ ],
1512
+ basemap: usgsBasemap(mapFile, "EPSG:4326", layer, 7)
1513
+ };
1514
+ }
1515
+ /**
1516
+ * Presets verified to render against their WMS on 2026-10-19.
1517
+ *
1518
+ * The USGS polar map files serve their native polar stereographic grid under
1519
+ * `EPSG:32661`/`EPSG:32761` (MapServer's PROJ rejects the IAU codes they also
1520
+ * list), centred on the pole with no false origin, so those labels are used for
1521
+ * the basemap requests while the map registers the body's IAU code.
1522
+ */
1523
+ const MAP_CRS_PRESETS = [
1524
+ {
1525
+ id: "earth-arctic",
1526
+ name: "Arctic Polar Stereographic",
1527
+ code: "EPSG:3413",
1528
+ definition: "+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs",
1529
+ ellipsoidId: "earth",
1530
+ tileMatrixSet: polarGrid(4194304),
1531
+ bounds: [
1532
+ -180,
1533
+ 60,
1534
+ 180,
1535
+ 90
1536
+ ],
1537
+ basemap: gibsBasemap(3413)
1538
+ },
1539
+ {
1540
+ id: "earth-antarctic",
1541
+ name: "Antarctic Polar Stereographic",
1542
+ code: "EPSG:3031",
1543
+ definition: "+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs",
1544
+ ellipsoidId: "earth",
1545
+ tileMatrixSet: polarGrid(4194304),
1546
+ bounds: [
1547
+ -180,
1548
+ -90,
1549
+ 180,
1550
+ -60
1551
+ ],
1552
+ basemap: gibsBasemap(3031)
1553
+ },
1554
+ {
1555
+ id: "earth-cylindrical",
1556
+ name: "Plate Carrée (WGS 84)",
1557
+ code: "EPSG:4326",
1558
+ definition: "+proj=longlat +datum=WGS84 +no_defs",
1559
+ ellipsoidId: "earth",
1560
+ tileMatrixSet: GEOGRAPHIC_GRID,
1561
+ bounds: [
1562
+ -180,
1563
+ -90,
1564
+ 180,
1565
+ 90
1566
+ ],
1567
+ basemap: gibsBasemap(4326)
1568
+ },
1569
+ cylindrical("moon", "Moon Simple Cylindrical", 30100, MOON_RADIUS, "/maps/earth/moon_simp_cyl.map", "LROC_WAC"),
1570
+ {
1571
+ id: "moon-north-polar",
1572
+ name: "Moon North Polar Stereographic",
1573
+ code: "IAU_2015:30118",
1574
+ wmsSrs: "EPSG:32661",
1575
+ definition: polarStereographic(true, MOON_RADIUS),
1576
+ ellipsoidId: "moon",
1577
+ tileMatrixSet: polarGrid(931e3),
1578
+ bounds: [
1579
+ -180,
1580
+ 70,
1581
+ 180,
1582
+ 90
1583
+ ],
1584
+ basemap: usgsBasemap("/maps/earth/moon_npole.map", "EPSG:32661", "LRO_WAC_north", 8)
1585
+ },
1586
+ {
1587
+ id: "moon-south-polar",
1588
+ name: "Moon South Polar Stereographic",
1589
+ code: "IAU_2015:30120",
1590
+ wmsSrs: "EPSG:32761",
1591
+ definition: polarStereographic(false, MOON_RADIUS),
1592
+ ellipsoidId: "moon",
1593
+ tileMatrixSet: polarGrid(931e3),
1594
+ bounds: [
1595
+ -180,
1596
+ -90,
1597
+ 180,
1598
+ -70
1599
+ ],
1600
+ basemap: usgsBasemap("/maps/earth/moon_spole.map", "EPSG:32761", "LRO_WAC_south", 8)
1601
+ },
1602
+ cylindrical("mars", "Mars Simple Cylindrical", 49900, MARS_RADIUS, "/maps/mars/mars_simp_cyl.map", "MOLA_color"),
1603
+ {
1604
+ id: "mars-north-polar",
1605
+ name: "Mars North Polar Stereographic",
1606
+ code: "IAU_2015:49918",
1607
+ wmsSrs: "EPSG:32661",
1608
+ definition: polarStereographic(true, MARS_RADIUS),
1609
+ ellipsoidId: "mars",
1610
+ tileMatrixSet: polarGrid(2357030),
1611
+ bounds: [
1612
+ -180,
1613
+ 65,
1614
+ 180,
1615
+ 90
1616
+ ],
1617
+ basemap: usgsBasemap("/maps/mars/mars_npole.map", "EPSG:32661", "MOLA_color_north", 7)
1618
+ },
1619
+ {
1620
+ id: "mars-south-polar",
1621
+ name: "Mars South Polar Stereographic",
1622
+ code: "IAU_2015:49920",
1623
+ wmsSrs: "EPSG:32761",
1624
+ definition: polarStereographic(false, MARS_RADIUS),
1625
+ ellipsoidId: "mars",
1626
+ tileMatrixSet: polarGrid(2357030),
1627
+ bounds: [
1628
+ -180,
1629
+ -90,
1630
+ 180,
1631
+ -65
1632
+ ],
1633
+ basemap: usgsBasemap("/maps/mars/mars_spole.map", "EPSG:32761", "MOLA_color_south", 7)
1634
+ },
1635
+ cylindrical("mercury", "Mercury Simple Cylindrical", 19900, 2439400, "/maps/mercury/mercury_simp_cyl.map", "MESSENGER"),
1636
+ cylindrical("venus", "Venus Simple Cylindrical", 29900, 6051800, "/maps/venus/venus_simp_cyl.map", "MAGELLAN"),
1637
+ cylindrical("io", "Io Simple Cylindrical", 50100, 1821600, "/maps/jupiter/io_simp_cyl.map", "SSI_color"),
1638
+ cylindrical("europa", "Europa Simple Cylindrical", 50200, 1560800, "/maps/jupiter/europa_simp_cyl.map", "GALILEO_VOYAGER"),
1639
+ cylindrical("ganymede", "Ganymede Simple Cylindrical", 50300, 2631200, "/maps/jupiter/ganymede_simp_cyl.map", "GALILEO_VOYAGER"),
1640
+ cylindrical("callisto", "Callisto Simple Cylindrical", 50400, 2410300, "/maps/jupiter/callisto_simp_cyl.map", "GALILEO_VOYAGER"),
1641
+ cylindrical("titan", "Titan Simple Cylindrical", 60600, 2574730, "/maps/saturn/titan_simp_cyl.map", "Titan_ISS_Controlled_Mosaic"),
1642
+ cylindrical("pluto", "Pluto Simple Cylindrical", 99900, 1188300, "/maps/pluto/pluto_simp_cyl.map", "NEWHORIZONS_PLUTO_MOSAIC"),
1643
+ cylindrical("charon", "Charon Simple Cylindrical", 90100, 606e3, "/maps/pluto/charon_simp_cyl.map", "NEWHORIZONS_CHARON_MOSAIC")
1644
+ ];
1645
+ /**
1646
+ * Look a preset up by its id.
1647
+ *
1648
+ * Args:
1649
+ * id: A `preferences.map.mapCrs` value.
1650
+ *
1651
+ * Returns:
1652
+ * The preset, or `undefined` when `id` is not a preset id.
1653
+ */
1654
+ function getMapCrsPreset(id) {
1655
+ return id === void 0 ? void 0 : MAP_CRS_PRESETS.find((preset) => preset.id === id);
1656
+ }
1657
+ /**
1658
+ * Read an `EPSG:<code>` map CRS value.
1659
+ *
1660
+ * Args:
1661
+ * value: A `preferences.map.mapCrs` value.
1662
+ *
1663
+ * Returns:
1664
+ * The numeric EPSG code, or `null` when the value is not an EPSG CRS.
1665
+ */
1666
+ function parseMapCrsEpsgCode(value) {
1667
+ const match = /^\s*EPSG:(\d+)\s*$/i.exec(value ?? "");
1668
+ if (!match) return null;
1669
+ const code = Number(match[1]);
1670
+ return Number.isSafeInteger(code) && code > 0 ? code : null;
1671
+ }
1672
+ /**
1673
+ * Normalize a stored map CRS.
1674
+ *
1675
+ * Args:
1676
+ * value: The raw `preferences.map.mapCrs` value.
1677
+ *
1678
+ * Returns:
1679
+ * A preset id, `"EPSG:<code>"`, or `undefined` for the default Web Mercator
1680
+ * map (absent, malformed or a Web Mercator code).
1681
+ */
1682
+ function normalizeMapCrs(value) {
1683
+ if (typeof value !== "string") return void 0;
1684
+ if (getMapCrsPreset(value)) return value;
1685
+ const code = parseMapCrsEpsgCode(value);
1686
+ if (code === null || WEB_MERCATOR_CODES.has(code)) return void 0;
1687
+ const epsg = `EPSG:${code}`;
1688
+ return MAP_CRS_PRESETS.find((preset) => preset.code === epsg)?.id ?? epsg;
1689
+ }
1690
+ /**
1691
+ * Point a Web Mercator WMS or ArcGIS export template at a map CRS.
1692
+ *
1693
+ * GeoLibre's WMS layers request `BBOX={bbox-epsg-3857}` in `EPSG:3857`. On a
1694
+ * map in another CRS each tile is a box in that CRS instead, which MapLibre
1695
+ * writes into the `{bbox}` token, so the template asks for `{bbox}` in `code`.
1696
+ * WMS requests move to version 1.1.1, whose `SRS` BBOX is east, north for every
1697
+ * CRS (1.3.0 swaps the axes of geographic ones), and an ArcGIS `export`
1698
+ * template has its `bboxSR`/`imageSR` set to the EPSG number.
1699
+ *
1700
+ * Args:
1701
+ * template: A raster tile template.
1702
+ * code: The map CRS's authority code, e.g. `EPSG:3031` or `IAU_2015:49918`.
1703
+ *
1704
+ * Returns:
1705
+ * The template for the CRS, the template unchanged when it already uses
1706
+ * `{bbox}`, or `null` for a template with no bounding box (an XYZ or TMS
1707
+ * template), which cannot be drawn in another CRS.
1708
+ */
1709
+ function adaptBboxTemplateToCrs(template, code) {
1710
+ if (template.includes("{bbox}")) return template;
1711
+ if (!template.includes("{bbox-epsg-3857}")) return null;
1712
+ let url = template.replace(/\{bbox-epsg-3857\}/g, "{bbox}");
1713
+ const epsg = /^EPSG:(\d+)$/i.exec(code)?.[1];
1714
+ const setParam = (name, value) => {
1715
+ const pattern = new RegExp(`([?&])${name}=[^&]*`, "i");
1716
+ url = pattern.test(url) ? url.replace(pattern, `$1${name}=${value}`) : `${url}${url.includes("?") ? "&" : "?"}${name}=${value}`;
1717
+ };
1718
+ const removeParam = (name) => {
1719
+ url = url.replace(new RegExp(`([?&])${name}=[^&]*(&?)`, "i"), (_, lead, trail) => trail ? lead : "");
1720
+ };
1721
+ if (/[?&]bboxSR=/i.test(url)) {
1722
+ if (!epsg) return null;
1723
+ setParam("bboxSR", epsg);
1724
+ setParam("imageSR", epsg);
1725
+ return url;
1726
+ }
1727
+ removeParam("CRS");
1728
+ setParam("SRS", code);
1729
+ setParam("VERSION", "1.1.1");
1730
+ return url;
1731
+ }
1732
+ /**
1733
+ * The smallest centered square grid over the part of a CRS worth mapping, for
1734
+ * an EPSG code without a published tile grid.
1735
+ *
1736
+ * Samples a 2° lng/lat lattice and keeps the points `projection` takes forward
1737
+ * and back to where they started (a transverse Mercator's far side does not)
1738
+ * whose local scale is at most {@link MAX_RELATIVE_SCALE} times the CRS's
1739
+ * smallest. A polar stereographic map round-trips almost to the other pole,
1740
+ * but out there a kilometre spans thousands of CRS units, and a grid that wide
1741
+ * leaves the CRS's own area a speck at every useful zoom. The extent of the
1742
+ * kept points is padded by 4%. MapLibre finds the edge of a world projection
1743
+ * by walking out from the grid's center, so the grid is centered on them.
1744
+ *
1745
+ * Args:
1746
+ * projection: Lng/lat to CRS and back, the shape of a proj4 converter.
1747
+ * geographic: Whether the CRS is in degrees, which gets the whole-world grid.
1748
+ *
1749
+ * Returns:
1750
+ * The grid, or `null` when no sampled point survives the round trip.
1751
+ */
1752
+ function deriveTileMatrixSet(projection, geographic) {
1753
+ if (geographic) return GEOGRAPHIC_GRID;
1754
+ const forward = (lng, lat) => {
1755
+ try {
1756
+ const [x, y] = projection.forward([lng, lat]) ?? [];
1757
+ return Number.isFinite(x) && Number.isFinite(y) && Math.abs(x) <= 1e9 && Math.abs(y) <= 1e9 ? [x, y] : null;
1758
+ } catch {
1759
+ return null;
1760
+ }
1761
+ };
1762
+ const roundTrips = (point, lng, lat) => {
1763
+ let back;
1764
+ try {
1765
+ back = projection.inverse(point);
1766
+ } catch {
1767
+ return false;
1768
+ }
1769
+ if (!back || !Number.isFinite(back[0]) || !Number.isFinite(back[1])) return false;
1770
+ const dLng = Math.abs(((back[0] - lng) % 360 + 540) % 360 - 180);
1771
+ return Math.abs(back[1] - lat) <= 1e-4 && dLng <= 1e-4;
1772
+ };
1773
+ const STEP = .01;
1774
+ const radians = STEP * Math.PI / 180;
1775
+ const samples = [];
1776
+ for (let lng = -180; lng <= 180; lng += 2) for (let lat = -88; lat <= 88; lat += 2) {
1777
+ const point = forward(lng, lat);
1778
+ if (!point || !roundTrips(point, lng, lat)) continue;
1779
+ const north = forward(lng, lat + STEP);
1780
+ const east = forward(lng + STEP, lat);
1781
+ if (!north || !east) continue;
1782
+ const scale = Math.max(Math.hypot(north[0] - point[0], north[1] - point[1]) / radians, Math.hypot(east[0] - point[0], east[1] - point[1]) / (radians * Math.cos(lat * Math.PI / 180)));
1783
+ if (scale > 0 && Number.isFinite(scale)) samples.push({
1784
+ x: point[0],
1785
+ y: point[1],
1786
+ scale
1787
+ });
1788
+ }
1789
+ if (samples.length === 0) return null;
1790
+ const limit = MAX_RELATIVE_SCALE * Math.min(...samples.map((sample) => sample.scale));
1791
+ let minX = Infinity;
1792
+ let minY = Infinity;
1793
+ let maxX = -Infinity;
1794
+ let maxY = -Infinity;
1795
+ for (const { x, y, scale } of samples) {
1796
+ if (scale > limit) continue;
1797
+ minX = Math.min(minX, x);
1798
+ minY = Math.min(minY, y);
1799
+ maxX = Math.max(maxX, x);
1800
+ maxY = Math.max(maxY, y);
1801
+ }
1802
+ const side = Math.max(maxX - minX, maxY - minY) * 1.04;
1803
+ if (!(side > 0)) return null;
1804
+ return {
1805
+ origin: [(minX + maxX - side) / 2, (minY + maxY + side) / 2],
1806
+ extentAtZoom0: side
1807
+ };
1808
+ }
1809
+ /**
1810
+ * How much more a derived grid's edge may stretch distances than the CRS's
1811
+ * least distorted place: about 11°N for an Antarctic polar stereographic map,
1812
+ * and some 65° either side of a transverse Mercator's central meridian.
1813
+ */
1814
+ const MAX_RELATIVE_SCALE = 2.5;
1815
+ //#endregion
1428
1816
  //#region src/geojson-z.ts
1429
1817
  /**
1430
1818
  * Helpers for vector data whose coordinates carry a Z value (elevation), e.g.
@@ -1828,6 +2216,176 @@ function batchDecodePolylines(input, options = {}) {
1828
2216
  };
1829
2217
  }
1830
2218
  //#endregion
2219
+ //#region src/runtime-env.ts
2220
+ const buildEnv = typeof __GEOLIBRE_BUILD_ENV__ === "undefined" ? {} : __GEOLIBRE_BUILD_ENV__;
2221
+ /**
2222
+ * The allowlisted build-time environment, with no runtime overlay.
2223
+ *
2224
+ * For settings that must be fixed by the build and NOT overridable by a
2225
+ * `.geolibre.json` a user opened — the auth gate, the deployment profile, the
2226
+ * embed origin allowlist. Routing those through {@link getRuntimeEnvironment}
2227
+ * would let project-supplied `__GEOLIBRE_RUNTIME_ENV__` values relax them.
2228
+ *
2229
+ * Callers should prefer this over reading `import.meta.env` directly: a
2230
+ * whole-object read defeats Vite's per-key replacement and inlines every
2231
+ * `VITE_` var on the build machine into the chunk.
2232
+ *
2233
+ * @returns The build-time environment record.
2234
+ */
2235
+ function getBuildEnvironment() {
2236
+ return buildEnv;
2237
+ }
2238
+ /**
2239
+ * Merges build-time env with project runtime env (the latter wins). Falls back
2240
+ * to build-time env alone outside a browser (e.g. in tests).
2241
+ *
2242
+ * @returns The resolved environment variables.
2243
+ */
2244
+ function getRuntimeEnvironment() {
2245
+ if (typeof window === "undefined") return buildEnv;
2246
+ return {
2247
+ ...buildEnv,
2248
+ ...window.__GEOLIBRE_RUNTIME_ENV__ ?? {}
2249
+ };
2250
+ }
2251
+ /**
2252
+ * Resolves a local DuckDB spatial extension path from the runtime environment.
2253
+ *
2254
+ * When `VITE_DUCKDB_SPATIAL_EXTENSION_PATH` is set, DuckDB consumers (the
2255
+ * desktop app's own loader and the Add Vector panel's maplibre-gl-vector
2256
+ * control) load the spatial extension from this path with `LOAD '<path>'`
2257
+ * instead of installing it from the remote repository, which hangs in
2258
+ * sandboxed or firewalled environments. Lives in `@geolibre/core` so every
2259
+ * consumer shares one implementation.
2260
+ *
2261
+ * @param env - Environment record (defaults to the runtime environment);
2262
+ * injectable for testing.
2263
+ * @returns The trimmed extension path, or undefined when unset.
2264
+ */
2265
+ function getSpatialExtensionPath(env) {
2266
+ return (env ?? getRuntimeEnvironment()).VITE_DUCKDB_SPATIAL_EXTENSION_PATH?.trim() || void 0;
2267
+ }
2268
+ /**
2269
+ * Resolves the Protomaps API key from the runtime environment.
2270
+ *
2271
+ * Protomaps' hosted styles require an API key embedded in the style URL. The
2272
+ * key is supplied via `VITE_PROTOMAPS_API_KEY` (baked in at build time for the
2273
+ * web demo; see the deploy workflow). When unset, the Protomaps basemaps are
2274
+ * unavailable and should be hidden from the UI.
2275
+ *
2276
+ * @param env - Environment record (defaults to the runtime environment);
2277
+ * injectable for testing.
2278
+ * @returns The trimmed API key, or undefined when unset.
2279
+ */
2280
+ function getProtomapsApiKey(env) {
2281
+ return (env ?? getRuntimeEnvironment()).VITE_PROTOMAPS_API_KEY?.trim() || void 0;
2282
+ }
2283
+ /**
2284
+ * Resolves the Google Maps API key from the runtime environment.
2285
+ *
2286
+ * GeoLibre's browser-facing builds normally use `VITE_GOOGLE_MAPS_API_KEY`.
2287
+ * The bare `GOOGLE_MAPS_API_KEY` fallback is reached two ways: (1) the desktop
2288
+ * Vite config copies it to `VITE_GOOGLE_MAPS_API_KEY` at build time for local
2289
+ * shell testing (`vite.config.ts`'s `envPrefix` does not include the bare
2290
+ * name), and (2) a project's own runtime environment variables
2291
+ * (`window.__GEOLIBRE_RUNTIME_ENV__`), which are not subject to Vite's
2292
+ * envPrefix allowlist at all.
2293
+ *
2294
+ * @param env - Environment record (defaults to the runtime environment);
2295
+ * injectable for testing.
2296
+ * @returns The trimmed API key, or undefined when unset.
2297
+ */
2298
+ function getGoogleMapsApiKey(env) {
2299
+ const runtimeEnv = env ?? getRuntimeEnvironment();
2300
+ return runtimeEnv.VITE_GOOGLE_MAPS_API_KEY?.trim() || runtimeEnv.GOOGLE_MAPS_API_KEY?.trim() || void 0;
2301
+ }
2302
+ /**
2303
+ * Resolves the Mapbox access token from the runtime environment.
2304
+ *
2305
+ * The basemap control's Mapbox styles authenticate with the user's own token.
2306
+ * It is supplied via `VITE_MAPBOX_ACCESS_TOKEN` (baked in at build time from the
2307
+ * bare `MAPBOX_TOKEN` env var, which `vite.config.ts` copies into the prefixed
2308
+ * name — the spelling Mapbox's own tooling uses) or set at runtime through
2309
+ * Settings → Environment variables (`window.__GEOLIBRE_RUNTIME_ENV__`, which
2310
+ * bypasses Vite's envPrefix allowlist, so a bare `MAPBOX_TOKEN` entry works
2311
+ * there too). When unset, the Mapbox basemaps prompt for a token in the basemap
2312
+ * panel's API keys view instead.
2313
+ *
2314
+ * Note the precedence, shared with {@link getGoogleMapsApiKey} and
2315
+ * {@link getCesiumIonToken}: the prefixed name always wins over the bare one,
2316
+ * and `getRuntimeEnvironment` merges build-time and runtime vars into one
2317
+ * record. So a build that baked in `VITE_MAPBOX_ACCESS_TOKEN` is overridden at
2318
+ * runtime by a Settings entry under that *same* prefixed name; a bare
2319
+ * `MAPBOX_TOKEN` entry is a fallback for when nothing was baked in, not a way
2320
+ * to override a baked token.
2321
+ *
2322
+ * @param env - Environment record (defaults to the runtime environment);
2323
+ * injectable for testing.
2324
+ * @returns The trimmed token, or undefined when unset.
2325
+ */
2326
+ function getMapboxAccessToken(env) {
2327
+ const runtimeEnv = env ?? getRuntimeEnvironment();
2328
+ return runtimeEnv.VITE_MAPBOX_ACCESS_TOKEN?.trim() || runtimeEnv.MAPBOX_TOKEN?.trim() || void 0;
2329
+ }
2330
+ /**
2331
+ * Resolves the ArcGIS API key from the runtime environment.
2332
+ *
2333
+ * The ArcGIS renderer draws the translated project basemap and non-Esri layers
2334
+ * without a key; Esri's basemap styles and location services require one
2335
+ * (an ArcGIS Location Platform or ArcGIS Online API key credential). It is
2336
+ * supplied via `VITE_ARCGIS_API_KEY` (baked in at build time from the bare
2337
+ * `ARCGIS_API_KEY` env var, which `vite.config.ts` copies into the prefixed
2338
+ * name) or set at runtime through Settings → Environment variables
2339
+ * (`window.__GEOLIBRE_RUNTIME_ENV__`, so a bare `ARCGIS_API_KEY` entry works
2340
+ * there too). Same precedence as {@link getMapboxAccessToken}.
2341
+ *
2342
+ * @param env - Environment record (defaults to the runtime environment);
2343
+ * injectable for testing.
2344
+ * @returns The trimmed key, or undefined when unset.
2345
+ */
2346
+ function getArcgisApiKey(env) {
2347
+ const runtimeEnv = env ?? getRuntimeEnvironment();
2348
+ return runtimeEnv.VITE_ARCGIS_API_KEY?.trim() || runtimeEnv.ARCGIS_API_KEY?.trim() || void 0;
2349
+ }
2350
+ /**
2351
+ * Resolves the Cesium Ion access token from the runtime environment.
2352
+ *
2353
+ * Cesium World Terrain and Ion World Imagery need a Cesium Ion token. It is
2354
+ * supplied via `VITE_CESIUM_TOKEN` (baked in at build time from the bare
2355
+ * `CESIUM_TOKEN` env var, which `vite.config.ts` copies into the prefixed name)
2356
+ * or set at runtime through Settings → Environment variables
2357
+ * (`window.__GEOLIBRE_RUNTIME_ENV__`, which bypasses Vite's envPrefix allowlist,
2358
+ * so a bare `CESIUM_TOKEN` entry works there too).
2359
+ *
2360
+ * The 3D-globe view itself does **not** need one: it draws the project basemap
2361
+ * as its base imagery whenever that basemap has a raster form (see
2362
+ * {@link basemapToCesiumImagery}), and falls back to Ion World Imagery — or, with
2363
+ * no token, to OpenStreetMap — for the ones that do not. So an absent token
2364
+ * costs terrain and the quality of that fallback, not the view.
2365
+ *
2366
+ * @param env - Environment record (defaults to the runtime environment);
2367
+ * injectable for testing.
2368
+ * @returns The trimmed token, or undefined when unset.
2369
+ */
2370
+ function getCesiumIonToken(env) {
2371
+ const runtimeEnv = env ?? getRuntimeEnvironment();
2372
+ return runtimeEnv.VITE_CESIUM_TOKEN?.trim() || runtimeEnv.CESIUM_TOKEN?.trim() || void 0;
2373
+ }
2374
+ /**
2375
+ * Builds a full Protomaps v5 style URL for a flavor, injecting the API key.
2376
+ *
2377
+ * @param flavor - The Protomaps flavor name (e.g. `light`, `dark`, `white`,
2378
+ * `grayscale`, `black`).
2379
+ * @param env - Environment record (defaults to the runtime environment);
2380
+ * injectable for testing.
2381
+ * @returns The resolved style URL, or undefined when no API key is configured.
2382
+ */
2383
+ function getProtomapsStyleUrl(flavor, env) {
2384
+ const key = getProtomapsApiKey(env);
2385
+ if (!key) return void 0;
2386
+ return `https://api.protomaps.com/styles/v5/${encodeURIComponent(flavor)}/en.json?key=${encodeURIComponent(key)}`;
2387
+ }
2388
+ //#endregion
1831
2389
  //#region src/selection.ts
1832
2390
  /** Selection modes in display order. */
1833
2391
  const SELECTION_MODES = [
@@ -3040,8 +3598,9 @@ function applyJoinsToLayer(layer, allLayers, nextJoins, nextVirtualFields) {
3040
3598
  return allLayers.find((candidate) => candidate.id === joinLayerId)?.geojson;
3041
3599
  });
3042
3600
  const withVirtual = applyLayerVirtualFields(applied.features, virtualFields);
3601
+ const { geoarrow: _staleGeoarrow, ...rest } = layer;
3043
3602
  return {
3044
- ...layer,
3603
+ ...rest,
3045
3604
  geojson: {
3046
3605
  ...geojson,
3047
3606
  features: withVirtual.features
@@ -4942,6 +5501,406 @@ const BUILT_IN_STYLE_PRESETS = [
4942
5501
  }
4943
5502
  ];
4944
5503
  //#endregion
5504
+ //#region src/raster-source-defaults.ts
5505
+ /**
5506
+ * Older projects and manually added Wayback templates may omit the native
5507
+ * zoom limit. Cap their requests at level 19 so renderers overzoom available
5508
+ * imagery instead of requesting missing higher-level tiles (issue #3116).
5509
+ * Explicit numeric limits remain authoritative, including lower regional limits.
5510
+ * Renderers ignore string limits, so only numeric minzoom can constrain this default.
5511
+ */
5512
+ function withRasterSourceDefaults(layer) {
5513
+ if (![
5514
+ "raster",
5515
+ "xyz",
5516
+ "wmts"
5517
+ ].includes(layer.type) || !layer.source || typeof layer.source.maxzoom === "number" && Number.isFinite(layer.source.maxzoom) || typeof layer.source.minzoom === "number" && layer.source.minzoom > 19) return layer;
5518
+ if (!(Array.isArray(layer.source.tiles) && layer.source.tiles.length ? layer.source.tiles : [layer.source.url]).every(isWaybackTileUrl)) return layer;
5519
+ return {
5520
+ ...layer,
5521
+ source: {
5522
+ ...layer.source,
5523
+ maxzoom: 19
5524
+ }
5525
+ };
5526
+ }
5527
+ function isWaybackTileUrl(value) {
5528
+ if (typeof value !== "string") return false;
5529
+ try {
5530
+ const url = new URL(value);
5531
+ return (url.protocol === "https:" || url.protocol === "http:") && /^wayback(?:-[a-z])?\.maptiles\.arcgis\.com$/i.test(url.hostname) && /^\/arcgis\/rest\/services\/World_Imagery\/(?:WMTS\/1\.0\.0\/[^/]+\/)?MapServer\/tile\/\d+\//i.test(url.pathname);
5532
+ } catch {
5533
+ return false;
5534
+ }
5535
+ }
5536
+ //#endregion
5537
+ //#region src/geoarrow.ts
5538
+ /**
5539
+ * Type guard for {@link GeoArrowBatch}.
5540
+ * Validates structure and basic buffer invariants without deep scanning.
5541
+ */
5542
+ function isGeoArrowBatch(value) {
5543
+ if (!value || typeof value !== "object") return false;
5544
+ const candidate = value;
5545
+ if (![
5546
+ "Point",
5547
+ "MultiPoint",
5548
+ "LineString",
5549
+ "MultiLineString",
5550
+ "Polygon",
5551
+ "MultiPolygon"
5552
+ ].includes(candidate.geometryType ?? "")) return false;
5553
+ if (!(candidate.coordinates instanceof Float64Array)) return false;
5554
+ if (candidate.coordDimensions !== 2 && candidate.coordDimensions !== 3) return false;
5555
+ if (!Number.isSafeInteger(candidate.featureCount) || candidate.featureCount < 0) return false;
5556
+ if (candidate.coordinates.length % candidate.coordDimensions !== 0) return false;
5557
+ if (candidate.geomOffsets !== void 0 && !(candidate.geomOffsets instanceof Int32Array)) return false;
5558
+ if (candidate.ringOffsets !== void 0 && !(candidate.ringOffsets instanceof Int32Array)) return false;
5559
+ if (candidate.polygonOffsets !== void 0 && !(candidate.polygonOffsets instanceof Int32Array)) return false;
5560
+ const vertexCount = candidate.coordinates.length / candidate.coordDimensions;
5561
+ const validOffsets = (offsets, length, end) => {
5562
+ if (!offsets || offsets.length !== length || offsets[0] !== 0) return false;
5563
+ for (let i = 1; i < offsets.length; i++) if (offsets[i] < offsets[i - 1] || offsets[i] > end) return false;
5564
+ return offsets[offsets.length - 1] === end;
5565
+ };
5566
+ const count = candidate.featureCount;
5567
+ if (candidate.geometryType === "Point") {
5568
+ if (vertexCount !== count) return false;
5569
+ } else if (candidate.geometryType === "LineString" || candidate.geometryType === "MultiPoint") {
5570
+ if (!validOffsets(candidate.geomOffsets, count + 1, vertexCount)) return false;
5571
+ } else {
5572
+ const rings = (candidate.ringOffsets?.length ?? 0) - 1;
5573
+ if (!validOffsets(candidate.ringOffsets, rings + 1, vertexCount)) return false;
5574
+ if (candidate.geometryType === "MultiPolygon") {
5575
+ const polygons = (candidate.geomOffsets?.length ?? 0) - 1;
5576
+ if (!validOffsets(candidate.geomOffsets, polygons + 1, rings)) return false;
5577
+ if (!validOffsets(candidate.polygonOffsets, count + 1, polygons)) return false;
5578
+ } else if (!validOffsets(candidate.geomOffsets, count + 1, rings)) return false;
5579
+ }
5580
+ if (candidate.properties !== void 0 && (!Array.isArray(candidate.properties) || candidate.properties.length !== count)) return false;
5581
+ if (candidate.featureIds !== void 0 && (!Array.isArray(candidate.featureIds) || candidate.featureIds.length !== count)) return false;
5582
+ return true;
5583
+ }
5584
+ /**
5585
+ * Computes a 2D bounding box [minX, minY, maxX, maxY] for a {@link GeoArrowBatch}
5586
+ * in a single fast, vectorized pass over the flat coordinates buffer.
5587
+ * Returns null if the batch contains no finite coordinates.
5588
+ */
5589
+ function computeGeoArrowBBox(batch) {
5590
+ if (batch.bbox) return [...batch.bbox];
5591
+ const { coordinates, coordDimensions } = batch;
5592
+ if (coordinates.length === 0) return null;
5593
+ let minX = Infinity;
5594
+ let minY = Infinity;
5595
+ let maxX = -Infinity;
5596
+ let maxY = -Infinity;
5597
+ const step = coordDimensions;
5598
+ for (let i = 0; i < coordinates.length; i += step) {
5599
+ const x = coordinates[i];
5600
+ const y = coordinates[i + 1];
5601
+ if (Number.isFinite(x) && Number.isFinite(y)) {
5602
+ if (x < minX) minX = x;
5603
+ if (y < minY) minY = y;
5604
+ if (x > maxX) maxX = x;
5605
+ if (y > maxY) maxY = y;
5606
+ }
5607
+ }
5608
+ return minX === Infinity ? null : [
5609
+ minX,
5610
+ minY,
5611
+ maxX,
5612
+ maxY
5613
+ ];
5614
+ }
5615
+ /**
5616
+ * Convert a {@link GeoArrowBatch} to a standard GeoJSON {@link FeatureCollection}.
5617
+ * Lossless round-trip converter for renderers or components (Cesium, Attribute Table, Turf)
5618
+ * that expect standard GeoJSON objects.
5619
+ */
5620
+ function geoArrowToFeatureCollection(batch) {
5621
+ const { geometryType, coordinates, coordDimensions: dim, geomOffsets, ringOffsets, polygonOffsets, featureCount, properties, featureIds } = batch;
5622
+ const features = new Array(featureCount);
5623
+ function readPosition(vertexIdx) {
5624
+ const o = vertexIdx * dim;
5625
+ return dim === 3 ? [
5626
+ coordinates[o],
5627
+ coordinates[o + 1],
5628
+ coordinates[o + 2]
5629
+ ] : [coordinates[o], coordinates[o + 1]];
5630
+ }
5631
+ function readLine(startVertex, endVertex) {
5632
+ const count = endVertex - startVertex;
5633
+ const line = new Array(count);
5634
+ for (let i = 0; i < count; i++) line[i] = readPosition(startVertex + i);
5635
+ return line;
5636
+ }
5637
+ for (let i = 0; i < featureCount; i++) {
5638
+ const props = properties ? { ...properties[i] } : {};
5639
+ const id = featureIds ? featureIds[i] : void 0;
5640
+ let geometry = null;
5641
+ switch (geometryType) {
5642
+ case "Point": {
5643
+ const o = i * dim;
5644
+ if (o + 1 < coordinates.length && Number.isFinite(coordinates[o]) && Number.isFinite(coordinates[o + 1])) geometry = {
5645
+ type: "Point",
5646
+ coordinates: readPosition(i)
5647
+ };
5648
+ break;
5649
+ }
5650
+ case "MultiPoint":
5651
+ if (geomOffsets) {
5652
+ const start = geomOffsets[i];
5653
+ const end = geomOffsets[i + 1];
5654
+ if (end > start) geometry = {
5655
+ type: "MultiPoint",
5656
+ coordinates: readLine(start, end)
5657
+ };
5658
+ }
5659
+ break;
5660
+ case "LineString":
5661
+ if (geomOffsets) {
5662
+ const start = geomOffsets[i];
5663
+ const end = geomOffsets[i + 1];
5664
+ if (end > start) geometry = {
5665
+ type: "LineString",
5666
+ coordinates: readLine(start, end)
5667
+ };
5668
+ }
5669
+ break;
5670
+ case "MultiLineString":
5671
+ if (geomOffsets && ringOffsets) {
5672
+ const startLine = geomOffsets[i];
5673
+ const endLine = geomOffsets[i + 1];
5674
+ const lines = [];
5675
+ for (let l = startLine; l < endLine; l++) {
5676
+ const vStart = ringOffsets[l];
5677
+ const vEnd = ringOffsets[l + 1];
5678
+ if (vEnd > vStart) lines.push(readLine(vStart, vEnd));
5679
+ }
5680
+ if (lines.length > 0) geometry = {
5681
+ type: "MultiLineString",
5682
+ coordinates: lines
5683
+ };
5684
+ }
5685
+ break;
5686
+ case "Polygon":
5687
+ if (geomOffsets && ringOffsets) {
5688
+ const startRing = geomOffsets[i];
5689
+ const endRing = geomOffsets[i + 1];
5690
+ const rings = [];
5691
+ for (let r = startRing; r < endRing; r++) {
5692
+ const vStart = ringOffsets[r];
5693
+ const vEnd = ringOffsets[r + 1];
5694
+ if (vEnd > vStart) rings.push(readLine(vStart, vEnd));
5695
+ }
5696
+ if (rings.length > 0) geometry = {
5697
+ type: "Polygon",
5698
+ coordinates: rings
5699
+ };
5700
+ }
5701
+ break;
5702
+ case "MultiPolygon": if (polygonOffsets && geomOffsets && ringOffsets) {
5703
+ const startPoly = polygonOffsets[i];
5704
+ const endPoly = polygonOffsets[i + 1];
5705
+ const polys = [];
5706
+ for (let p = startPoly; p < endPoly; p++) {
5707
+ const startRing = geomOffsets[p];
5708
+ const endRing = geomOffsets[p + 1];
5709
+ const rings = [];
5710
+ for (let r = startRing; r < endRing; r++) {
5711
+ const vStart = ringOffsets[r];
5712
+ const vEnd = ringOffsets[r + 1];
5713
+ if (vEnd > vStart) rings.push(readLine(vStart, vEnd));
5714
+ }
5715
+ if (rings.length > 0) polys.push(rings);
5716
+ }
5717
+ if (polys.length > 0) geometry = {
5718
+ type: "MultiPolygon",
5719
+ coordinates: polys
5720
+ };
5721
+ }
5722
+ }
5723
+ features[i] = {
5724
+ type: "Feature",
5725
+ id,
5726
+ properties: props,
5727
+ geometry
5728
+ };
5729
+ }
5730
+ return {
5731
+ type: "FeatureCollection",
5732
+ features
5733
+ };
5734
+ }
5735
+ function extractVerticesForLineOrMultiPoint(geometry, primaryType) {
5736
+ if (!geometry) return null;
5737
+ if (primaryType === "LineString") {
5738
+ if (geometry.type === "LineString" && Array.isArray(geometry.coordinates)) return geometry.coordinates;
5739
+ return null;
5740
+ }
5741
+ if (primaryType === "MultiPoint") {
5742
+ if (geometry.type === "MultiPoint" && Array.isArray(geometry.coordinates)) return geometry.coordinates;
5743
+ if (geometry.type === "Point" && Array.isArray(geometry.coordinates)) return [geometry.coordinates];
5744
+ return null;
5745
+ }
5746
+ return null;
5747
+ }
5748
+ /**
5749
+ * Flatten supported Point, MultiPoint, LineString and Polygon collections.
5750
+ * Empty collections produce an empty Point batch; unsupported geometries return null.
5751
+ */
5752
+ function featureCollectionToGeoArrow(collection) {
5753
+ const { features } = collection;
5754
+ if (!features || features.length === 0) return {
5755
+ geometryType: "Point",
5756
+ coordinates: /* @__PURE__ */ new Float64Array(0),
5757
+ coordDimensions: 2,
5758
+ featureCount: 0,
5759
+ properties: []
5760
+ };
5761
+ let primaryType = null;
5762
+ let hasZ = false;
5763
+ for (const f of features) {
5764
+ const g = f.geometry;
5765
+ if (!g) continue;
5766
+ if (primaryType === null) {
5767
+ if (g.type === "Point" || g.type === "MultiPoint" || g.type === "LineString" || g.type === "Polygon") primaryType = g.type;
5768
+ else return null;
5769
+ } else if (g.type !== primaryType) {
5770
+ if (primaryType === "Point" && g.type === "MultiPoint" || primaryType === "MultiPoint" && g.type === "Point") primaryType = "MultiPoint";
5771
+ else return null;
5772
+ }
5773
+ if (!hasZ) {
5774
+ const positionHasZ = (position) => position.length > 2 && Number.isFinite(position[2]);
5775
+ if (g.type === "Point") hasZ = positionHasZ(g.coordinates);
5776
+ else if (g.type === "Polygon") hasZ = g.coordinates.some((ring) => ring.some(positionHasZ));
5777
+ else if (g.type === "LineString" || g.type === "MultiPoint") hasZ = g.coordinates.some(positionHasZ);
5778
+ }
5779
+ }
5780
+ if (!primaryType) primaryType = "Point";
5781
+ const dim = hasZ ? 3 : 2;
5782
+ const featureCount = features.length;
5783
+ const properties = new Array(featureCount);
5784
+ const featureIds = new Array(featureCount);
5785
+ for (let i = 0; i < featureCount; i++) {
5786
+ properties[i] = features[i].properties ?? {};
5787
+ featureIds[i] = features[i].id;
5788
+ }
5789
+ if (primaryType === "Point") {
5790
+ const coordinates = new Float64Array(featureCount * dim);
5791
+ for (let i = 0; i < featureCount; i++) {
5792
+ const g = features[i].geometry;
5793
+ const o = i * dim;
5794
+ if (g && g.type === "Point" && Array.isArray(g.coordinates)) {
5795
+ coordinates[o] = Number(g.coordinates[0] ?? NaN);
5796
+ coordinates[o + 1] = Number(g.coordinates[1] ?? NaN);
5797
+ if (dim === 3) coordinates[o + 2] = Number(g.coordinates[2] ?? 0);
5798
+ } else {
5799
+ coordinates[o] = NaN;
5800
+ coordinates[o + 1] = NaN;
5801
+ if (dim === 3) coordinates[o + 2] = NaN;
5802
+ }
5803
+ }
5804
+ const batch = {
5805
+ geometryType: "Point",
5806
+ coordinates,
5807
+ coordDimensions: dim,
5808
+ featureCount,
5809
+ properties,
5810
+ featureIds
5811
+ };
5812
+ const bbox = computeGeoArrowBBox(batch);
5813
+ return bbox ? {
5814
+ ...batch,
5815
+ bbox
5816
+ } : batch;
5817
+ }
5818
+ if (primaryType === "LineString" || primaryType === "MultiPoint") {
5819
+ const geomOffsets = new Int32Array(featureCount + 1);
5820
+ let totalVertices = 0;
5821
+ for (let i = 0; i < featureCount; i++) {
5822
+ geomOffsets[i] = totalVertices;
5823
+ const vertices = extractVerticesForLineOrMultiPoint(features[i].geometry, primaryType);
5824
+ if (vertices) totalVertices += vertices.length;
5825
+ }
5826
+ geomOffsets[featureCount] = totalVertices;
5827
+ const coordinates = new Float64Array(totalVertices * dim);
5828
+ let vIdx = 0;
5829
+ for (let i = 0; i < featureCount; i++) {
5830
+ const vertices = extractVerticesForLineOrMultiPoint(features[i].geometry, primaryType);
5831
+ if (vertices) for (const coord of vertices) {
5832
+ const o = vIdx * dim;
5833
+ coordinates[o] = Number(coord[0] ?? NaN);
5834
+ coordinates[o + 1] = Number(coord[1] ?? NaN);
5835
+ if (dim === 3) coordinates[o + 2] = Number(coord[2] ?? 0);
5836
+ vIdx++;
5837
+ }
5838
+ }
5839
+ const batch = {
5840
+ geometryType: primaryType,
5841
+ coordinates,
5842
+ coordDimensions: dim,
5843
+ geomOffsets,
5844
+ featureCount,
5845
+ properties,
5846
+ featureIds
5847
+ };
5848
+ const bbox = computeGeoArrowBBox(batch);
5849
+ return bbox ? {
5850
+ ...batch,
5851
+ bbox
5852
+ } : batch;
5853
+ }
5854
+ if (primaryType === "Polygon") {
5855
+ const geomOffsets = new Int32Array(featureCount + 1);
5856
+ let totalRings = 0;
5857
+ let totalVertices = 0;
5858
+ for (let i = 0; i < featureCount; i++) {
5859
+ geomOffsets[i] = totalRings;
5860
+ const g = features[i].geometry;
5861
+ if (g && g.type === "Polygon" && Array.isArray(g.coordinates)) {
5862
+ totalRings += g.coordinates.length;
5863
+ for (const ring of g.coordinates) totalVertices += ring.length;
5864
+ }
5865
+ }
5866
+ geomOffsets[featureCount] = totalRings;
5867
+ const ringOffsets = new Int32Array(totalRings + 1);
5868
+ const coordinates = new Float64Array(totalVertices * dim);
5869
+ let rIdx = 0;
5870
+ let vIdx = 0;
5871
+ for (let i = 0; i < featureCount; i++) {
5872
+ const g = features[i].geometry;
5873
+ if (g && g.type === "Polygon" && Array.isArray(g.coordinates)) for (const ring of g.coordinates) {
5874
+ ringOffsets[rIdx++] = vIdx;
5875
+ for (const coord of ring) {
5876
+ const o = vIdx * dim;
5877
+ coordinates[o] = Number(coord[0] ?? NaN);
5878
+ coordinates[o + 1] = Number(coord[1] ?? NaN);
5879
+ if (dim === 3) coordinates[o + 2] = Number(coord[2] ?? 0);
5880
+ vIdx++;
5881
+ }
5882
+ }
5883
+ }
5884
+ ringOffsets[totalRings] = vIdx;
5885
+ const batch = {
5886
+ geometryType: "Polygon",
5887
+ coordinates,
5888
+ coordDimensions: dim,
5889
+ geomOffsets,
5890
+ ringOffsets,
5891
+ featureCount,
5892
+ properties,
5893
+ featureIds
5894
+ };
5895
+ const bbox = computeGeoArrowBBox(batch);
5896
+ return bbox ? {
5897
+ ...batch,
5898
+ bbox
5899
+ } : batch;
5900
+ }
5901
+ return null;
5902
+ }
5903
+ //#endregion
4945
5904
  //#region src/czml.ts
4946
5905
  /**
4947
5906
  * CZML (Cesium Language) domain models and layer authoring (issue #2290).
@@ -5436,7 +6395,12 @@ const PUBLISHABLE_PLUGIN_SETTINGS = {
5436
6395
  "maplibre-gl-components": ["legend", "colorbar"],
5437
6396
  "maplibre-gl-time-slider": null,
5438
6397
  "gods-eye-view": null,
5439
- "geolibre-point-cloud-annotation": null
6398
+ "geolibre-point-cloud-annotation": null,
6399
+ "maplibre-gl-geolens": [
6400
+ "serverUrl",
6401
+ "mapKey",
6402
+ "layerKeys"
6403
+ ]
5440
6404
  };
5441
6405
  /**
5442
6406
  * Hash a credential value to a short, stable 64-bit token: two independent
@@ -5787,6 +6751,1289 @@ function redactCredentials(project) {
5787
6751
  return redactProjectCredentials(project).project;
5788
6752
  }
5789
6753
  //#endregion
6754
+ //#region src/storymap-duration.ts
6755
+ /** Default chapter dwell time after the camera arrives, in seconds. */
6756
+ const DEFAULT_STORY_CHAPTER_DURATION_SECONDS = 15;
6757
+ const MIN_STORY_CHAPTER_DURATION_SECONDS = 1;
6758
+ const MAX_STORY_CHAPTER_DURATION_SECONDS = 3600;
6759
+ /** Resolve old or hand-authored chapter timings to a safe autoplay duration. */
6760
+ function normalizeStoryChapterDurationSeconds(value) {
6761
+ if (typeof value !== "number" || !Number.isFinite(value)) return 15;
6762
+ return Math.min(MAX_STORY_CHAPTER_DURATION_SECONDS, Math.max(1, value));
6763
+ }
6764
+ //#endregion
6765
+ //#region src/geocoding.ts
6766
+ /**
6767
+ * Geocoding client and pure helpers shared by the batch-geocode dialog and the
6768
+ * reverse-geocode plugin.
6769
+ *
6770
+ * This module lives in `@geolibre/core` rather than `@geolibre/processing`
6771
+ * because `@geolibre/plugins` depends on core but not on processing, and the
6772
+ * reverse-geocode plugin needs the same client. The pure helpers (URL builders,
6773
+ * result mappers, pacing) carry no React or MapLibre dependency so they can be
6774
+ * unit-tested without a browser or network.
6775
+ *
6776
+ * Providers: geocoding is dispatched through a small {@link GeocodingProvider}
6777
+ * registry so alternatives to Nominatim (ArcGIS World Geocoder, CartoCiudad, Mapbox,
6778
+ * Pelias, Google, organization-hosted ArcGIS GeocodeServer locators) can be
6779
+ * selected per project. Each provider builds its own request
6780
+ * URLs and normalizes its response into a {@link GeocodeMatch} (forward) or a
6781
+ * {@link ReverseGeocodeDisplay} (reverse), so the dialog and plugin stay
6782
+ * provider-agnostic. Nominatim is the default.
6783
+ *
6784
+ * The 1 request/second throttle and the row cap are part of Nominatim's public
6785
+ * usage policy and are therefore applied ONLY to the default public host; a
6786
+ * self-hosted endpoint relaxes both, and keyed providers (Mapbox/ArcGIS/Google)
6787
+ * are paced by their own quotas, not by us.
6788
+ *
6789
+ * Browser fetch cannot set `User-Agent`/`Referer`, so the app is identified to
6790
+ * Nominatim via the optional `email` query parameter plus the automatically
6791
+ * sent `Referer`. The desktop build overrides this with a native (Tauri) fetch
6792
+ * via {@link setGeocodingFetch} — see the note there. See
6793
+ * docs/user-guide/data-integrations.md#geocoding.
6794
+ */
6795
+ const DEFAULT_FORWARD_GEOCODE_ENDPOINT = "https://nominatim.openstreetmap.org/search";
6796
+ const DEFAULT_REVERSE_GEOCODE_ENDPOINT = "https://nominatim.openstreetmap.org/reverse";
6797
+ /** Host whose public usage policy (1 req/sec, bulk limits) we must respect. */
6798
+ const NOMINATIM_PUBLIC_HOST = "nominatim.openstreetmap.org";
6799
+ /** Minimum spacing between requests to the public Nominatim endpoint. */
6800
+ const NOMINATIM_MIN_INTERVAL_MS = 1100;
6801
+ /** Host of the free, unkeyed public CartoCiudad geocoder (IGN/CNIG). */
6802
+ const CARTOCIUDAD_PUBLIC_HOST = "cartociudad.es";
6803
+ /**
6804
+ * Courtesy spacing (~5 req/sec) between requests to the public CartoCiudad
6805
+ * endpoint. Unlike {@link NOMINATIM_MIN_INTERVAL_MS} this is not a published
6806
+ * policy — IGN documents no rate limit — it is our own conservative default so
6807
+ * a large batch does not burst at a free public government service. See
6808
+ * {@link geocoderMinIntervalMs}.
6809
+ */
6810
+ const CARTOCIUDAD_MIN_INTERVAL_MS = 200;
6811
+ /** Max rows a single batch run will geocode against the public endpoint. */
6812
+ const PUBLIC_GEOCODE_ROW_CAP = 1e3;
6813
+ /** Property keys added to each geocoded feature. */
6814
+ const GEOCODE_LAT_KEY = "geocode_lat";
6815
+ const GEOCODE_LON_KEY = "geocode_lon";
6816
+ const GEOCODE_DISPLAY_NAME_KEY = "geocode_display_name";
6817
+ const GEOCODE_SCORE_KEY = "geocode_importance";
6818
+ /** Provider id that produced (or failed to produce) a match, when stamped. */
6819
+ const GEOCODE_PROVIDER_KEY = "geocode_provider";
6820
+ /** Whether a row matched, when stamped. See {@link GeocodeStatus}. */
6821
+ const GEOCODE_STATUS_KEY = "geocode_status";
6822
+ /** Prefix of a matched candidate's own attributes, when they are kept. */
6823
+ const GEOCODE_ATTRIBUTE_PREFIX = "geocode_attr_";
6824
+ /** The provider used when none is configured. */
6825
+ const DEFAULT_GEOCODING_PROVIDER_ID = "nominatim";
6826
+ /**
6827
+ * An error the geocoding service reported about the request itself, as opposed
6828
+ * to a network failure or a search with no matches. ArcGIS answers failures with
6829
+ * HTTP 200 and an `error` envelope, so `response.ok` alone would turn them into
6830
+ * an empty result. `code` is the service's own code (498/499 for a missing or
6831
+ * invalid token, 403 for insufficient permission).
6832
+ */
6833
+ var GeocoderServiceError = class extends Error {
6834
+ code;
6835
+ constructor(message, code) {
6836
+ super(message);
6837
+ this.name = "GeocoderServiceError";
6838
+ this.code = code;
6839
+ }
6840
+ /** Whether the service asked for a token or rejected the one it got. */
6841
+ get isAuthError() {
6842
+ return this.code === 401 || this.code === 403 || this.code === 498 || this.code === 499;
6843
+ }
6844
+ };
6845
+ /** The configured geocoder does not support the requested operation. */
6846
+ var GeocoderUnsupportedError = class extends Error {
6847
+ constructor(message) {
6848
+ super(message);
6849
+ this.name = "GeocoderUnsupportedError";
6850
+ }
6851
+ };
6852
+ function coerceScore(value) {
6853
+ if (value === void 0 || value === null || value === "") return null;
6854
+ const num = Number(value);
6855
+ return Number.isFinite(num) ? num : null;
6856
+ }
6857
+ /** Append `base` to `existing`, suffixing `_2`, `_3`, ... if the key collides. */
6858
+ function uniqueKey(base, existing) {
6859
+ if (!(base in existing)) return base;
6860
+ let suffix = 2;
6861
+ while (`${base}_${suffix}` in existing) suffix += 1;
6862
+ return `${base}_${suffix}`;
6863
+ }
6864
+ /**
6865
+ * Coerce an address-parts record to `Record<string, string>`. Provider address
6866
+ * objects can carry numbers (ArcGIS `Score`/`X`/`Y`, Pelias coordinates), so
6867
+ * values are stringified rather than unsafely cast; null/undefined are dropped.
6868
+ */
6869
+ function stringifyParts(obj) {
6870
+ const parts = {};
6871
+ for (const [key, value] of Object.entries(obj)) {
6872
+ if (value === null || value === void 0) continue;
6873
+ parts[key] = typeof value === "string" ? value : String(value);
6874
+ }
6875
+ return parts;
6876
+ }
6877
+ /** Read a string property off an unknown object, or undefined. */
6878
+ function readString(obj, key) {
6879
+ if (obj && typeof obj === "object" && key in obj) {
6880
+ const value = obj[key];
6881
+ if (typeof value === "string") return value;
6882
+ if (typeof value === "number") return String(value);
6883
+ }
6884
+ }
6885
+ /** Build a Nominatim forward-geocoding URL (jsonv2, address details on). */
6886
+ function buildForwardGeocodeUrl(endpoint, query, options = {}) {
6887
+ const url = new URL(endpoint);
6888
+ url.searchParams.set("q", query);
6889
+ url.searchParams.set("format", "jsonv2");
6890
+ url.searchParams.set("addressdetails", "1");
6891
+ url.searchParams.set("limit", String(options.limit ?? 1));
6892
+ if (options.email) url.searchParams.set("email", options.email);
6893
+ return url.toString();
6894
+ }
6895
+ /** Build a Nominatim reverse-geocoding URL (jsonv2, address details on). */
6896
+ function buildReverseGeocodeUrl(endpoint, lon, lat, options = {}) {
6897
+ const url = new URL(endpoint);
6898
+ url.searchParams.set("lat", String(lat));
6899
+ url.searchParams.set("lon", String(lon));
6900
+ url.searchParams.set("format", "jsonv2");
6901
+ url.searchParams.set("addressdetails", "1");
6902
+ if (options.zoom !== void 0) url.searchParams.set("zoom", String(options.zoom));
6903
+ if (options.email) url.searchParams.set("email", options.email);
6904
+ return url.toString();
6905
+ }
6906
+ /** Map one Nominatim forward result onto a normalized match, or null. */
6907
+ function nominatimForwardResultToMatch(result) {
6908
+ const lat = Number(result.lat);
6909
+ const lon = Number(result.lon);
6910
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
6911
+ return {
6912
+ lat,
6913
+ lon,
6914
+ displayName: result.display_name ?? "",
6915
+ score: coerceScore(result.importance)
6916
+ };
6917
+ }
6918
+ /**
6919
+ * Convert a normalized {@link GeocodeMatch} into a point Feature whose
6920
+ * properties carry the original CSV row plus `geocode_lat`/`geocode_lon`/
6921
+ * `geocode_display_name`/`geocode_importance`. The added keys are de-duplicated
6922
+ * against the original columns so an existing `geocode_lat` is not clobbered.
6923
+ * Geometry coordinates are `[lon, lat]`. Returns null when the match has no
6924
+ * finite coordinates.
6925
+ *
6926
+ * `extra.providerId`, when passed, also stamps `geocode_provider` and
6927
+ * `geocode_status: "matched"` (dropped when omitted, so the existing
6928
+ * `GeocodeDialog` call site keeps its current output unchanged).
6929
+ * `extra.attributes` also adds the candidate's own attributes, each under
6930
+ * {@link GEOCODE_ATTRIBUTE_PREFIX} plus its name.
6931
+ */
6932
+ function geocodeMatchToFeature(match, originalRow = {}, extra) {
6933
+ if (!Number.isFinite(match.lat) || !Number.isFinite(match.lon)) return null;
6934
+ const properties = { ...originalRow };
6935
+ const added = {
6936
+ [GEOCODE_LAT_KEY]: match.lat,
6937
+ [GEOCODE_LON_KEY]: match.lon,
6938
+ [GEOCODE_DISPLAY_NAME_KEY]: match.displayName ?? "",
6939
+ [GEOCODE_SCORE_KEY]: match.score
6940
+ };
6941
+ if (extra?.providerId) {
6942
+ added[GEOCODE_PROVIDER_KEY] = extra.providerId;
6943
+ added[GEOCODE_STATUS_KEY] = "matched";
6944
+ }
6945
+ if (extra?.attributes) for (const [key, value] of Object.entries(match.attributes ?? {})) added[`${GEOCODE_ATTRIBUTE_PREFIX}${key}`] = value;
6946
+ for (const [key, value] of Object.entries(added)) properties[uniqueKey(key, properties)] = value;
6947
+ return {
6948
+ type: "Feature",
6949
+ geometry: {
6950
+ type: "Point",
6951
+ coordinates: [match.lon, match.lat]
6952
+ },
6953
+ properties
6954
+ };
6955
+ }
6956
+ /**
6957
+ * Build a null-geometry Feature for a CSV row that could not be geocoded, so
6958
+ * it is kept (not silently dropped) alongside the matched point features.
6959
+ * Mirrors the null-geometry shape {@link parseDelimitedTextLayer} already uses
6960
+ * for a non-spatial attribute table. Stamps `geocode_status: "unmatched"` and
6961
+ * `geocode_provider`, de-duplicated against the original columns the same way
6962
+ * {@link geocodeMatchToFeature} does.
6963
+ */
6964
+ function unmatchedGeocodeFeature(originalRow, providerId) {
6965
+ const properties = { ...originalRow };
6966
+ const added = {
6967
+ [GEOCODE_STATUS_KEY]: "unmatched",
6968
+ [GEOCODE_PROVIDER_KEY]: providerId
6969
+ };
6970
+ for (const [key, value] of Object.entries(added)) properties[uniqueKey(key, properties)] = value;
6971
+ return {
6972
+ type: "Feature",
6973
+ geometry: null,
6974
+ properties
6975
+ };
6976
+ }
6977
+ /**
6978
+ * Convert a Nominatim forward result into a point Feature. Retained as a thin
6979
+ * wrapper over {@link geocodeMatchToFeature} for callers and tests that work
6980
+ * directly with Nominatim results. Returns null when the result has no finite
6981
+ * coordinates.
6982
+ */
6983
+ function nominatimResultToFeature(result, originalRow = {}) {
6984
+ const match = nominatimForwardResultToMatch(result);
6985
+ return match ? geocodeMatchToFeature(match, originalRow) : null;
6986
+ }
6987
+ /**
6988
+ * Resolve a Nominatim reverse result to a display string and address parts, or
6989
+ * null when the point could not be reverse-geocoded.
6990
+ */
6991
+ function nominatimReverseResultToDisplay(result) {
6992
+ if (!result || result.error) return null;
6993
+ const displayName = result.display_name?.trim();
6994
+ if (!displayName) return null;
6995
+ return {
6996
+ displayName,
6997
+ parts: result.address ?? {}
6998
+ };
6999
+ }
7000
+ const nominatimProvider = {
7001
+ id: "nominatim",
7002
+ label: "Nominatim (OpenStreetMap)",
7003
+ forward: true,
7004
+ reverse: true,
7005
+ requiresApiKey: false,
7006
+ acceptsApiKey: false,
7007
+ defaultForwardEndpoint: DEFAULT_FORWARD_GEOCODE_ENDPOINT,
7008
+ defaultReverseEndpoint: DEFAULT_REVERSE_GEOCODE_ENDPOINT,
7009
+ buildForwardUrl: (config, query, options) => buildForwardGeocodeUrl(config.forwardEndpoint, query, {
7010
+ email: options.email,
7011
+ limit: options.limit
7012
+ }),
7013
+ parseForward: (data) => Array.isArray(data) ? data.map(nominatimForwardResultToMatch).filter((m) => m !== null) : [],
7014
+ buildReverseUrl: (config, lon, lat, options) => buildReverseGeocodeUrl(config.reverseEndpoint, lon, lat, {
7015
+ email: options.email,
7016
+ zoom: options.zoom
7017
+ }),
7018
+ parseReverse: (data) => nominatimReverseResultToDisplay(data && typeof data === "object" ? data : null)
7019
+ };
7020
+ /** Read a GeoJSON FeatureCollection's features array, or []. */
7021
+ function geojsonFeatures(data) {
7022
+ if (data && typeof data === "object" && Array.isArray(data.features)) return data.features.filter((f) => !!f && typeof f === "object");
7023
+ return [];
7024
+ }
7025
+ /** Pull `[lon, lat]` from a GeoJSON Point geometry, or null. */
7026
+ function pointCoords(feature) {
7027
+ const coords = feature.geometry?.coordinates;
7028
+ if (Array.isArray(coords) && coords.length >= 2) {
7029
+ const lon = Number(coords[0]);
7030
+ const lat = Number(coords[1]);
7031
+ if (Number.isFinite(lon) && Number.isFinite(lat)) return [lon, lat];
7032
+ }
7033
+ return null;
7034
+ }
7035
+ const peliasProvider = {
7036
+ id: "pelias",
7037
+ label: "Pelias",
7038
+ forward: true,
7039
+ reverse: true,
7040
+ requiresApiKey: false,
7041
+ acceptsApiKey: true,
7042
+ defaultForwardEndpoint: "https://api.geocode.earth/v1/search",
7043
+ defaultReverseEndpoint: "https://api.geocode.earth/v1/reverse",
7044
+ buildForwardUrl: (config, query, options) => {
7045
+ const url = new URL(config.forwardEndpoint);
7046
+ url.searchParams.set("text", query);
7047
+ if (options.limit) url.searchParams.set("size", String(options.limit));
7048
+ if (config.apiKey) url.searchParams.set("api_key", config.apiKey);
7049
+ return url.toString();
7050
+ },
7051
+ parseForward: (data) => geojsonFeatures(data).map((feature) => {
7052
+ const coords = pointCoords(feature);
7053
+ if (!coords) return null;
7054
+ const props = feature.properties ?? {};
7055
+ return {
7056
+ lon: coords[0],
7057
+ lat: coords[1],
7058
+ displayName: readString(props, "label") ?? "",
7059
+ score: coerceScore(props.confidence)
7060
+ };
7061
+ }).filter((m) => m !== null),
7062
+ buildReverseUrl: (config, lon, lat) => {
7063
+ const url = new URL(config.reverseEndpoint);
7064
+ url.searchParams.set("point.lat", String(lat));
7065
+ url.searchParams.set("point.lon", String(lon));
7066
+ if (config.apiKey) url.searchParams.set("api_key", config.apiKey);
7067
+ return url.toString();
7068
+ },
7069
+ parseReverse: (data) => {
7070
+ const feature = geojsonFeatures(data)[0];
7071
+ if (!feature) return null;
7072
+ const props = feature.properties ?? {};
7073
+ const displayName = readString(props, "label")?.trim();
7074
+ return displayName ? {
7075
+ displayName,
7076
+ parts: stringifyParts(props)
7077
+ } : null;
7078
+ }
7079
+ };
7080
+ const arcgisProvider = {
7081
+ id: "arcgis",
7082
+ label: "ArcGIS World Geocoder",
7083
+ forward: true,
7084
+ reverse: true,
7085
+ requiresApiKey: true,
7086
+ acceptsApiKey: true,
7087
+ defaultForwardEndpoint: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates",
7088
+ defaultReverseEndpoint: "https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode",
7089
+ buildForwardUrl: (config, query, options) => {
7090
+ const url = new URL(config.forwardEndpoint);
7091
+ url.searchParams.set("SingleLine", query);
7092
+ url.searchParams.set("f", "json");
7093
+ url.searchParams.set("outFields", "Match_addr");
7094
+ if (options.limit) url.searchParams.set("maxLocations", String(options.limit));
7095
+ if (config.apiKey) url.searchParams.set("token", config.apiKey);
7096
+ return url.toString();
7097
+ },
7098
+ parseForward: parseArcGISCandidates,
7099
+ buildReverseUrl: (config, lon, lat) => {
7100
+ const url = new URL(config.reverseEndpoint);
7101
+ url.searchParams.set("location", `${lon},${lat}`);
7102
+ url.searchParams.set("f", "json");
7103
+ if (config.apiKey) url.searchParams.set("token", config.apiKey);
7104
+ return url.toString();
7105
+ },
7106
+ parseReverse: parseArcGISReverse
7107
+ };
7108
+ /** Spatial references ArcGIS reports for Web Mercator. */
7109
+ const WEB_MERCATOR_WKIDS = /* @__PURE__ */ new Set([
7110
+ 102100,
7111
+ 102113,
7112
+ 3857,
7113
+ 900913
7114
+ ]);
7115
+ /** Earth radius of the Web Mercator sphere, in meters. */
7116
+ const WEB_MERCATOR_RADIUS = 6378137;
7117
+ /** The operations a pasted GeocodeServer URL may already end in. */
7118
+ const LOCATOR_OPERATION = /\/(findAddressCandidates|reverseGeocode|suggest|geocodeAddresses)$/i;
7119
+ /** The ArcGIS `error` envelope in a response body, as a {@link GeocoderServiceError}. */
7120
+ function arcgisServiceError(data) {
7121
+ const error = data && typeof data === "object" ? data.error : void 0;
7122
+ if (!error || typeof error !== "object") return null;
7123
+ const code = Number(error.code);
7124
+ return new GeocoderServiceError(readString(error, "message") ?? "ArcGIS geocoding service error", Number.isFinite(code) ? code : void 0);
7125
+ }
7126
+ /** The well-known ID of an ArcGIS `spatialReference`, preferring `latestWkid`. */
7127
+ function spatialReferenceWkid(spatialReference) {
7128
+ if (!spatialReference || typeof spatialReference !== "object") return null;
7129
+ const sr = spatialReference;
7130
+ const wkid = Number(sr.latestWkid ?? sr.wkid);
7131
+ return Number.isFinite(wkid) ? wkid : null;
7132
+ }
7133
+ /**
7134
+ * A candidate's position as longitude/latitude. Organization-hosted locators
7135
+ * often work in a projected system, and ArcGIS ignores an `outSR` it cannot
7136
+ * honor, so the reported spatial reference decides: geographic WGS 84 (or none,
7137
+ * the World Geocoder's default) is read as is, Web Mercator is unprojected, and
7138
+ * anything else is refused rather than plotted in the wrong place.
7139
+ *
7140
+ * @param x - The candidate's x coordinate.
7141
+ * @param y - The candidate's y coordinate.
7142
+ * @param wkid - The response's spatial reference, or null when it reports none.
7143
+ * @returns The position, or null when the coordinates are not finite or not on Earth.
7144
+ * @throws GeocoderServiceError When the coordinates are in an unsupported spatial reference.
7145
+ */
7146
+ function arcgisCandidatePosition(x, y, wkid) {
7147
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null;
7148
+ let lon = x;
7149
+ let lat = y;
7150
+ if (wkid !== null && WEB_MERCATOR_WKIDS.has(wkid)) {
7151
+ lon = x / WEB_MERCATOR_RADIUS * (180 / Math.PI);
7152
+ lat = (2 * Math.atan(Math.exp(y / WEB_MERCATOR_RADIUS)) - Math.PI / 2) * (180 / Math.PI);
7153
+ } else if (wkid !== null && wkid !== 4326) throw new GeocoderServiceError(`The geocoder returned coordinates in spatial reference ${wkid}, which GeoLibre cannot place on the map.`);
7154
+ if (lon < -180 || lon > 180 || lat < -90 || lat > 90) return null;
7155
+ return {
7156
+ lon,
7157
+ lat
7158
+ };
7159
+ }
7160
+ /**
7161
+ * A candidate's attributes worth keeping: plain values that are not empty.
7162
+ * The shape and the location fields the match already carries are dropped.
7163
+ */
7164
+ function candidateAttributes(value) {
7165
+ if (!value || typeof value !== "object") return void 0;
7166
+ const kept = {};
7167
+ for (const [key, entry] of Object.entries(value)) {
7168
+ if (/^(shape|x|y|displayx|displayy|xmin|xmax|ymin|ymax)$/i.test(key)) continue;
7169
+ if (typeof entry === "string" ? entry.trim() === "" : typeof entry !== "number" && typeof entry !== "boolean") continue;
7170
+ if (typeof entry === "number" && !Number.isFinite(entry)) continue;
7171
+ kept[key] = entry;
7172
+ }
7173
+ return Object.keys(kept).length ? kept : void 0;
7174
+ }
7175
+ /**
7176
+ * Normalize an ArcGIS `findAddressCandidates` response. Throws the service's
7177
+ * error envelope instead of reporting it as "no matches".
7178
+ *
7179
+ * @param data - The parsed response body.
7180
+ * @returns The candidates as normalized matches.
7181
+ */
7182
+ function parseArcGISCandidates(data) {
7183
+ const error = arcgisServiceError(data);
7184
+ if (error) throw error;
7185
+ if (!data || typeof data !== "object") return [];
7186
+ const body = data;
7187
+ if (!Array.isArray(body.candidates)) return [];
7188
+ const wkid = spatialReferenceWkid(body.spatialReference);
7189
+ return body.candidates.map((candidate) => {
7190
+ const location = candidate.location;
7191
+ const position = arcgisCandidatePosition(Number(location?.x), Number(location?.y), wkid);
7192
+ if (!position) return null;
7193
+ const attributes = candidateAttributes(candidate.attributes);
7194
+ return {
7195
+ ...position,
7196
+ displayName: readString(candidate, "address") ?? "",
7197
+ score: coerceScore(candidate.score),
7198
+ ...attributes ? { attributes } : {}
7199
+ };
7200
+ }).filter((m) => m !== null);
7201
+ }
7202
+ /**
7203
+ * Whether an ArcGIS error envelope is the "Unable to find address" answer:
7204
+ * code 400 with that text in its message or (Enterprise) its details. Other
7205
+ * 400s, such as malformed parameters, are real failures.
7206
+ */
7207
+ function isNoAddressError(data, error) {
7208
+ if (error.code !== 400) return false;
7209
+ const envelope = data.error;
7210
+ const details = Array.isArray(envelope?.details) ? envelope.details.join(" ") : "";
7211
+ return /unable to find address/i.test(`${error.message} ${details}`);
7212
+ }
7213
+ /**
7214
+ * Normalize an ArcGIS `reverseGeocode` response. "Unable to find address" comes
7215
+ * back as an `error` envelope with code 400, which is a valid empty answer
7216
+ * rather than a failure.
7217
+ *
7218
+ * @param data - The parsed response body.
7219
+ * @returns The address, or null when there is none.
7220
+ */
7221
+ function parseArcGISReverse(data) {
7222
+ const error = arcgisServiceError(data);
7223
+ if (error && !isNoAddressError(data, error)) throw error;
7224
+ const address = data && typeof data === "object" ? data.address : void 0;
7225
+ if (!address || typeof address !== "object") return null;
7226
+ const displayName = (readString(address, "LongLabel") ?? readString(address, "Match_addr") ?? readString(address, "Address"))?.trim();
7227
+ return displayName ? {
7228
+ displayName,
7229
+ parts: stringifyParts(address)
7230
+ } : null;
7231
+ }
7232
+ /**
7233
+ * The GeocodeServer base URL of a pasted locator URL: an operation suffix
7234
+ * (`/findAddressCandidates`, ...), the query string and any credentials are
7235
+ * dropped, so a token in a pasted URL is never stored.
7236
+ *
7237
+ * @param input - The URL as typed.
7238
+ * @returns The base URL ending in `/GeocodeServer`, or null when it is not one.
7239
+ */
7240
+ function normalizeArcGISLocatorUrl(input) {
7241
+ let url;
7242
+ try {
7243
+ url = new URL((input ?? "").trim());
7244
+ } catch {
7245
+ return null;
7246
+ }
7247
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
7248
+ const path = url.pathname.replace(/\/+$/, "").replace(LOCATOR_OPERATION, "");
7249
+ if (!/\/GeocodeServer$/i.test(path)) return null;
7250
+ url.pathname = path;
7251
+ url.search = "";
7252
+ url.hash = "";
7253
+ url.username = "";
7254
+ url.password = "";
7255
+ return url.toString();
7256
+ }
7257
+ /**
7258
+ * Coerce a stored locator to a {@link GeocodingLocator}, or null when it has no
7259
+ * usable id or GeocodeServer URL. Used for project files and runtime env, so
7260
+ * anything unexpected is dropped rather than trusted.
7261
+ *
7262
+ * @param value - The stored value.
7263
+ * @returns The locator, or null.
7264
+ */
7265
+ function normalizeGeocodingLocator(value) {
7266
+ if (!value || typeof value !== "object") return null;
7267
+ const candidate = value;
7268
+ const id = typeof candidate.id === "string" ? candidate.id.trim() : "";
7269
+ const url = normalizeArcGISLocatorUrl(typeof candidate.url === "string" ? candidate.url : void 0);
7270
+ if (!id || !url) return null;
7271
+ const text = (key) => typeof candidate[key] === "string" && candidate[key].trim() ? candidate[key].trim() : void 0;
7272
+ const flag = (key) => typeof candidate[key] === "boolean" ? candidate[key] : void 0;
7273
+ const addressFields = Array.isArray(candidate.addressFields) ? candidate.addressFields.filter((field) => typeof field === "string" && field.trim() !== "") : void 0;
7274
+ const locator = {
7275
+ id,
7276
+ name: text("name") ?? id,
7277
+ url
7278
+ };
7279
+ const portalUrl = text("portalUrl");
7280
+ if (portalUrl) locator.portalUrl = portalUrl;
7281
+ const singleLineField = text("singleLineField");
7282
+ if (singleLineField) locator.singleLineField = singleLineField;
7283
+ if (addressFields?.length) locator.addressFields = addressFields;
7284
+ const reverse = flag("reverse");
7285
+ if (reverse !== void 0) locator.reverse = reverse;
7286
+ const suggest = flag("suggest");
7287
+ if (suggest !== void 0) locator.suggest = suggest;
7288
+ const searchField = text("searchField");
7289
+ if (searchField) locator.searchField = searchField;
7290
+ return locator;
7291
+ }
7292
+ /**
7293
+ * Read a GeocodeServer's capabilities from its `?f=json` metadata. A service
7294
+ * that lists no `capabilities` (older ArcGIS Server) is taken to support
7295
+ * geocoding and reverse geocoding but not suggestions.
7296
+ *
7297
+ * @param data - The parsed metadata.
7298
+ * @returns The capabilities.
7299
+ * @throws GeocoderServiceError For an error envelope, or metadata that is not a
7300
+ * GeocodeServer's.
7301
+ * @throws GeocoderUnsupportedError When the service does not geocode.
7302
+ */
7303
+ function parseArcGISLocatorInfo(data) {
7304
+ const error = arcgisServiceError(data);
7305
+ if (error) throw error;
7306
+ if (!data || typeof data !== "object") throw new GeocoderServiceError("The service did not return GeocodeServer metadata.");
7307
+ const info = data;
7308
+ const fieldName = (field) => field && typeof field === "object" ? readString(field, "name")?.trim() || void 0 : void 0;
7309
+ const addressFields = Array.isArray(info.addressFields) ? info.addressFields.map(fieldName).filter((name) => !!name) : [];
7310
+ const singleLineField = fieldName(info.singleLineAddressField);
7311
+ if (!singleLineField && !addressFields.length) throw new GeocoderServiceError("The service did not return GeocodeServer metadata.");
7312
+ const listed = typeof info.capabilities === "string" ? new Set(info.capabilities.split(",").map((entry) => entry.trim().toLowerCase())) : null;
7313
+ if (listed && !listed.has("geocode")) throw new GeocoderUnsupportedError("This locator does not support geocoding.");
7314
+ return {
7315
+ singleLineField,
7316
+ addressFields: addressFields.length ? addressFields : void 0,
7317
+ reverse: listed ? listed.has("reversegeocode") : true,
7318
+ suggest: listed ? listed.has("suggest") : false
7319
+ };
7320
+ }
7321
+ /**
7322
+ * The input field a single search string is sent in: the published field the
7323
+ * user chose for it, else the locator's single-line field, else its only
7324
+ * structured field (an asset ID locator often publishes just one), else
7325
+ * ArcGIS's conventional `SingleLine`.
7326
+ *
7327
+ * @param locator - The locator.
7328
+ * @returns The input field name.
7329
+ */
7330
+ function arcgisLocatorInputField(locator) {
7331
+ const published = [locator.singleLineField, ...locator.addressFields ?? []];
7332
+ if (locator.searchField && published.includes(locator.searchField)) return locator.searchField;
7333
+ if (locator.singleLineField) return locator.singleLineField;
7334
+ if (locator.addressFields?.length === 1) return locator.addressFields[0];
7335
+ return "SingleLine";
7336
+ }
7337
+ /** The config's locator, or an error naming what is missing. */
7338
+ function requireLocator(config) {
7339
+ if (!config.locator) throw new GeocoderUnsupportedError("No ArcGIS GeocodeServer locator is selected.");
7340
+ return config.locator;
7341
+ }
7342
+ const arcgisLocatorProvider = {
7343
+ id: "arcgis-locator",
7344
+ label: "ArcGIS GeocodeServer (organization)",
7345
+ forward: true,
7346
+ reverse: true,
7347
+ requiresApiKey: false,
7348
+ acceptsApiKey: false,
7349
+ defaultForwardEndpoint: "",
7350
+ defaultReverseEndpoint: "",
7351
+ buildForwardUrl: (config, query, options) => {
7352
+ const locator = requireLocator(config);
7353
+ const url = new URL(config.forwardEndpoint || `${locator.url}/findAddressCandidates`);
7354
+ url.searchParams.set(arcgisLocatorInputField(locator), query);
7355
+ url.searchParams.set("f", "json");
7356
+ url.searchParams.set("outSR", "4326");
7357
+ if (options.attributes) url.searchParams.set("outFields", "*");
7358
+ if (options.magicKey) url.searchParams.set("magicKey", options.magicKey);
7359
+ if (options.limit) url.searchParams.set("maxLocations", String(options.limit));
7360
+ return url.toString();
7361
+ },
7362
+ parseForward: parseArcGISCandidates,
7363
+ buildReverseUrl: (config, lon, lat) => {
7364
+ const locator = requireLocator(config);
7365
+ const url = new URL(config.reverseEndpoint || `${locator.url}/reverseGeocode`);
7366
+ url.searchParams.set("location", JSON.stringify({
7367
+ x: lon,
7368
+ y: lat,
7369
+ spatialReference: { wkid: 4326 }
7370
+ }));
7371
+ url.searchParams.set("outSR", "4326");
7372
+ url.searchParams.set("f", "json");
7373
+ return url.toString();
7374
+ },
7375
+ parseReverse: parseArcGISReverse
7376
+ };
7377
+ const mapboxProvider = {
7378
+ id: "mapbox",
7379
+ label: "Mapbox",
7380
+ forward: true,
7381
+ reverse: true,
7382
+ requiresApiKey: true,
7383
+ acceptsApiKey: true,
7384
+ defaultForwardEndpoint: "https://api.mapbox.com/geocoding/v5/mapbox.places",
7385
+ defaultReverseEndpoint: "https://api.mapbox.com/geocoding/v5/mapbox.places",
7386
+ buildForwardUrl: (config, query, options) => {
7387
+ const base = config.forwardEndpoint.replace(/\/+$/, "");
7388
+ const url = new URL(`${base}/${encodeURIComponent(query)}.json`);
7389
+ if (options.limit) url.searchParams.set("limit", String(options.limit));
7390
+ if (config.apiKey) url.searchParams.set("access_token", config.apiKey);
7391
+ return url.toString();
7392
+ },
7393
+ parseForward: (data) => geojsonFeatures(data).map((feature) => {
7394
+ const center = feature.center;
7395
+ let coords = null;
7396
+ if (Array.isArray(center) && center.length >= 2) {
7397
+ const lon = Number(center[0]);
7398
+ const lat = Number(center[1]);
7399
+ if (Number.isFinite(lon) && Number.isFinite(lat)) coords = [lon, lat];
7400
+ }
7401
+ coords ??= pointCoords(feature);
7402
+ if (!coords) return null;
7403
+ return {
7404
+ lon: coords[0],
7405
+ lat: coords[1],
7406
+ displayName: readString(feature, "place_name") ?? "",
7407
+ score: coerceScore(feature.relevance)
7408
+ };
7409
+ }).filter((m) => m !== null),
7410
+ buildReverseUrl: (config, lon, lat) => {
7411
+ const base = config.reverseEndpoint.replace(/\/+$/, "");
7412
+ const url = new URL(`${base}/${lon},${lat}.json`);
7413
+ if (config.apiKey) url.searchParams.set("access_token", config.apiKey);
7414
+ return url.toString();
7415
+ },
7416
+ parseReverse: (data) => {
7417
+ const feature = geojsonFeatures(data)[0];
7418
+ if (!feature) return null;
7419
+ const displayName = readString(feature, "place_name")?.trim();
7420
+ return displayName ? {
7421
+ displayName,
7422
+ parts: stringifyParts(feature.properties ?? {})
7423
+ } : null;
7424
+ }
7425
+ };
7426
+ /**
7427
+ * Detect a Google Geocoding API error embedded in an HTTP 200 body. Google v3
7428
+ * returns REQUEST_DENIED / OVER_QUERY_LIMIT / INVALID_REQUEST and friends with
7429
+ * a 200 status and the failure only in the `status` field, so `response.ok`
7430
+ * alone would silently turn those into empty results. Returns null for OK and
7431
+ * ZERO_RESULTS (a successful empty match).
7432
+ */
7433
+ function googleErrorMessage(data) {
7434
+ const status = readString(data, "status");
7435
+ if (!status || status === "OK" || status === "ZERO_RESULTS") return null;
7436
+ const detail = readString(data, "error_message");
7437
+ return detail ? `Google geocoder error: ${status} - ${detail}` : `Google geocoder error: ${status}`;
7438
+ }
7439
+ const googleProvider = {
7440
+ id: "google",
7441
+ label: "Google",
7442
+ forward: true,
7443
+ reverse: true,
7444
+ requiresApiKey: true,
7445
+ acceptsApiKey: true,
7446
+ browserCorsRestricted: true,
7447
+ defaultForwardEndpoint: "https://maps.googleapis.com/maps/api/geocode/json",
7448
+ defaultReverseEndpoint: "https://maps.googleapis.com/maps/api/geocode/json",
7449
+ buildForwardUrl: (config, query) => {
7450
+ const url = new URL(config.forwardEndpoint);
7451
+ url.searchParams.set("address", query);
7452
+ if (config.apiKey) url.searchParams.set("key", config.apiKey);
7453
+ return url.toString();
7454
+ },
7455
+ parseForward: (data) => {
7456
+ const error = googleErrorMessage(data);
7457
+ if (error) throw new Error(error);
7458
+ return parseGoogleResults(data);
7459
+ },
7460
+ buildReverseUrl: (config, lon, lat) => {
7461
+ const url = new URL(config.reverseEndpoint);
7462
+ url.searchParams.set("latlng", `${lat},${lon}`);
7463
+ if (config.apiKey) url.searchParams.set("key", config.apiKey);
7464
+ return url.toString();
7465
+ },
7466
+ parseReverse: (data) => {
7467
+ const error = googleErrorMessage(data);
7468
+ if (error) throw new Error(error);
7469
+ const displayName = readString((data && typeof data === "object" && Array.isArray(data.results) ? data.results : [])[0], "formatted_address")?.trim();
7470
+ return displayName ? {
7471
+ displayName,
7472
+ parts: {}
7473
+ } : null;
7474
+ }
7475
+ };
7476
+ /** Map a Google Geocoding API `results` array onto normalized matches. */
7477
+ function parseGoogleResults(data) {
7478
+ return (data && typeof data === "object" && Array.isArray(data.results) ? data.results : []).map((result) => {
7479
+ const location = result.geometry?.location;
7480
+ const lat = Number(location?.lat);
7481
+ const lon = Number(location?.lng);
7482
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
7483
+ return {
7484
+ lat,
7485
+ lon,
7486
+ displayName: readString(result, "formatted_address") ?? "",
7487
+ score: null
7488
+ };
7489
+ }).filter((m) => m !== null);
7490
+ }
7491
+ /**
7492
+ * Read a CartoCiudad `lat`/`lng` field as a finite number, or null when the
7493
+ * field is absent, blank, or not numeric (`"NaN"` and `"invalid"` included).
7494
+ * Guarding the raw value matters because `Number(null)` is 0, which would turn
7495
+ * a coordinate-less response into a match in the Gulf of Guinea.
7496
+ */
7497
+ function cartociudadCoordinate(value) {
7498
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
7499
+ if (typeof value !== "string" || !value.trim()) return null;
7500
+ const parsed = Number(value);
7501
+ return Number.isFinite(parsed) ? parsed : null;
7502
+ }
7503
+ /** Validated WGS84 position for a result, or null when it carries none. */
7504
+ function cartociudadCoords(r) {
7505
+ const lat = cartociudadCoordinate(r.lat);
7506
+ const lon = cartociudadCoordinate(r.lng);
7507
+ if (lat === null || lon === null) return null;
7508
+ if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;
7509
+ return {
7510
+ lat,
7511
+ lon
7512
+ };
7513
+ }
7514
+ /** Default endpoints for the public CartoCiudad REST API (no auth required). */
7515
+ const CARTOCIUDAD_FORWARD_ENDPOINT = "https://www.cartociudad.es/geocoder/api/geocoder/find";
7516
+ const CARTOCIUDAD_REVERSE_ENDPOINT = "https://www.cartociudad.es/geocoder/api/geocoder/reverseGeocode";
7517
+ /**
7518
+ * Build a human-readable display name from CartoCiudad address fields.
7519
+ * Format: "Tip Via Address Num, CP Poblacion (Province)"
7520
+ */
7521
+ function cartociudadDisplayName(r) {
7522
+ const parts = [];
7523
+ const streetParts = [r.tip_via, r.address].filter((s) => s && String(s).trim()).map((s) => String(s).trim());
7524
+ if (streetParts.length) {
7525
+ let street = streetParts.join(" ");
7526
+ const portalNumber = r.portalNumber === void 0 || r.portalNumber === null ? "" : String(r.portalNumber).trim();
7527
+ if (portalNumber) street += ` ${portalNumber}`;
7528
+ parts.push(street);
7529
+ }
7530
+ const localityName = [r.poblacion, r.muni].find((s) => s && String(s).trim());
7531
+ const locality = [r.postalCode, localityName].filter((s) => s && String(s).trim()).map((s) => String(s).trim()).join(" ");
7532
+ if (locality) parts.push(locality);
7533
+ const province = r.province ? String(r.province).trim() : "";
7534
+ if (province) parts.push(`(${province})`);
7535
+ return parts.join(", ") || r.address || r.poblacion || r.muni || "";
7536
+ }
7537
+ /** All selectable geocoding providers, Nominatim first (the default). */
7538
+ const GEOCODING_PROVIDERS = [
7539
+ nominatimProvider,
7540
+ peliasProvider,
7541
+ arcgisProvider,
7542
+ arcgisLocatorProvider,
7543
+ mapboxProvider,
7544
+ googleProvider,
7545
+ {
7546
+ id: "cartociudad",
7547
+ label: "CartoCiudad (IGN España)",
7548
+ forward: true,
7549
+ reverse: true,
7550
+ requiresApiKey: false,
7551
+ acceptsApiKey: false,
7552
+ defaultForwardEndpoint: CARTOCIUDAD_FORWARD_ENDPOINT,
7553
+ defaultReverseEndpoint: CARTOCIUDAD_REVERSE_ENDPOINT,
7554
+ buildForwardUrl: (config, query) => {
7555
+ const url = new URL(config.forwardEndpoint);
7556
+ url.searchParams.set("q", query);
7557
+ return url.toString();
7558
+ },
7559
+ parseForward: (data) => {
7560
+ if (!data || typeof data !== "object" || Array.isArray(data)) return [];
7561
+ const r = data;
7562
+ const coords = cartociudadCoords(r);
7563
+ if (!coords) return [];
7564
+ const displayName = cartociudadDisplayName(r).trim();
7565
+ if (!displayName) return [];
7566
+ return [{
7567
+ lat: coords.lat,
7568
+ lon: coords.lon,
7569
+ displayName,
7570
+ score: null
7571
+ }];
7572
+ },
7573
+ buildReverseUrl: (config, lon, lat) => {
7574
+ const url = new URL(config.reverseEndpoint);
7575
+ url.searchParams.set("lat", String(lat));
7576
+ url.searchParams.set("lon", String(lon));
7577
+ return url.toString();
7578
+ },
7579
+ parseReverse: (data) => {
7580
+ if (!data || typeof data !== "object" || Array.isArray(data)) return null;
7581
+ const r = data;
7582
+ if (!cartociudadCoords(r)) return null;
7583
+ const displayName = cartociudadDisplayName(r).trim();
7584
+ if (!displayName) return null;
7585
+ const parts = {};
7586
+ for (const [key, value] of Object.entries({
7587
+ type: r.type,
7588
+ tip_via: r.tip_via,
7589
+ address: r.address,
7590
+ portalNumber: r.portalNumber,
7591
+ postalCode: r.postalCode,
7592
+ poblacion: r.poblacion,
7593
+ muni: r.muni,
7594
+ province: r.province,
7595
+ comunidadAutonoma: r.comunidadAutonoma,
7596
+ refCatastral: r.refCatastral
7597
+ })) if (value !== void 0 && value !== null && value !== "") parts[key] = String(value);
7598
+ return {
7599
+ displayName,
7600
+ parts
7601
+ };
7602
+ }
7603
+ }
7604
+ ];
7605
+ const PROVIDERS_BY_ID = new Map(GEOCODING_PROVIDERS.map((provider) => [provider.id, provider]));
7606
+ /** Coerce an arbitrary string to a known provider id, defaulting to Nominatim. */
7607
+ function normalizeGeocodingProviderId(value) {
7608
+ const id = value?.trim();
7609
+ return id && PROVIDERS_BY_ID.has(id) ? id : DEFAULT_GEOCODING_PROVIDER_ID;
7610
+ }
7611
+ /** Look up a provider by id, falling back to Nominatim for unknown ids. */
7612
+ function getGeocodingProvider(id) {
7613
+ return PROVIDERS_BY_ID.get(normalizeGeocodingProviderId(id)) ?? nominatimProvider;
7614
+ }
7615
+ /**
7616
+ * Resolve the geocoder configuration from runtime env, falling back to the
7617
+ * selected provider's default endpoints. `VITE_GEOCODER_PROVIDER` selects the
7618
+ * backend; `VITE_GEOCODER_ENDPOINT`/`VITE_GEOCODER_REVERSE_ENDPOINT` override
7619
+ * the endpoints (except for `arcgis-locator`, whose endpoints always come from
7620
+ * `VITE_GEOCODER_LOCATOR`); `VITE_GEOCODER_EMAIL` supplies the contact email; and
7621
+ * `VITE_GEOCODER_API_KEY` supplies the API key for keyed providers.
7622
+ */
7623
+ function getGeocoderConfig() {
7624
+ const env = getRuntimeEnvironment();
7625
+ const provider = getGeocodingProvider(env.VITE_GEOCODER_PROVIDER);
7626
+ let locator;
7627
+ if (provider.id === "arcgis-locator" && env.VITE_GEOCODER_LOCATOR) try {
7628
+ locator = normalizeGeocodingLocator(JSON.parse(env.VITE_GEOCODER_LOCATOR)) ?? void 0;
7629
+ } catch {
7630
+ locator = void 0;
7631
+ }
7632
+ const endpoints = locator ? locatorEndpoints(locator) : provider;
7633
+ const overrides = provider.id === "arcgis-locator" ? {} : env;
7634
+ return {
7635
+ providerId: provider.id,
7636
+ forwardEndpoint: overrides.VITE_GEOCODER_ENDPOINT?.trim() || endpoints.defaultForwardEndpoint,
7637
+ reverseEndpoint: overrides.VITE_GEOCODER_REVERSE_ENDPOINT?.trim() || endpoints.defaultReverseEndpoint,
7638
+ email: env.VITE_GEOCODER_EMAIL?.trim() || void 0,
7639
+ apiKey: env.VITE_GEOCODER_API_KEY?.trim() || void 0,
7640
+ ...locator ? { locator } : {}
7641
+ };
7642
+ }
7643
+ /** A locator's operation URLs, derived from its base GeocodeServer URL. */
7644
+ function locatorEndpoints(locator) {
7645
+ return {
7646
+ defaultForwardEndpoint: `${locator.url}/findAddressCandidates`,
7647
+ defaultReverseEndpoint: `${locator.url}/reverseGeocode`
7648
+ };
7649
+ }
7650
+ /**
7651
+ * The locator the `arcgis-locator` provider uses: the one `locatorId` names,
7652
+ * else the first configured.
7653
+ *
7654
+ * @param input - The geocoding preference.
7655
+ * @returns The locator, or undefined when none is configured.
7656
+ */
7657
+ function selectedGeocodingLocator(input) {
7658
+ const locators = input.locators ?? [];
7659
+ return locators.find((locator) => locator.id === input.locatorId) ?? locators[0];
7660
+ }
7661
+ /**
7662
+ * Build a {@link GeocoderConfig} from structured project preferences. Used by
7663
+ * the geocode dialog so a per-run provider choice resolves the right endpoints
7664
+ * and API key without round-tripping through runtime env.
7665
+ */
7666
+ function resolveGeocoderConfig(input) {
7667
+ const provider = getGeocodingProvider(input.providerId);
7668
+ if (provider.id === "arcgis-locator") {
7669
+ const locator = selectedGeocodingLocator(input);
7670
+ const endpoints = locator ? locatorEndpoints(locator) : provider;
7671
+ return {
7672
+ providerId: provider.id,
7673
+ forwardEndpoint: endpoints.defaultForwardEndpoint,
7674
+ reverseEndpoint: endpoints.defaultReverseEndpoint,
7675
+ email: input.email?.trim() || void 0,
7676
+ ...locator ? { locator } : {}
7677
+ };
7678
+ }
7679
+ return {
7680
+ providerId: provider.id,
7681
+ forwardEndpoint: input.forwardEndpoint?.trim() || provider.defaultForwardEndpoint,
7682
+ reverseEndpoint: input.reverseEndpoint?.trim() || provider.defaultReverseEndpoint,
7683
+ email: input.email?.trim() || void 0,
7684
+ apiKey: input.apiKeys?.[provider.id]?.trim() || void 0
7685
+ };
7686
+ }
7687
+ /** Hostname of `endpoint`, or null when it does not parse as a URL. */
7688
+ function geocoderHostname(endpoint) {
7689
+ try {
7690
+ return new URL(endpoint).hostname;
7691
+ } catch {
7692
+ return null;
7693
+ }
7694
+ }
7695
+ /** Whether `hostname` is the public CartoCiudad service or a subdomain of it. */
7696
+ function isCartociudadHost(hostname) {
7697
+ if (hostname === null) return false;
7698
+ return hostname === "cartociudad.es" || hostname.endsWith(`.cartociudad.es`);
7699
+ }
7700
+ /**
7701
+ * Whether requests to `endpoint` must be throttled AND row-capped. True for the
7702
+ * public Nominatim host (its usage policy applies) and, defensively, for any
7703
+ * endpoint that does not parse as a URL. Every other host (a self-hosted
7704
+ * Nominatim or a keyed provider) returns false.
7705
+ *
7706
+ * Note this is the *Nominatim policy* gate, not "is this host paced at all" —
7707
+ * see {@link geocoderMinIntervalMs}, which paces CartoCiudad more gently
7708
+ * without capping its row count.
7709
+ */
7710
+ function shouldThrottle(endpoint) {
7711
+ const hostname = geocoderHostname(endpoint);
7712
+ return hostname === null || hostname === "nominatim.openstreetmap.org";
7713
+ }
7714
+ /**
7715
+ * The row cap to apply for `endpoint`: a finite cap for the public Nominatim
7716
+ * host, else Infinity. CartoCiudad is uncapped — IGN publishes no row limit and
7717
+ * its own bulk address geocoder processes up to 60,000 records per run, so a
7718
+ * cap here would be an invented constraint. It is still paced; see
7719
+ * {@link geocoderMinIntervalMs}.
7720
+ */
7721
+ function rowCap(endpoint) {
7722
+ return shouldThrottle(endpoint) ? PUBLIC_GEOCODE_ROW_CAP : Number.POSITIVE_INFINITY;
7723
+ }
7724
+ /**
7725
+ * Minimum spacing (ms) between requests for `endpoint`. Gated on the hostname,
7726
+ * not the selected provider, so pointing any provider at the public Nominatim
7727
+ * host still honors its 1 req/sec policy.
7728
+ *
7729
+ * CartoCiudad gets a gentler pace of its own. IGN documents no rate limit for
7730
+ * the single-lookup `find`/`reverseGeocode` endpoints either way, so the honest
7731
+ * position is that unbounded bursting is unverified rather than sanctioned:
7732
+ * a large CSV run against a free public government service with zero spacing
7733
+ * risks getting the caller's IP throttled and degrading the service for
7734
+ * everyone. Nominatim's 1 req/sec is that provider's published policy and is
7735
+ * not borrowed here. Keyed providers and self-hosted endpoints are paced by
7736
+ * their own quotas, not by us.
7737
+ */
7738
+ function geocoderMinIntervalMs(endpoint) {
7739
+ if (shouldThrottle(endpoint)) return NOMINATIM_MIN_INTERVAL_MS;
7740
+ return isCartociudadHost(geocoderHostname(endpoint)) ? 200 : 0;
7741
+ }
7742
+ /** Hosted Pelias service that rejects keyless requests. */
7743
+ const GEOCODE_EARTH_HOST_SUFFIX = "geocode.earth";
7744
+ /**
7745
+ * Whether `config` cannot geocode without an API key. True for the keyed
7746
+ * providers (ArcGIS, Mapbox, Google) and for Pelias only when it targets the
7747
+ * hosted geocode.earth endpoint (a self-hosted Pelias needs no key). Drives the
7748
+ * batch dialog's "missing key" warning and disabled run.
7749
+ */
7750
+ function geocoderNeedsApiKey(config) {
7751
+ const provider = getGeocodingProvider(config.providerId);
7752
+ if (provider.requiresApiKey) return true;
7753
+ if (provider.id === "pelias") try {
7754
+ const host = new URL(config.forwardEndpoint).hostname;
7755
+ return host === GEOCODE_EARTH_HOST_SUFFIX || host.endsWith(`.${GEOCODE_EARTH_HOST_SUFFIX}`);
7756
+ } catch {
7757
+ return false;
7758
+ }
7759
+ return false;
7760
+ }
7761
+ /**
7762
+ * Milliseconds to wait before starting the next request so consecutive
7763
+ * requests are spaced at least `intervalMs` apart, measured from the previous
7764
+ * request's start time (not its completion) so a slow network does not double
7765
+ * the wait. Returns 0 for the first request or when enough time has elapsed.
7766
+ */
7767
+ function nextDelayMs(lastStartedAt, now, intervalMs) {
7768
+ if (lastStartedAt === null) return 0;
7769
+ return Math.max(0, intervalMs - (now - lastStartedAt));
7770
+ }
7771
+ /**
7772
+ * Build geocoding requests from parsed CSV rows. The address for each row is
7773
+ * the selected columns trimmed and joined with ", " (so multi-part addresses
7774
+ * like street/city/country can be combined). Rows whose composed address is
7775
+ * empty are skipped.
7776
+ */
7777
+ function csvRowsToGeocodeRequests(rows, addressColumns) {
7778
+ const requests = [];
7779
+ rows.forEach((row, index) => {
7780
+ const address = addressColumns.map((column) => (row[column] ?? "").trim()).filter(Boolean).join(", ").trim();
7781
+ if (!address) return;
7782
+ requests.push({
7783
+ index,
7784
+ address,
7785
+ row
7786
+ });
7787
+ });
7788
+ return requests;
7789
+ }
7790
+ /**
7791
+ * The fetch implementation used by {@link geocodeForward}/{@link geocodeReverse}.
7792
+ * Null means "use the global browser `fetch`" (the default for the web and
7793
+ * embedded builds).
7794
+ */
7795
+ let geocodingFetch = null;
7796
+ /**
7797
+ * Override the fetch used for geocoding requests, or pass null to restore the
7798
+ * global browser `fetch`.
7799
+ *
7800
+ * The desktop shell sets this to Tauri's native HTTP fetch so geocoding requests
7801
+ * are made from the Rust side, which:
7802
+ * - bypasses the WebView's CORS enforcement. Public Nominatim's CDN
7803
+ * intermittently omits the `Access-Control-Allow-Origin` header on cached
7804
+ * responses, which the browser then rejects — surfacing to the user as
7805
+ * "Search failed. Try again." (the symptom that failed Microsoft Store
7806
+ * certification); and
7807
+ * - can send a `User-Agent` identifying the app, as Nominatim's usage policy
7808
+ * requires (browser fetch cannot set that header).
7809
+ *
7810
+ * Safe to call multiple times; only the most recent implementation is used.
7811
+ */
7812
+ function setGeocodingFetch(fetchImpl) {
7813
+ geocodingFetch = fetchImpl;
7814
+ }
7815
+ let geocodingTokenProvider = null;
7816
+ /**
7817
+ * Register how a secured locator gets its token, or pass null to remove it.
7818
+ *
7819
+ * Core has no sign-in of its own: the app installs a provider that reuses its
7820
+ * ArcGIS portal sessions. It is only consulted for a locator that names a
7821
+ * `portalUrl`, and the provider decides whether that portal's token may be sent
7822
+ * to the locator's host, so a token is never forwarded to an unrelated server.
7823
+ *
7824
+ * @param provider - The token provider, or null.
7825
+ */
7826
+ function setGeocodingTokenProvider(provider) {
7827
+ geocodingTokenProvider = provider;
7828
+ }
7829
+ /** The token to send with a request for `config`, when its locator is secured. */
7830
+ async function locatorToken(config) {
7831
+ const locator = config.providerId === "arcgis-locator" ? config.locator : void 0;
7832
+ if (!locator?.portalUrl || !geocodingTokenProvider) return void 0;
7833
+ return geocodingTokenProvider(locator);
7834
+ }
7835
+ /**
7836
+ * `url` with the locator's token added, when there is one. The token is only
7837
+ * attached to a request under the locator's own GeocodeServer URL, whatever
7838
+ * endpoint the config carries.
7839
+ *
7840
+ * @returns The URL, and whether a token was attached (such a request must not
7841
+ * follow redirects, which could replay the token to another host).
7842
+ */
7843
+ async function withLocatorToken(url, config) {
7844
+ const token = await locatorToken(config);
7845
+ if (!token) return {
7846
+ url,
7847
+ token: false
7848
+ };
7849
+ const withToken = new URL(url);
7850
+ const base = `${config.locator?.url}/`;
7851
+ if (!`${withToken.origin}${withToken.pathname}`.startsWith(base)) throw new GeocoderServiceError("The geocoding request does not go to the locator's own server, so its sign-in is not sent.");
7852
+ withToken.searchParams.set("token", token);
7853
+ return {
7854
+ url: withToken.toString(),
7855
+ token: true
7856
+ };
7857
+ }
7858
+ /**
7859
+ * Whether `config` can reverse-geocode: a locator only when its metadata
7860
+ * advertises `reverseGeocode` (unchecked locators are assumed to).
7861
+ *
7862
+ * @param config - The geocoder configuration.
7863
+ * @returns Whether reverse geocoding is available.
7864
+ */
7865
+ function geocoderSupportsReverse(config) {
7866
+ const provider = getGeocodingProvider(config.providerId);
7867
+ if (!provider.reverse) return false;
7868
+ return provider.id !== "arcgis-locator" || config.locator?.reverse !== false;
7869
+ }
7870
+ /**
7871
+ * Whether `config` offers suggestions while typing: a locator whose metadata
7872
+ * advertises `suggest`, searched through its single-line field (a suggestion
7873
+ * is a whole address, so it does not fit a structured field the user chose).
7874
+ *
7875
+ * @param config - The geocoder configuration.
7876
+ * @returns Whether {@link geocodeSuggest} can be used.
7877
+ */
7878
+ function geocoderSupportsSuggest(config) {
7879
+ const locator = config.providerId === "arcgis-locator" ? config.locator : void 0;
7880
+ if (!locator?.suggest || !locator.singleLineField) return false;
7881
+ return arcgisLocatorInputField(locator) === locator.singleLineField;
7882
+ }
7883
+ /**
7884
+ * Normalize an ArcGIS `suggest` response. Collection suggestions (a category
7885
+ * such as "Coffee shop") are dropped: they name many places, not one.
7886
+ *
7887
+ * @param data - The parsed response body.
7888
+ * @returns The suggestions.
7889
+ */
7890
+ function parseArcGISSuggestions(data) {
7891
+ const error = arcgisServiceError(data);
7892
+ if (error) throw error;
7893
+ const suggestions = data && typeof data === "object" ? data.suggestions : void 0;
7894
+ if (!Array.isArray(suggestions)) return [];
7895
+ return suggestions.flatMap((entry) => {
7896
+ const text = readString(entry, "text")?.trim();
7897
+ const magicKey = readString(entry, "magicKey");
7898
+ const collection = entry?.isCollection === true;
7899
+ return text && magicKey && !collection ? [{
7900
+ text,
7901
+ magicKey
7902
+ }] : [];
7903
+ });
7904
+ }
7905
+ /**
7906
+ * Ask the selected locator for suggestions that complete `text`. Resolve one
7907
+ * with {@link geocodeForward} and its `magicKey`.
7908
+ *
7909
+ * @param text - What the user has typed so far.
7910
+ * @param options - Abort signal, configuration and the most suggestions wanted.
7911
+ * @returns The suggestions.
7912
+ * @throws GeocoderUnsupportedError When the geocoder does not suggest.
7913
+ * @throws GeocoderServiceError For an HTTP or service error.
7914
+ */
7915
+ async function geocodeSuggest(text, options = {}) {
7916
+ const config = options.config ?? getGeocoderConfig();
7917
+ if (!geocoderSupportsSuggest(config)) throw new GeocoderUnsupportedError("The selected geocoder does not offer suggestions.");
7918
+ const url = new URL(`${requireLocator(config).url}/suggest`);
7919
+ url.searchParams.set("text", text);
7920
+ url.searchParams.set("f", "json");
7921
+ if (options.limit) url.searchParams.set("maxSuggestions", String(options.limit));
7922
+ const request = await withLocatorToken(url.toString(), config);
7923
+ const response = await geocodeFetch()(request.url, {
7924
+ signal: options.signal,
7925
+ headers: { Accept: "application/json" },
7926
+ ...request.token ? { redirect: "error" } : {}
7927
+ });
7928
+ if (!response.ok) throw new GeocoderServiceError(`Geocoder returned HTTP ${response.status}`, response.status);
7929
+ return parseArcGISSuggestions(await readGeocodeJson(response));
7930
+ }
7931
+ /**
7932
+ * Fetch and read a GeocodeServer's capabilities. A secured service is asked
7933
+ * with `token`.
7934
+ *
7935
+ * @param url - The locator's GeocodeServer URL (normalized first).
7936
+ * @param options - An abort signal and an optional token.
7937
+ * @returns The capabilities.
7938
+ * @throws GeocoderServiceError For an HTTP or service error.
7939
+ */
7940
+ async function fetchArcGISLocatorInfo(url, options = {}) {
7941
+ const base = normalizeArcGISLocatorUrl(url);
7942
+ if (!base) throw new GeocoderServiceError("Not an ArcGIS GeocodeServer URL.");
7943
+ const request = new URL(base);
7944
+ request.searchParams.set("f", "json");
7945
+ if (options.token) request.searchParams.set("token", options.token);
7946
+ const response = await geocodeFetch()(request.toString(), {
7947
+ signal: options.signal,
7948
+ headers: { Accept: "application/json" },
7949
+ ...options.token ? { redirect: "error" } : {}
7950
+ });
7951
+ if (!response.ok) throw new GeocoderServiceError(`Geocoder returned HTTP ${response.status}`, response.status);
7952
+ return parseArcGISLocatorInfo(await readGeocodeJson(response));
7953
+ }
7954
+ /** The active geocoding fetch: the injected override, else the global fetch. */
7955
+ function geocodeFetch() {
7956
+ return geocodingFetch ?? fetch;
7957
+ }
7958
+ /**
7959
+ * Read a geocoder response body as JSON, treating an empty one as "no results".
7960
+ *
7961
+ * CartoCiudad answers an unmatched query with HTTP 204 and an empty body rather
7962
+ * than a JSON null (verified against the live `find` and `reverseGeocode`
7963
+ * endpoints). `response.ok` is true for 204, so parsing unconditionally would
7964
+ * throw a JSON syntax error and surface a nonsense address as "Search failed"
7965
+ * instead of "no match". Returning undefined lets each provider's parser report
7966
+ * no results the way it already does for a null body.
7967
+ */
7968
+ async function readGeocodeJson(response) {
7969
+ if (response.status === 204) return void 0;
7970
+ const text = await response.text();
7971
+ if (!text.trim()) return void 0;
7972
+ return JSON.parse(text);
7973
+ }
7974
+ /**
7975
+ * Normalize a longitude into [-180, 180], or null when it is not finite.
7976
+ *
7977
+ * MapLibre reports `lngLat.lng` unwrapped once the map is zoomed out far enough
7978
+ * to show repeated world copies, so a click on a non-primary copy arrives as
7979
+ * e.g. 210 rather than -150. Rejecting those outright would silently drop a
7980
+ * legitimate reverse-geocode click for every provider, so wrap instead.
7981
+ *
7982
+ * An in-range longitude returns untouched: the modulo round-trip is not exact
7983
+ * in floating point (-3.7 comes back as -3.7000000000000455), and the common
7984
+ * case should not be perturbed to handle the rare one.
7985
+ */
7986
+ function wrapLongitude(lon) {
7987
+ if (!Number.isFinite(lon)) return null;
7988
+ if (lon >= -180 && lon <= 180) return lon;
7989
+ return ((lon + 180) % 360 + 360) % 360 - 180;
7990
+ }
7991
+ /**
7992
+ * Forward-geocode a single query through the configured provider, returning
7993
+ * normalized matches.
7994
+ */
7995
+ async function geocodeForward(query, options = {}) {
7996
+ const config = options.config ?? getGeocoderConfig();
7997
+ const provider = getGeocodingProvider(config.providerId);
7998
+ const request = await withLocatorToken(provider.buildForwardUrl(config, query, {
7999
+ email: config.email,
8000
+ limit: options.limit,
8001
+ magicKey: options.magicKey,
8002
+ attributes: options.attributes
8003
+ }), config);
8004
+ const response = await geocodeFetch()(request.url, {
8005
+ signal: options.signal,
8006
+ headers: { Accept: "application/json" },
8007
+ ...request.token ? { redirect: "error" } : {}
8008
+ });
8009
+ if (!response.ok) throw new GeocoderServiceError(`Geocoder returned HTTP ${response.status}`, response.status);
8010
+ const data = await readGeocodeJson(response);
8011
+ return provider.parseForward(data);
8012
+ }
8013
+ /**
8014
+ * Reverse-geocode a single point ([lon, lat]) through the configured provider,
8015
+ * returning a normalized display result or null when no address was found.
8016
+ */
8017
+ async function geocodeReverse(lon, lat, options = {}) {
8018
+ const wrappedLon = wrapLongitude(lon);
8019
+ if (wrappedLon === null || !Number.isFinite(lat) || lat < -90 || lat > 90) return null;
8020
+ const config = options.config ?? getGeocoderConfig();
8021
+ const provider = getGeocodingProvider(config.providerId);
8022
+ if (!geocoderSupportsReverse(config)) throw new GeocoderUnsupportedError("The selected geocoder does not support reverse geocoding.");
8023
+ const request = await withLocatorToken(provider.buildReverseUrl(config, wrappedLon, lat, {
8024
+ email: config.email,
8025
+ zoom: options.zoom
8026
+ }), config);
8027
+ const response = await geocodeFetch()(request.url, {
8028
+ signal: options.signal,
8029
+ headers: { Accept: "application/json" },
8030
+ ...request.token ? { redirect: "error" } : {}
8031
+ });
8032
+ if (!response.ok) throw new GeocoderServiceError(`Geocoder returned HTTP ${response.status}`, response.status);
8033
+ const data = await readGeocodeJson(response);
8034
+ return provider.parseReverse(data);
8035
+ }
8036
+ //#endregion
5790
8037
  //#region src/project.ts
5791
8038
  /** Placeholder name a project carries before the user names it. */
5792
8039
  const DEFAULT_PROJECT_NAME = "Untitled Project";
@@ -6276,6 +8523,7 @@ function normalizeStoryChapter(chapter) {
6276
8523
  },
6277
8524
  mapAnimation: STORY_ANIMATIONS.has(candidate.mapAnimation) ? candidate.mapAnimation : "flyTo",
6278
8525
  rotateAnimation: normalizeBoolean(candidate.rotateAnimation, false),
8526
+ ...typeof candidate.durationSeconds === "number" && Number.isFinite(candidate.durationSeconds) ? { durationSeconds: normalizeStoryChapterDurationSeconds(candidate.durationSeconds) } : {},
6279
8527
  onChapterEnter: normalizeOpacityChanges(candidate.onChapterEnter),
6280
8528
  onChapterExit: normalizeOpacityChanges(candidate.onChapterExit)
6281
8529
  };
@@ -6891,6 +9139,7 @@ function normalizeProjectPreferences(preferences) {
6891
9139
  mapboxStyleUrl: normalizeString(map.mapboxStyleUrl) || void 0,
6892
9140
  arcgisBasemap: normalizeString(map.arcgisBasemap) || void 0,
6893
9141
  arcgisWkid: normalizeArcgisWkid(map.arcgisWkid),
9142
+ mapCrs: normalizeMapCrs(map.mapCrs),
6894
9143
  cesiumBasemap: normalizeCesiumBasemap(map.cesiumBasemap),
6895
9144
  terrainEnabled: normalizeBoolean(map.terrainEnabled, DEFAULT_PROJECT_PREFERENCES.map.terrainEnabled),
6896
9145
  terrainIonAssetId: parseCesiumIonAssetId(map.terrainIonAssetId) ?? void 0,
@@ -6930,9 +9179,29 @@ function normalizeGeocodingPreferences(geocoding) {
6930
9179
  apiKeys,
6931
9180
  forwardEndpoint: typeof candidate.forwardEndpoint === "string" && candidate.forwardEndpoint.trim() ? candidate.forwardEndpoint.trim() : void 0,
6932
9181
  reverseEndpoint: typeof candidate.reverseEndpoint === "string" && candidate.reverseEndpoint.trim() ? candidate.reverseEndpoint.trim() : void 0,
6933
- email: typeof candidate.email === "string" && candidate.email.trim() ? candidate.email.trim() : void 0
9182
+ email: typeof candidate.email === "string" && candidate.email.trim() ? candidate.email.trim() : void 0,
9183
+ ...normalizeGeocodingLocators(candidate.locators, candidate.locatorId)
6934
9184
  };
6935
9185
  }
9186
+ /**
9187
+ * The project's ArcGIS GeocodeServer locators, dropping malformed entries and
9188
+ * duplicate ids, plus the selected locator id when it still names one. Both are
9189
+ * omitted when there are none, so older projects round-trip unchanged.
9190
+ */
9191
+ function normalizeGeocodingLocators(locators, locatorId) {
9192
+ if (!Array.isArray(locators)) return {};
9193
+ const seen = /* @__PURE__ */ new Set();
9194
+ const result = locators.map(normalizeGeocodingLocator).filter((locator) => {
9195
+ if (!locator || seen.has(locator.id)) return false;
9196
+ seen.add(locator.id);
9197
+ return true;
9198
+ });
9199
+ if (!result.length) return {};
9200
+ return typeof locatorId === "string" && seen.has(locatorId) ? {
9201
+ locators: result,
9202
+ locatorId
9203
+ } : { locators: result };
9204
+ }
6936
9205
  /** Coerce an unknown value to a supported scale unit, defaulting to metric. */
6937
9206
  function normalizeScaleUnit(value) {
6938
9207
  return value === "imperial" || value === "nautical" ? value : "metric";
@@ -7073,6 +9342,11 @@ function withoutZarrHeaders(layer) {
7073
9342
  source
7074
9343
  };
7075
9344
  }
9345
+ function withoutRuntimeGeoArrow(layer) {
9346
+ if (layer.geoarrow === void 0) return layer;
9347
+ const { geoarrow: _geoarrow, ...rest } = layer;
9348
+ return rest;
9349
+ }
7076
9350
  /**
7077
9351
  * Strip the session-only state every serializer must leave out, whether or not
7078
9352
  * the project came through `projectFromStore`.
@@ -7081,11 +9355,11 @@ function withoutZarrHeaders(layer) {
7081
9355
  * @returns The layer as it may be written to a project file.
7082
9356
  */
7083
9357
  function portableLayer(layer) {
7084
- return withoutZarrHeaders(withoutLocalRasterBytes(layer));
9358
+ return withoutRuntimeGeoArrow(withoutZarrHeaders(withoutLocalRasterBytes(layer)));
7085
9359
  }
7086
9360
  function normalizeLayer(layer) {
7087
- layer = withoutLocalRasterBytes(layer);
7088
- const { capabilities: rawCapabilities, filterExpression: rawFilterExpression, descriptiveMetadata: rawDescriptiveMetadata, ...rest } = layer;
9361
+ layer = withRasterSourceDefaults(withoutLocalRasterBytes(layer));
9362
+ const { capabilities: rawCapabilities, filterExpression: rawFilterExpression, descriptiveMetadata: rawDescriptiveMetadata, geoarrow: _rawGeoarrow, ...rest } = layer;
7089
9363
  const capabilities = normalizeLayerCapabilities(rawCapabilities);
7090
9364
  const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);
7091
9365
  const filterExpression = Array.isArray(rawFilterExpression) && rawFilterExpression.length > 0 && validateMapExpression(JSON.stringify(rawFilterExpression), { expectedType: "boolean" }).ok ? rawFilterExpression : void 0;
@@ -7387,6 +9661,7 @@ function portableWmsTileUrl(tile) {
7387
9661
  * @returns A new layer record ready for the store.
7388
9662
  */
7389
9663
  function hydrateProjectLayer(project, layer) {
9664
+ layer = withRasterSourceDefaults(layer);
7390
9665
  const topLevel = project.styles?.[layer.id];
7391
9666
  return {
7392
9667
  ...layer,
@@ -8135,6 +10410,6 @@ function metadataRank(path) {
8135
10410
  return 2;
8136
10411
  }
8137
10412
  //#endregion
8138
- export { isHeaderReferenceOnly as $, geoJSONToPolylineStr as $n, MAX_DASHBOARD_COLUMNS as $r, layerGroupSortability as $t, normalizeSecondaryMapViews as A, metadataDateToRfc3339 as An, getActiveEllipsoid as Ar, normalizeStyleLibraryEntries as At, storyMapHasContent as B, isStyleSpecColor as Bn, DEFAULT_BASEMAP as Br, scrubPrintLayoutForRemovedLayers as Bt, normalizeModelGraph as C, stripVirtualFieldColumns as Cn, PLANETARY_BASEMAP_GROUPS as Cr, RAMP_STYLE_KEYS as Ct, normalizeProcessingHistory as D, isValidMetadataDate as Dn, earthAreaToBody as Dr, createStyleLibraryEntryId as Dt, normalizePrimaryRenderer as E, buildLayerStacItem as En, bodyLengthToEarth as Er, SYMBOL_STYLE_KEYS as Et, projectFromStore as F, EXPRESSION_FUNCTION_CATEGORIES as Fn, getPlanetaryBasemapByStyleUrl as Fr, createDefaultPrintLayout as Ft, isCredentialUrlParam as G, SELECTION_MODES as Gn, DEFAULT_MAP_GRID_LAYOUT as Gr, DEFAULT_LAYER_GROUP_OPACITY as Gt, PROJECT_CREDENTIAL_FIELDS as H, removeTrailingJsonCommas as Hn, DEFAULT_DASHBOARD_COLUMNS as Hr, scrubCommentsForRemovedLayers as Ht, resolveMapGrid as I, compileFeatureExpression as In, meanRadiusMeters as Ir, isDefaultPrintLayout as It, redactProjectCredentials as J, invertSelection as Jn, DEFAULT_STORY_MAP as Jr, buildLayerTree as Jt, redactConfigurationCredentials as K, applySelectionMode as Kn, DEFAULT_PRIMARY_RENDERER as Kr, applyGroupEffects as Kt, serializeProject as L, evaluateMapExpression as Ln, setActiveEllipsoidId as Lr, normalizePrintLayoutConfig as Lt, normalizeWidgets as M, parseMetadataKeywords as Mn, getActiveSemiMajorAxisMeters as Mr, sanitizeLayerStylePatch as Mt, parseProject as N, stacAssetMediaType as Nn, getEllipsoid as Nr, serializeStyleLibrary as Nt, normalizeProjectComments as O, isValidMetadataEmail as On, earthLengthToBody as Or, extractStyleLibraryStyle as Ot, portableWmsTileUrl as P, validateLayerDescriptiveMetadata as Pn, getPlanetaryBasemapById as Pr, DEFAULT_PRINT_LAYOUT as Pt, hasHeaderReferences as Q, encodePolyline as Qn, LAYER_TYPES as Qr, layerGroupMoveability as Qt, serializeProjectWithLayerCache as R, formatExpressionPreviewValue as Rn, BLANK_BASEMAP as Rr, printLayoutConfigsEqual as Rt, normalizeMapViewState as S, applyLayerVirtualFields as Sn, PLANETARY_BASEMAPS as Sr, BUILT_IN_STYLE_PRESETS as St, normalizeObiaWorkbench as T, STAC_VERSION as Tn, PLANET_SWITCHER_OPTIONS as Tr, STYLE_LIBRARY_BUNDLE_VERSION as Tt, PUBLISHABLE_PLUGIN_SETTINGS as U, substituteExpressionVariables as Un, DEFAULT_LAYER_STYLE as Ur, scrubLegendForRemovedLayers as Ut, MAX_REDACT_DEPTH as V, matchFeaturesByExpression as Vn, DEFAULT_BLEND_MODE as Vr, removedLayerIdSet as Vt, isCredentialFieldName as W, validateMapExpression as Wn, DEFAULT_LEGEND_CONFIG as Wr, scrubWidgetsForRemovedLayers as Wt, setRegistryPublishableSettings as X, decodePolyline as Xn, DUCKDB_VECTOR_ROUTE_BYTES as Xr, groupMemberIndices as Xt, redactUrlCredentials as Y, batchDecodePolylines as Yn, DUCKDB_VECTOR_FEATURE_WARN_COUNT as Yr, effectiveLayerRenderState as Yt, allowsCredentialHeaders as Z, decodePolylineDetailed as Zn, LARGE_VECTOR_FEATURE_THRESHOLD as Zr, layerGroupDepth as Zt, normalizeBookmarkGroups as _, cascadeLayerJoinRefresh as _n, getRegionalBasemapByStyleUrl as _r, CZML_SOURCE_KIND as _t, DEFAULT_PROJECT_NAME as a, PROJECT_VERSION as ai, ROLE_PRIVILEGES as an, ARCGIS_PROJECTION_PRESETS as ar, CESIUM_OSM_BUILDINGS_ASSET_ID as at, normalizeLegendConfig as b, reapplyLayerJoins as bn, EARTH_MEAN_RADIUS_METERS as br, isCzmlLayer as bt, createDefaultMapView as c, detectNonGeographicCoordinates as ci, hasAppPrivilege as cn, availableCesiumBasemap as cr, createCesiumIonLayer as ct, createProjectTemplate as d, styleValue as di, normalizeAppPrivileges as dn, sameCesiumImagery as dr, parseCesiumIonAssetId as dt, MAX_MAP_GRID_DIM as ei, layerPanelGroupHeaders as en, polylineStrToGeoJSON as er, resolveHeaderReferences as et, detachProjectCopy as f, normalizeLayerCapabilities as fn, CHINA_BASEMAPS as fr, CESIUM_KML_SOURCE_KIND as ft, normalizeBlankBackgroundColor as g, applyLayerJoins as gn, getRegionalBasemapById as gr, CZML_QUICK_PICKS as gt, isSessionOnlyLayer as h, applyJoinsToLayer as hn, REGIONAL_BASEMAP_SENTINEL_PREFIX as hr, isCesiumKmlLayer as ht, previewValue as i, OPENFREEMAP_BASEMAPS as ii, ALL_APP_PRIVILEGES as in, transformGeojsonElevation as ir, CESIUM_ION_SOURCE_KIND as it, normalizeStoryMap as j, normalizeLayerDescriptiveMetadata as jn, getActiveMeanRadiusMeters as jr, parseStyleLibrary as jt, normalizeProjectInteraction as k, isValidMetadataUrl as kn, getActiveBodyRadiusRatio as kr, isStyleLibraryTargetLayer as kt, createEmptyProject as l, isDuckDBQueryLayer as li, inferLayerCapabilities as ln, basemapToCesiumImagery as lr, isCesiumIonLayer as lt, isAllowedPluginManifestUrl as m, resolveRolePrivileges as mn, REGIONAL_BASEMAP_GROUPS as mr, createCesiumKmlLayer as mt, diffProjects as n, MIN_DASHBOARD_COLUMNS as ni, reorderLayerGroupInPanel as nn, geojsonHasZCoordinates as nr, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID as nt, MAX_PROJECT_BOOKMARKS as o, PROTOMAPS_BASEMAPS as oi, appPrivilegeReason as on, normalizeArcgisWkid as or, cesiumIonAssetId as ot, hydrateProjectLayer as p, resolveLayerCapabilities as pn, REGIONAL_BASEMAPS as pr, cesiumKmlSource as pt, redactCredentials as q, featureSelectionId as qn, DEFAULT_PROJECT_PREFERENCES as qr, buildLayerPanelUnits as qt, jsonValuesEqual as r, NETCDF_IMAGE_SOURCE_KIND as ri, sortLayerGroupInPanel as rn, horizontalBbox as rr, CESIUM_ION_QUICK_PICKS as rt, applyProjectToStore as s, SQL_QUERY_SOURCE_KIND as si, createDefaultAppCapabilities as sn, CESIUM_BASEMAPS as sr, cesiumIonAssetKind as st, diffFeatureCollections as t, MAX_PROCESSING_HISTORY as ti, normalizeGroupContiguity as tn, unescapePolyline as tr, CESIUM_BING_AERIAL_ASSET_ID as tt, createProjectLayerSerializationCache as u, shouldUseTiledRendering as ui, intersectPrivileges as un, normalizeCesiumBasemap as ur, isCesiumOnlyLayer as ut, normalizeBookmarks as v, collectTransitiveJoinSourceIds as vn, isRegionalBasemapSentinel as vr, createCzmlLayer as vt, normalizeModels as w, STAC_PROCESSING_EXTENSION as wn, PLANETARY_BASEMAP_SENTINEL_PREFIX as wr, STYLE_LIBRARY_BUNDLE_TYPE as wt, normalizeMapLayout as x, stripJoinFields as xn, ELLIPSOIDS as xr, parseCzml as xt, normalizeDashboardColumns as y, layerJoinKey as yn, DEFAULT_ELLIPSOID_ID as yr, czmlSource as yt, serializeProjectWithLayerCacheAsync as z, inferFieldTypes as zn, BLEND_MODES as zr, scrubPrintLayoutForLayers as zt };
10413
+ export { GEOCODING_PROVIDERS as $, getPlanetaryBasemapByStyleUrl as $i, groupMemberIndices as $n, getBuildEnvironment as $r, CESIUM_BING_AERIAL_ASSET_ID as $t, normalizeSecondaryMapViews as A, CHINA_BASEMAPS as Ai, createStyleLibraryEntryId as An, isValidMetadataDate as Ar, selectedGeocodingLocator as At, storyMapHasContent as B, PLANETARY_BASEMAPS as Bi, normalizePrintLayoutConfig as Bn, evaluateMapExpression as Br, PROJECT_CREDENTIAL_FIELDS as Bt, normalizeModelGraph as C, PROJECT_VERSION as Ca, ARCGIS_PROJECTION_PRESETS as Ci, isGeoArrowBatch as Cn, reapplyLayerJoins as Cr, normalizeArcGISLocatorUrl as Ct, normalizeProcessingHistory as D, isDuckDBQueryLayer as Da, basemapToCesiumImagery as Di, STYLE_LIBRARY_BUNDLE_TYPE as Dn, STAC_PROCESSING_EXTENSION as Dr, parseArcGISSuggestions as Dt, normalizePrimaryRenderer as E, detectNonGeographicCoordinates as Ea, availableCesiumBasemap as Ei, RAMP_STYLE_KEYS as En, stripVirtualFieldColumns as Er, parseArcGISLocatorInfo as Et, projectFromStore as F, getRegionalBasemapByStyleUrl as Fi, sanitizeLayerStylePatch as Fn, parseMetadataKeywords as Fr, DEFAULT_STORY_CHAPTER_DURATION_SECONDS as Ft, DEFAULT_REVERSE_GEOCODE_ENDPOINT as G, earthAreaToBody as Gi, scrubCommentsForRemovedLayers as Gn, removeTrailingJsonCommas as Gr, redactCredentials as Gt, CARTOCIUDAD_PUBLIC_HOST as H, PLANETARY_BASEMAP_SENTINEL_PREFIX as Hi, scrubPrintLayoutForLayers as Hn, inferFieldTypes as Hr, isCredentialFieldName as Ht, resolveMapGrid as I, isRegionalBasemapSentinel as Ii, serializeStyleLibrary as In, stacAssetMediaType as Ir, MAX_STORY_CHAPTER_DURATION_SECONDS as It, GEOCODE_LAT_KEY as J, getActiveEllipsoid as Ji, DEFAULT_LAYER_GROUP_OPACITY as Jn, SELECTION_MODES as Jr, setRegistryPublishableSettings as Jt, GEOCODE_ATTRIBUTE_PREFIX as K, earthLengthToBody as Ki, scrubLegendForRemovedLayers as Kn, substituteExpressionVariables as Kr, redactProjectCredentials as Kt, serializeProject as L, DEFAULT_ELLIPSOID_ID as Li, DEFAULT_PRINT_LAYOUT as Ln, validateLayerDescriptiveMetadata as Lr, MIN_STORY_CHAPTER_DURATION_SECONDS as Lt, normalizeWidgets as M, REGIONAL_BASEMAP_GROUPS as Mi, isStyleLibraryTargetLayer as Mn, isValidMetadataUrl as Mr, setGeocodingTokenProvider as Mt, parseProject as N, REGIONAL_BASEMAP_SENTINEL_PREFIX as Ni, normalizeStyleLibraryEntries as Nn, metadataDateToRfc3339 as Nr, shouldThrottle as Nt, normalizeProjectComments as O, shouldUseTiledRendering as Oa, normalizeCesiumBasemap as Oi, STYLE_LIBRARY_BUNDLE_VERSION as On, STAC_VERSION as Or, resolveGeocoderConfig as Ot, portableWmsTileUrl as P, getRegionalBasemapById as Pi, parseStyleLibrary as Pn, normalizeLayerDescriptiveMetadata as Pr, unmatchedGeocodeFeature as Pt, GEOCODE_STATUS_KEY as Q, getPlanetaryBasemapById as Qi, effectiveLayerRenderState as Qn, getArcgisApiKey as Qr, resolveHeaderReferences as Qt, serializeProjectWithLayerCache as R, EARTH_MEAN_RADIUS_METERS as Ri, createDefaultPrintLayout as Rn, EXPRESSION_FUNCTION_CATEGORIES as Rr, normalizeStoryChapterDurationSeconds as Rt, normalizeMapViewState as S, OPENFREEMAP_BASEMAPS as Sa, parseMapCrsEpsgCode as Si, geoArrowToFeatureCollection as Sn, layerJoinKey as Sr, nominatimReverseResultToDisplay as St, normalizeObiaWorkbench as T, SQL_QUERY_SOURCE_KIND as Ta, CESIUM_BASEMAPS as Ti, BUILT_IN_STYLE_PRESETS as Tn, applyLayerVirtualFields as Tr, normalizeGeocodingProviderId as Tt, DEFAULT_FORWARD_GEOCODE_ENDPOINT as U, PLANET_SWITCHER_OPTIONS as Ui, scrubPrintLayoutForRemovedLayers as Un, isStyleSpecColor as Ur, isCredentialUrlParam as Ut, CARTOCIUDAD_MIN_INTERVAL_MS as V, PLANETARY_BASEMAP_GROUPS as Vi, printLayoutConfigsEqual as Vn, formatExpressionPreviewValue as Vr, PUBLISHABLE_PLUGIN_SETTINGS as Vt, DEFAULT_GEOCODING_PROVIDER_ID as W, bodyLengthToEarth as Wi, removedLayerIdSet as Wn, matchFeaturesByExpression as Wr, redactConfigurationCredentials as Wt, GEOCODE_PROVIDER_KEY as X, getActiveSemiMajorAxisMeters as Xi, buildLayerPanelUnits as Xn, featureSelectionId as Xr, hasHeaderReferences as Xt, GEOCODE_LON_KEY as Y, getActiveMeanRadiusMeters as Yi, applyGroupEffects as Yn, applySelectionMode as Yr, allowsCredentialHeaders as Yt, GEOCODE_SCORE_KEY as Z, getEllipsoid as Zi, buildLayerTree as Zn, invertSelection as Zr, isHeaderReferenceOnly as Zt, normalizeBookmarkGroups as _, MAX_DASHBOARD_COLUMNS as _a, MAP_CRS_PRESETS as _i, czmlSource as _n, resolveRolePrivileges as _r, geocoderSupportsSuggest as _t, DEFAULT_PROJECT_NAME as a, DEFAULT_BLEND_MODE as aa, getRuntimeEnvironment as ai, cesiumIonAssetKind as an, reorderLayerGroupInPanel as ar, arcgisLocatorInputField as at, normalizeLegendConfig as b, MIN_DASHBOARD_COLUMNS as ba, getMapCrsPreset as bi, computeGeoArrowBBox as bn, cascadeLayerJoinRefresh as br, nextDelayMs as bt, createDefaultMapView as c, DEFAULT_LEGEND_CONFIG as ca, decodePolyline as ci, isCesiumOnlyLayer as cn, ROLE_PRIVILEGES as cr, csvRowsToGeocodeRequests as ct, createProjectTemplate as d, DEFAULT_PROJECT_PREFERENCES as da, geoJSONToPolylineStr as di, cesiumKmlSource as dn, hasAppPrivilege as dr, geocodeMatchToFeature as dt, meanRadiusMeters as ea, getCesiumIonToken as ei, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID as en, layerGroupDepth as er, GeocoderServiceError as et, detachProjectCopy as f, DEFAULT_STORY_MAP as fa, polylineStrToGeoJSON as fi, createCesiumKmlLayer as fn, inferLayerCapabilities as fr, geocodeReverse as ft, normalizeBlankBackgroundColor as g, LAYER_TYPES as ga, transformGeojsonElevation as gi, createCzmlLayer as gn, resolveLayerCapabilities as gr, geocoderSupportsReverse as gt, isSessionOnlyLayer as h, LARGE_VECTOR_FEATURE_THRESHOLD as ha, horizontalBbox as hi, CZML_SOURCE_KIND as hn, normalizeLayerCapabilities as hr, geocoderNeedsApiKey as ht, previewValue as i, DEFAULT_BASEMAP as ia, getProtomapsStyleUrl as ii, cesiumIonAssetId as in, normalizeGroupContiguity as ir, PUBLIC_GEOCODE_ROW_CAP as it, normalizeStoryMap as j, REGIONAL_BASEMAPS as ji, extractStyleLibraryStyle as jn, isValidMetadataEmail as jr, setGeocodingFetch as jt, normalizeProjectInteraction as k, styleValue as ka, sameCesiumImagery as ki, SYMBOL_STYLE_KEYS as kn, buildLayerStacItem as kr, rowCap as kt, createEmptyProject as l, DEFAULT_MAP_GRID_LAYOUT as la, decodePolylineDetailed as li, parseCesiumIonAssetId as ln, appPrivilegeReason as lr, fetchArcGISLocatorInfo as lt, isAllowedPluginManifestUrl as m, DUCKDB_VECTOR_ROUTE_BYTES as ma, geojsonHasZCoordinates as mi, CZML_QUICK_PICKS as mn, normalizeAppPrivileges as mr, geocoderMinIntervalMs as mt, diffProjects as n, BLANK_BASEMAP as na, getMapboxAccessToken as ni, CESIUM_ION_SOURCE_KIND as nn, layerGroupSortability as nr, NOMINATIM_MIN_INTERVAL_MS as nt, MAX_PROJECT_BOOKMARKS as o, DEFAULT_DASHBOARD_COLUMNS as oa, getSpatialExtensionPath as oi, createCesiumIonLayer as on, sortLayerGroupInPanel as or, buildForwardGeocodeUrl as ot, hydrateProjectLayer as p, DUCKDB_VECTOR_FEATURE_WARN_COUNT as pa, unescapePolyline as pi, isCesiumKmlLayer as pn, intersectPrivileges as pr, geocodeSuggest as pt, GEOCODE_DISPLAY_NAME_KEY as q, getActiveBodyRadiusRatio as qi, scrubWidgetsForRemovedLayers as qn, validateMapExpression as qr, redactUrlCredentials as qt, jsonValuesEqual as r, BLEND_MODES as ra, getProtomapsApiKey as ri, CESIUM_OSM_BUILDINGS_ASSET_ID as rn, layerPanelGroupHeaders as rr, NOMINATIM_PUBLIC_HOST as rt, applyProjectToStore as s, DEFAULT_LAYER_STYLE as sa, batchDecodePolylines as si, isCesiumIonLayer as sn, ALL_APP_PRIVILEGES as sr, buildReverseGeocodeUrl as st, diffFeatureCollections as t, setActiveEllipsoidId as ta, getGoogleMapsApiKey as ti, CESIUM_ION_QUICK_PICKS as tn, layerGroupMoveability as tr, GeocoderUnsupportedError as tt, createProjectLayerSerializationCache as u, DEFAULT_PRIMARY_RENDERER as ua, encodePolyline as ui, CESIUM_KML_SOURCE_KIND as un, createDefaultAppCapabilities as ur, geocodeForward as ut, normalizeBookmarks as v, MAX_MAP_GRID_DIM as va, adaptBboxTemplateToCrs as vi, isCzmlLayer as vn, applyJoinsToLayer as vr, getGeocoderConfig as vt, normalizeModels as w, PROTOMAPS_BASEMAPS as wa, normalizeArcgisWkid as wi, withRasterSourceDefaults as wn, stripJoinFields as wr, normalizeGeocodingLocator as wt, normalizeMapLayout as x, NETCDF_IMAGE_SOURCE_KIND as xa, normalizeMapCrs as xi, featureCollectionToGeoArrow as xn, collectTransitiveJoinSourceIds as xr, nominatimResultToFeature as xt, normalizeDashboardColumns as y, MAX_PROCESSING_HISTORY as ya, deriveTileMatrixSet as yi, parseCzml as yn, applyLayerJoins as yr, getGeocodingProvider as yt, serializeProjectWithLayerCacheAsync as z, ELLIPSOIDS as zi, isDefaultPrintLayout as zn, compileFeatureExpression as zr, MAX_REDACT_DEPTH as zt };
8139
10414
 
8140
- //# sourceMappingURL=project-diff-Bua7fBME.mjs.map
10415
+ //# sourceMappingURL=project-diff-CPb5ZXb_.mjs.map