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