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