@geolibre/core 3.2.0 → 3.3.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 +418 -3139
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1410 -1930
- package/dist/index.mjs.map +1 -1
- package/dist/joins-CGcRoTpc.mjs +1822 -0
- package/dist/joins-CGcRoTpc.mjs.map +1 -0
- package/dist/joins-CRZZcLqX.d.mts +3218 -0
- package/dist/joins-CRZZcLqX.d.mts.map +1 -0
- package/dist/worker-safe.d.mts +2 -0
- package/dist/worker-safe.mjs +2 -0
- package/package.json +6 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as unescapePolyline, B as PLANET_SWITCHER_OPTIONS, C as invertSelection, D as encodePolyline, E as decodePolylineDetailed, F as EARTH_MEAN_RADIUS_METERS, G as getActiveEllipsoid, H as earthAreaToBody, I as ELLIPSOIDS, J as getEllipsoid, K as getActiveMeanRadiusMeters, L as PLANETARY_BASEMAPS, M as horizontalBbox, N as transformGeojsonElevation, O as geoJSONToPolylineStr, P as DEFAULT_ELLIPSOID_ID, Q as setActiveEllipsoidId, R as PLANETARY_BASEMAP_GROUPS, S as featureSelectionId, T as decodePolyline, U as earthLengthToBody, V as bodyLengthToEarth, W as getActiveBodyRadiusRatio, X as getPlanetaryBasemapByStyleUrl, Y as getPlanetaryBasemapById, Z as meanRadiusMeters, _ as removeTrailingJsonCommas, a as layerJoinKey, b as SELECTION_MODES, c as applyLayerVirtualFields, d as compileFeatureExpression, f as evaluateMapExpression, g as matchFeaturesByExpression, h as isStyleSpecColor, i as collectTransitiveJoinSourceIds, j as geojsonHasZCoordinates, k as polylineStrToGeoJSON, l as stripVirtualFieldColumns, m as inferFieldTypes, n as applyLayerJoins, o as reapplyLayerJoins, p as formatExpressionPreviewValue, q as getActiveSemiMajorAxisMeters, r as cascadeLayerJoinRefresh, s as stripJoinFields, t as applyJoinsToLayer, u as EXPRESSION_FUNCTION_CATEGORIES, v as substituteExpressionVariables, w as batchDecodePolylines, x as applySelectionMode, y as validateMapExpression, z as PLANETARY_BASEMAP_SENTINEL_PREFIX } from "./joins-CGcRoTpc.mjs";
|
|
2
|
+
import { Color, featureFilter, latest, normalizePropertyExpression } from "@maplibre/maplibre-gl-style-spec";
|
|
2
3
|
import { create } from "zustand";
|
|
3
4
|
import { temporal } from "zundo";
|
|
4
5
|
import { v4 } from "uuid";
|
|
@@ -345,6 +346,7 @@ const DEFAULT_PROJECT_PREFERENCES = {
|
|
|
345
346
|
showPointerElevation: false,
|
|
346
347
|
terrainEnabled: false,
|
|
347
348
|
coordinateFormat: "dd",
|
|
349
|
+
zoomToNewLayers: true,
|
|
348
350
|
mapboxStyleUrl: "mapbox://styles/mapbox/standard",
|
|
349
351
|
arcgisBasemap: "arcgis/streets",
|
|
350
352
|
cesiumBasemap: "bing-aerial"
|
|
@@ -823,500 +825,6 @@ const PHOTO_PROPERTY = "photo";
|
|
|
823
825
|
*/
|
|
824
826
|
const PHOTO_FULL_PROPERTY = "photo_full";
|
|
825
827
|
//#endregion
|
|
826
|
-
//#region src/ellipsoids.ts
|
|
827
|
-
/**
|
|
828
|
-
* Built-in ellipsoids. Earth is WGS 84; every other body uses IAU-adopted
|
|
829
|
-
* figures. Only Earth and Mars are appreciably oblate; the rest are modelled as
|
|
830
|
-
* spheres (`inverseFlattening: 0`), which is how the IAU/USGS simple-cylindrical
|
|
831
|
-
* products treat them and is well within the accuracy MapLibre's unit-sphere
|
|
832
|
-
* rendering can represent anyway.
|
|
833
|
-
*/
|
|
834
|
-
const ELLIPSOIDS = [
|
|
835
|
-
{
|
|
836
|
-
id: "earth",
|
|
837
|
-
name: "Earth (WGS 84)",
|
|
838
|
-
semiMajorAxisMeters: 6378137,
|
|
839
|
-
inverseFlattening: 298.257223563
|
|
840
|
-
},
|
|
841
|
-
{
|
|
842
|
-
id: "moon",
|
|
843
|
-
name: "Moon",
|
|
844
|
-
semiMajorAxisMeters: 1737400,
|
|
845
|
-
inverseFlattening: 0
|
|
846
|
-
},
|
|
847
|
-
{
|
|
848
|
-
id: "mars",
|
|
849
|
-
name: "Mars (IAU 2000)",
|
|
850
|
-
semiMajorAxisMeters: 3396190,
|
|
851
|
-
inverseFlattening: 169.894447
|
|
852
|
-
},
|
|
853
|
-
{
|
|
854
|
-
id: "mercury",
|
|
855
|
-
name: "Mercury",
|
|
856
|
-
semiMajorAxisMeters: 2439400,
|
|
857
|
-
inverseFlattening: 0
|
|
858
|
-
},
|
|
859
|
-
{
|
|
860
|
-
id: "venus",
|
|
861
|
-
name: "Venus",
|
|
862
|
-
semiMajorAxisMeters: 6051800,
|
|
863
|
-
inverseFlattening: 0
|
|
864
|
-
},
|
|
865
|
-
{
|
|
866
|
-
id: "io",
|
|
867
|
-
name: "Io",
|
|
868
|
-
semiMajorAxisMeters: 1821600,
|
|
869
|
-
inverseFlattening: 0
|
|
870
|
-
},
|
|
871
|
-
{
|
|
872
|
-
id: "europa",
|
|
873
|
-
name: "Europa",
|
|
874
|
-
semiMajorAxisMeters: 1560800,
|
|
875
|
-
inverseFlattening: 0
|
|
876
|
-
},
|
|
877
|
-
{
|
|
878
|
-
id: "ganymede",
|
|
879
|
-
name: "Ganymede",
|
|
880
|
-
semiMajorAxisMeters: 2631200,
|
|
881
|
-
inverseFlattening: 0
|
|
882
|
-
},
|
|
883
|
-
{
|
|
884
|
-
id: "callisto",
|
|
885
|
-
name: "Callisto",
|
|
886
|
-
semiMajorAxisMeters: 2410300,
|
|
887
|
-
inverseFlattening: 0
|
|
888
|
-
},
|
|
889
|
-
{
|
|
890
|
-
id: "titan",
|
|
891
|
-
name: "Titan",
|
|
892
|
-
semiMajorAxisMeters: 2574730,
|
|
893
|
-
inverseFlattening: 0
|
|
894
|
-
},
|
|
895
|
-
{
|
|
896
|
-
id: "pluto",
|
|
897
|
-
name: "Pluto",
|
|
898
|
-
semiMajorAxisMeters: 1188300,
|
|
899
|
-
inverseFlattening: 0
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
id: "charon",
|
|
903
|
-
name: "Charon",
|
|
904
|
-
semiMajorAxisMeters: 606e3,
|
|
905
|
-
inverseFlattening: 0
|
|
906
|
-
}
|
|
907
|
-
];
|
|
908
|
-
const DEFAULT_ELLIPSOID_ID = "earth";
|
|
909
|
-
/** Look an ellipsoid up by id, falling back to Earth for unknown ids. */
|
|
910
|
-
function getEllipsoid(id) {
|
|
911
|
-
return ELLIPSOIDS.find((e) => e.id === id) ?? ELLIPSOIDS.find((e) => e.id === "earth");
|
|
912
|
-
}
|
|
913
|
-
/**
|
|
914
|
-
* Mean radius `R = (2a + b) / 3` in metres, where `b` is the polar radius
|
|
915
|
-
* derived from the inverse flattening. This is the radius used for spherical
|
|
916
|
-
* (haversine) distance and area math.
|
|
917
|
-
*/
|
|
918
|
-
function meanRadiusMeters(ellipsoid) {
|
|
919
|
-
const a = ellipsoid.semiMajorAxisMeters;
|
|
920
|
-
if (!ellipsoid.inverseFlattening) return a;
|
|
921
|
-
const b = a * (1 - 1 / ellipsoid.inverseFlattening);
|
|
922
|
-
return (2 * a + b) / 3;
|
|
923
|
-
}
|
|
924
|
-
/** Earth's mean radius in metres — the radius Turf.js and MapLibre bake in. */
|
|
925
|
-
const EARTH_MEAN_RADIUS_METERS = meanRadiusMeters(ELLIPSOIDS.find((e) => e.id === "earth"));
|
|
926
|
-
let activeEllipsoidId = DEFAULT_ELLIPSOID_ID;
|
|
927
|
-
/**
|
|
928
|
-
* Point the measurement helpers at a body. Unknown ids fall back to Earth so a
|
|
929
|
-
* malformed project can never break measurements. Safe to call on every
|
|
930
|
-
* preferences change; it is a cheap assignment.
|
|
931
|
-
*/
|
|
932
|
-
function setActiveEllipsoidId(id) {
|
|
933
|
-
activeEllipsoidId = getEllipsoid(id).id;
|
|
934
|
-
}
|
|
935
|
-
function getActiveEllipsoid() {
|
|
936
|
-
return getEllipsoid(activeEllipsoidId);
|
|
937
|
-
}
|
|
938
|
-
/** Mean radius (metres) of the active body — for haversine distance/area. */
|
|
939
|
-
function getActiveMeanRadiusMeters() {
|
|
940
|
-
return meanRadiusMeters(getActiveEllipsoid());
|
|
941
|
-
}
|
|
942
|
-
/** Semi-major axis (metres) of the active body — for its Web-Mercator scale. */
|
|
943
|
-
function getActiveSemiMajorAxisMeters() {
|
|
944
|
-
return getActiveEllipsoid().semiMajorAxisMeters;
|
|
945
|
-
}
|
|
946
|
-
/**
|
|
947
|
-
* The active body's mean radius as a fraction of Earth's — `R_body / R_earth`,
|
|
948
|
-
* and exactly `1` on Earth.
|
|
949
|
-
*
|
|
950
|
-
* Everything GeoLibre measures ultimately comes from lon/lat *angles*, and the
|
|
951
|
-
* only thing that turns an angle into a ground distance is the radius it is
|
|
952
|
-
* multiplied by. Turf.js hardcodes Earth's, with no per-call override (that is
|
|
953
|
-
* the "Turf is Earth-locked" half of GeoLibre#1128), so rather than patch or
|
|
954
|
-
* replace Turf we post-scale what it returns and pre-scale what we hand it. The
|
|
955
|
-
* result is exact for a sphere, which is how every non-Earth body GeoLibre
|
|
956
|
-
* offers is modelled anyway, and within Mars' 0.6% flattening at worst.
|
|
957
|
-
*
|
|
958
|
-
* Suggested by @thareUSGS (USGS Astrogeology) on GeoLibre#1128 as the practical
|
|
959
|
-
* workaround planetary web maps have long used on Earth-locked engines.
|
|
960
|
-
*/
|
|
961
|
-
function getActiveBodyRadiusRatio() {
|
|
962
|
-
return getActiveMeanRadiusMeters() / EARTH_MEAN_RADIUS_METERS;
|
|
963
|
-
}
|
|
964
|
-
/**
|
|
965
|
-
* Convert a length Turf.js measured on Earth into the active body's true ground
|
|
966
|
-
* length. A no-op on Earth.
|
|
967
|
-
*
|
|
968
|
-
* Use on anything Turf *returns* — a distance, a length, a perimeter.
|
|
969
|
-
*
|
|
970
|
-
* The correction is a dimensionless ratio, so it is unit-agnostic: pass the
|
|
971
|
-
* length in whatever unit Turf gave it back (metres, kilometres, miles) and the
|
|
972
|
-
* result is in that same unit. There is no hidden unit conversion here.
|
|
973
|
-
*/
|
|
974
|
-
function earthLengthToBody(length) {
|
|
975
|
-
return length * getActiveBodyRadiusRatio();
|
|
976
|
-
}
|
|
977
|
-
/**
|
|
978
|
-
* Convert a true ground length on the active body into the Earth-equivalent
|
|
979
|
-
* length to hand Turf.js. A no-op on Earth.
|
|
980
|
-
*
|
|
981
|
-
* Use on anything passed *into* Turf as a distance — a buffer width, a circle
|
|
982
|
-
* or sector radius, a search threshold — so the resulting geometry spans that
|
|
983
|
-
* distance on this body rather than on Earth.
|
|
984
|
-
*
|
|
985
|
-
* Unit-agnostic like {@link earthLengthToBody}: pass the length in whatever
|
|
986
|
-
* unit you are about to hand Turf alongside it, and the result is in that unit.
|
|
987
|
-
*/
|
|
988
|
-
function bodyLengthToEarth(length) {
|
|
989
|
-
return length / getActiveBodyRadiusRatio();
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* Convert an area Turf.js measured on Earth into the active body's true ground
|
|
993
|
-
* area. A no-op on Earth.
|
|
994
|
-
*
|
|
995
|
-
* Areas scale with the *square* of the radius ratio, so this is not the same
|
|
996
|
-
* correction as {@link earthLengthToBody}. Unit-agnostic in the same way, for
|
|
997
|
-
* whatever squared unit the area is expressed in.
|
|
998
|
-
*/
|
|
999
|
-
function earthAreaToBody(area) {
|
|
1000
|
-
const ratio = getActiveBodyRadiusRatio();
|
|
1001
|
-
return area * ratio * ratio;
|
|
1002
|
-
}
|
|
1003
|
-
const PLANETARY_BASEMAP_SENTINEL_PREFIX = "geolibre://basemap/";
|
|
1004
|
-
const OPM_ATTRIBUTION = "<a href=\"https://www.openplanetary.org/opm\">OpenPlanetaryMap</a>";
|
|
1005
|
-
/** Data-source credit joined with the OpenPlanetaryMap attribution. */
|
|
1006
|
-
const opmCredit = (source) => `${source} · ${OPM_ATTRIBUTION}`;
|
|
1007
|
-
const TILE_PROXY_BASE = "https://tiles.geolibre.app/opm";
|
|
1008
|
-
const WMS_PROXY_BASE = "https://tiles.geolibre.app/wms";
|
|
1009
|
-
const USGS_ASTRO_ATTRIBUTION = "<a href=\"https://astrogeology.usgs.gov/\">USGS Astrogeology</a>";
|
|
1010
|
-
/** Data-source credit joined with the USGS Astrogeology attribution. */
|
|
1011
|
-
const usgsCredit = (source) => `${source} · ${USGS_ASTRO_ATTRIBUTION}`;
|
|
1012
|
-
const PLANETARY_BASEMAPS = [
|
|
1013
|
-
{
|
|
1014
|
-
id: "mars-colour-mola-elevation",
|
|
1015
|
-
name: "Colour MOLA Elevation",
|
|
1016
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-colour-mola-elevation`,
|
|
1017
|
-
tileUrl: `${TILE_PROXY_BASE}/mars-mola-color-noshade/{z}/{x}/{y}.png`,
|
|
1018
|
-
scheme: "tms",
|
|
1019
|
-
maxZoom: 6,
|
|
1020
|
-
attribution: opmCredit("NASA / MOLA"),
|
|
1021
|
-
ellipsoidId: "mars"
|
|
1022
|
-
},
|
|
1023
|
-
{
|
|
1024
|
-
id: "mars-viking-mdim21",
|
|
1025
|
-
name: "Viking MDIM2.1",
|
|
1026
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-viking-mdim21`,
|
|
1027
|
-
tileUrl: `${TILE_PROXY_BASE}/mars-viking-mdim21/{z}/{x}/{y}.png`,
|
|
1028
|
-
scheme: "tms",
|
|
1029
|
-
maxZoom: 7,
|
|
1030
|
-
attribution: opmCredit("NASA / Viking / USGS"),
|
|
1031
|
-
ellipsoidId: "mars"
|
|
1032
|
-
},
|
|
1033
|
-
{
|
|
1034
|
-
id: "mars-hillshade",
|
|
1035
|
-
name: "Hillshade",
|
|
1036
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-hillshade`,
|
|
1037
|
-
tileUrl: `${TILE_PROXY_BASE}/mars-hillshade/{z}/{x}/{y}.png`,
|
|
1038
|
-
scheme: "tms",
|
|
1039
|
-
maxZoom: 6,
|
|
1040
|
-
attribution: opmCredit("NASA / MOLA"),
|
|
1041
|
-
ellipsoidId: "mars"
|
|
1042
|
-
},
|
|
1043
|
-
{
|
|
1044
|
-
id: "mars-basemap-v0-2",
|
|
1045
|
-
name: "OPM Basemap v0.2",
|
|
1046
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-basemap-v0-2`,
|
|
1047
|
-
tileUrl: "https://cartocdn-gusc.global.ssl.fastly.net/opmbuilder/api/v1/map/named/opm-mars-basemap-v0-2/all/{z}/{x}/{y}.png",
|
|
1048
|
-
maxZoom: 6,
|
|
1049
|
-
attribution: OPM_ATTRIBUTION,
|
|
1050
|
-
ellipsoidId: "mars"
|
|
1051
|
-
},
|
|
1052
|
-
{
|
|
1053
|
-
id: "mars-shaded-colour-mola-elevation",
|
|
1054
|
-
name: "Shaded Colour MOLA Elevation",
|
|
1055
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-shaded-colour-mola-elevation`,
|
|
1056
|
-
tileUrl: `${TILE_PROXY_BASE}/mars-mola-color/{z}/{x}/{y}.png`,
|
|
1057
|
-
scheme: "tms",
|
|
1058
|
-
maxZoom: 6,
|
|
1059
|
-
attribution: opmCredit("NASA / MOLA"),
|
|
1060
|
-
ellipsoidId: "mars"
|
|
1061
|
-
},
|
|
1062
|
-
{
|
|
1063
|
-
id: "mars-shaded-grayscale-mola-elevation",
|
|
1064
|
-
name: "Shaded Grayscale MOLA Elevation",
|
|
1065
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-shaded-grayscale-mola-elevation`,
|
|
1066
|
-
tileUrl: `${TILE_PROXY_BASE}/mars-mola-gray/{z}/{x}/{y}.png`,
|
|
1067
|
-
scheme: "tms",
|
|
1068
|
-
maxZoom: 9,
|
|
1069
|
-
attribution: opmCredit("NASA / MOLA"),
|
|
1070
|
-
ellipsoidId: "mars"
|
|
1071
|
-
},
|
|
1072
|
-
{
|
|
1073
|
-
id: "moon-hillshaded-albedo",
|
|
1074
|
-
name: "Hillshaded Albedo",
|
|
1075
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}moon-hillshaded-albedo`,
|
|
1076
|
-
tileUrl: `${TILE_PROXY_BASE}/moon-hillshaded-albedo/{z}/{x}/{y}.png`,
|
|
1077
|
-
scheme: "tms",
|
|
1078
|
-
maxZoom: 6,
|
|
1079
|
-
attribution: opmCredit("NASA / LOLA / USGS"),
|
|
1080
|
-
ellipsoidId: "moon"
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
id: "moon-basemap-v0-1",
|
|
1084
|
-
name: "OPM Basemap v0.1",
|
|
1085
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}moon-basemap-v0-1`,
|
|
1086
|
-
tileUrl: "https://cartocdn-gusc.global.ssl.fastly.net/opmbuilder/api/v1/map/named/opm-moon-basemap-v0-1/all/{z}/{x}/{y}.png",
|
|
1087
|
-
maxZoom: 6,
|
|
1088
|
-
attribution: OPM_ATTRIBUTION,
|
|
1089
|
-
ellipsoidId: "moon"
|
|
1090
|
-
},
|
|
1091
|
-
{
|
|
1092
|
-
id: "mercury-messenger-color",
|
|
1093
|
-
name: "MESSENGER Colour Mosaic",
|
|
1094
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mercury-messenger-color`,
|
|
1095
|
-
tileUrl: `${WMS_PROXY_BASE}/mercury-messenger-color/{z}/{x}/{y}.png`,
|
|
1096
|
-
maxZoom: 7,
|
|
1097
|
-
attribution: usgsCredit("NASA / JHU APL / CIW"),
|
|
1098
|
-
ellipsoidId: "mercury"
|
|
1099
|
-
},
|
|
1100
|
-
{
|
|
1101
|
-
id: "mercury-messenger",
|
|
1102
|
-
name: "MESSENGER Basemap",
|
|
1103
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mercury-messenger`,
|
|
1104
|
-
tileUrl: `${WMS_PROXY_BASE}/mercury-messenger/{z}/{x}/{y}.png`,
|
|
1105
|
-
maxZoom: 7,
|
|
1106
|
-
attribution: usgsCredit("NASA / JHU APL"),
|
|
1107
|
-
ellipsoidId: "mercury"
|
|
1108
|
-
},
|
|
1109
|
-
{
|
|
1110
|
-
id: "venus-magellan",
|
|
1111
|
-
name: "Magellan FMAP",
|
|
1112
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}venus-magellan`,
|
|
1113
|
-
tileUrl: `${WMS_PROXY_BASE}/venus-magellan/{z}/{x}/{y}.png`,
|
|
1114
|
-
maxZoom: 6,
|
|
1115
|
-
attribution: usgsCredit("NASA / JPL"),
|
|
1116
|
-
ellipsoidId: "venus"
|
|
1117
|
-
},
|
|
1118
|
-
{
|
|
1119
|
-
id: "venus-magellan-color",
|
|
1120
|
-
name: "Magellan C3-MDIR Colour",
|
|
1121
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}venus-magellan-color`,
|
|
1122
|
-
tileUrl: `${WMS_PROXY_BASE}/venus-magellan-color/{z}/{x}/{y}.png`,
|
|
1123
|
-
maxZoom: 6,
|
|
1124
|
-
attribution: usgsCredit("NASA / JPL"),
|
|
1125
|
-
ellipsoidId: "venus"
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
id: "io-galileo-color",
|
|
1129
|
-
name: "Galileo SSI Colour",
|
|
1130
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}io-galileo-color`,
|
|
1131
|
-
tileUrl: `${WMS_PROXY_BASE}/io-galileo-color/{z}/{x}/{y}.png`,
|
|
1132
|
-
maxZoom: 6,
|
|
1133
|
-
attribution: usgsCredit("NASA / JPL"),
|
|
1134
|
-
ellipsoidId: "io"
|
|
1135
|
-
},
|
|
1136
|
-
{
|
|
1137
|
-
id: "europa-galileo-voyager",
|
|
1138
|
-
name: "Galileo / Voyager",
|
|
1139
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}europa-galileo-voyager`,
|
|
1140
|
-
tileUrl: `${WMS_PROXY_BASE}/europa-galileo-voyager/{z}/{x}/{y}.png`,
|
|
1141
|
-
maxZoom: 6,
|
|
1142
|
-
attribution: usgsCredit("NASA / JPL"),
|
|
1143
|
-
ellipsoidId: "europa"
|
|
1144
|
-
},
|
|
1145
|
-
{
|
|
1146
|
-
id: "ganymede-galileo-voyager",
|
|
1147
|
-
name: "Galileo / Voyager",
|
|
1148
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}ganymede-galileo-voyager`,
|
|
1149
|
-
tileUrl: `${WMS_PROXY_BASE}/ganymede-galileo-voyager/{z}/{x}/{y}.png`,
|
|
1150
|
-
maxZoom: 6,
|
|
1151
|
-
attribution: usgsCredit("NASA / JPL"),
|
|
1152
|
-
ellipsoidId: "ganymede"
|
|
1153
|
-
},
|
|
1154
|
-
{
|
|
1155
|
-
id: "callisto-galileo-voyager",
|
|
1156
|
-
name: "Galileo / Voyager",
|
|
1157
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}callisto-galileo-voyager`,
|
|
1158
|
-
tileUrl: `${WMS_PROXY_BASE}/callisto-galileo-voyager/{z}/{x}/{y}.png`,
|
|
1159
|
-
maxZoom: 6,
|
|
1160
|
-
attribution: usgsCredit("NASA / JPL"),
|
|
1161
|
-
ellipsoidId: "callisto"
|
|
1162
|
-
},
|
|
1163
|
-
{
|
|
1164
|
-
id: "titan-cassini",
|
|
1165
|
-
name: "Cassini ISS Mosaic",
|
|
1166
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}titan-cassini`,
|
|
1167
|
-
tileUrl: `${WMS_PROXY_BASE}/titan-cassini/{z}/{x}/{y}.png`,
|
|
1168
|
-
maxZoom: 6,
|
|
1169
|
-
attribution: usgsCredit("NASA / JPL / SSI"),
|
|
1170
|
-
ellipsoidId: "titan"
|
|
1171
|
-
},
|
|
1172
|
-
{
|
|
1173
|
-
id: "titan-hisar",
|
|
1174
|
-
name: "Cassini HiSAR Mosaic",
|
|
1175
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}titan-hisar`,
|
|
1176
|
-
tileUrl: `${WMS_PROXY_BASE}/titan-hisar/{z}/{x}/{y}.png`,
|
|
1177
|
-
maxZoom: 6,
|
|
1178
|
-
attribution: usgsCredit("NASA / JPL / USGS"),
|
|
1179
|
-
ellipsoidId: "titan"
|
|
1180
|
-
},
|
|
1181
|
-
{
|
|
1182
|
-
id: "pluto-mosaic",
|
|
1183
|
-
name: "New Horizons Mosaic",
|
|
1184
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}pluto-mosaic`,
|
|
1185
|
-
tileUrl: `${WMS_PROXY_BASE}/pluto-mosaic/{z}/{x}/{y}.png`,
|
|
1186
|
-
maxZoom: 7,
|
|
1187
|
-
attribution: usgsCredit("NASA / JHU APL / SwRI"),
|
|
1188
|
-
ellipsoidId: "pluto"
|
|
1189
|
-
},
|
|
1190
|
-
{
|
|
1191
|
-
id: "pluto-color",
|
|
1192
|
-
name: "New Horizons Colour Shaded Relief",
|
|
1193
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}pluto-color`,
|
|
1194
|
-
tileUrl: `${WMS_PROXY_BASE}/pluto-color/{z}/{x}/{y}.png`,
|
|
1195
|
-
maxZoom: 7,
|
|
1196
|
-
attribution: usgsCredit("NASA / JHU APL / SwRI"),
|
|
1197
|
-
ellipsoidId: "pluto"
|
|
1198
|
-
},
|
|
1199
|
-
{
|
|
1200
|
-
id: "charon-mosaic",
|
|
1201
|
-
name: "New Horizons Mosaic",
|
|
1202
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}charon-mosaic`,
|
|
1203
|
-
tileUrl: `${WMS_PROXY_BASE}/charon-mosaic/{z}/{x}/{y}.png`,
|
|
1204
|
-
maxZoom: 6,
|
|
1205
|
-
attribution: usgsCredit("NASA / JHU APL / SwRI"),
|
|
1206
|
-
ellipsoidId: "charon"
|
|
1207
|
-
},
|
|
1208
|
-
{
|
|
1209
|
-
id: "earth-usgs-imagery",
|
|
1210
|
-
name: "USGS Imagery",
|
|
1211
|
-
styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}earth-usgs-imagery`,
|
|
1212
|
-
tileUrl: "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}",
|
|
1213
|
-
maxZoom: 16,
|
|
1214
|
-
attribution: "<a href=\"https://www.usgs.gov/programs/national-geospatial-program/national-map\">USGS The National Map</a>",
|
|
1215
|
-
ellipsoidId: "earth"
|
|
1216
|
-
}
|
|
1217
|
-
];
|
|
1218
|
-
const DEDICATED_SECTION_BODIES = ["moon", "mars"];
|
|
1219
|
-
/**
|
|
1220
|
-
* {@link PLANETARY_BASEMAPS} grouped for the pickers: one section each for the
|
|
1221
|
-
* Moon and Mars, then a single "Other celestial bodies" section for the rest,
|
|
1222
|
-
* in {@link PLANETARY_BASEMAPS} order (grouped by body). Empty sections are
|
|
1223
|
-
* dropped.
|
|
1224
|
-
*/
|
|
1225
|
-
const PLANETARY_BASEMAP_GROUPS = [
|
|
1226
|
-
{
|
|
1227
|
-
id: "moon",
|
|
1228
|
-
ellipsoidId: "moon"
|
|
1229
|
-
},
|
|
1230
|
-
{
|
|
1231
|
-
id: "mars",
|
|
1232
|
-
ellipsoidId: "mars"
|
|
1233
|
-
},
|
|
1234
|
-
{
|
|
1235
|
-
id: "other",
|
|
1236
|
-
ellipsoidId: null
|
|
1237
|
-
}
|
|
1238
|
-
].map(({ id, ellipsoidId }) => {
|
|
1239
|
-
const basemaps = PLANETARY_BASEMAPS.filter((b) => ellipsoidId ? b.ellipsoidId === ellipsoidId : b.ellipsoidId !== "earth" && !DEDICATED_SECTION_BODIES.includes(b.ellipsoidId));
|
|
1240
|
-
return {
|
|
1241
|
-
id,
|
|
1242
|
-
basemaps: id === "other" ? [...basemaps].sort((a, b) => getEllipsoid(a.ellipsoidId).name.localeCompare(getEllipsoid(b.ellipsoidId).name)) : basemaps
|
|
1243
|
-
};
|
|
1244
|
-
}).filter((group) => group.basemaps.length > 0);
|
|
1245
|
-
const LEGACY_PLANETARY_BASEMAP_IDS = {
|
|
1246
|
-
"mars-opm": "mars-basemap-v0-2",
|
|
1247
|
-
"moon-opm": "moon-basemap-v0-1",
|
|
1248
|
-
"mars-viking": "mars-viking-mdim21",
|
|
1249
|
-
"moon-lroc": "moon-basemap-v0-1"
|
|
1250
|
-
};
|
|
1251
|
-
/** Resolve a `geolibre://basemap/<id>` sentinel to its planetary basemap. */
|
|
1252
|
-
function getPlanetaryBasemapByStyleUrl(styleUrl) {
|
|
1253
|
-
if (!styleUrl?.startsWith("geolibre://basemap/")) return void 0;
|
|
1254
|
-
const id = styleUrl.slice(19);
|
|
1255
|
-
const resolvedId = Object.prototype.hasOwnProperty.call(LEGACY_PLANETARY_BASEMAP_IDS, id) ? LEGACY_PLANETARY_BASEMAP_IDS[id] : id;
|
|
1256
|
-
return PLANETARY_BASEMAPS.find((b) => b.id === resolvedId);
|
|
1257
|
-
}
|
|
1258
|
-
/** Look a planetary basemap up by its stable id. */
|
|
1259
|
-
function getPlanetaryBasemapById(id) {
|
|
1260
|
-
return PLANETARY_BASEMAPS.find((b) => b.id === id);
|
|
1261
|
-
}
|
|
1262
|
-
/**
|
|
1263
|
-
* The celestial bodies offered by the Layers-panel planet switcher, in menu
|
|
1264
|
-
* order (like Google Earth's planet dropdown). Each body is paired with the
|
|
1265
|
-
* basemap the switcher applies for it — USGS satellite imagery for Earth, the
|
|
1266
|
-
* OpenPlanetaryMap Hillshaded Albedo mosaic for the Moon, and the OpenPlanetaryMap
|
|
1267
|
-
* Viking mosaic for Mars.
|
|
1268
|
-
*/
|
|
1269
|
-
const PLANET_SWITCHER_OPTIONS = [
|
|
1270
|
-
{
|
|
1271
|
-
ellipsoidId: "earth",
|
|
1272
|
-
basemapId: "earth-usgs-imagery"
|
|
1273
|
-
},
|
|
1274
|
-
{
|
|
1275
|
-
ellipsoidId: "moon",
|
|
1276
|
-
basemapId: "moon-hillshaded-albedo"
|
|
1277
|
-
},
|
|
1278
|
-
{
|
|
1279
|
-
ellipsoidId: "mars",
|
|
1280
|
-
basemapId: "mars-viking-mdim21"
|
|
1281
|
-
},
|
|
1282
|
-
{
|
|
1283
|
-
ellipsoidId: "callisto",
|
|
1284
|
-
basemapId: "callisto-galileo-voyager"
|
|
1285
|
-
},
|
|
1286
|
-
{
|
|
1287
|
-
ellipsoidId: "charon",
|
|
1288
|
-
basemapId: "charon-mosaic"
|
|
1289
|
-
},
|
|
1290
|
-
{
|
|
1291
|
-
ellipsoidId: "europa",
|
|
1292
|
-
basemapId: "europa-galileo-voyager"
|
|
1293
|
-
},
|
|
1294
|
-
{
|
|
1295
|
-
ellipsoidId: "ganymede",
|
|
1296
|
-
basemapId: "ganymede-galileo-voyager"
|
|
1297
|
-
},
|
|
1298
|
-
{
|
|
1299
|
-
ellipsoidId: "io",
|
|
1300
|
-
basemapId: "io-galileo-color"
|
|
1301
|
-
},
|
|
1302
|
-
{
|
|
1303
|
-
ellipsoidId: "mercury",
|
|
1304
|
-
basemapId: "mercury-messenger-color"
|
|
1305
|
-
},
|
|
1306
|
-
{
|
|
1307
|
-
ellipsoidId: "pluto",
|
|
1308
|
-
basemapId: "pluto-mosaic"
|
|
1309
|
-
},
|
|
1310
|
-
{
|
|
1311
|
-
ellipsoidId: "titan",
|
|
1312
|
-
basemapId: "titan-cassini"
|
|
1313
|
-
},
|
|
1314
|
-
{
|
|
1315
|
-
ellipsoidId: "venus",
|
|
1316
|
-
basemapId: "venus-magellan-color"
|
|
1317
|
-
}
|
|
1318
|
-
];
|
|
1319
|
-
//#endregion
|
|
1320
828
|
//#region src/regional-basemaps.ts
|
|
1321
829
|
const REGIONAL_BASEMAP_SENTINEL_PREFIX = "geolibre://regional-basemap/";
|
|
1322
830
|
const AMAP_ATTRIBUTION = "© <a href=\"https://www.amap.com\">高德地图 Amap</a>";
|
|
@@ -1729,146 +1237,6 @@ function basemapToCesiumImagery(styleUrl, cesiumBasemap = "project") {
|
|
|
1729
1237
|
return analogue ? toImagery(analogue) : { kind: "default" };
|
|
1730
1238
|
}
|
|
1731
1239
|
//#endregion
|
|
1732
|
-
//#region src/geojson-z.ts
|
|
1733
|
-
/**
|
|
1734
|
-
* Helpers for vector data whose coordinates carry a Z value (elevation), e.g.
|
|
1735
|
-
* GPX tracks with `<ele>` readings or LineStringZ/PointZ geometries. MapLibre's
|
|
1736
|
-
* 2D style layers ignore the third coordinate, so layers that want to render
|
|
1737
|
-
* their Z values use these helpers to detect and rescale elevations before
|
|
1738
|
-
* handing the data to a deck.gl overlay. The same data can carry its elevation
|
|
1739
|
-
* in a `bbox` member too, which `horizontalBbox` trims back to two dimensions.
|
|
1740
|
-
*/
|
|
1741
|
-
/**
|
|
1742
|
-
* Reduces a bounding box to its horizontal extent, `[west, south, east, north]`.
|
|
1743
|
-
*
|
|
1744
|
-
* RFC 7946 §5 lets a `bbox` member carry elevation, in which case it holds six
|
|
1745
|
-
* values — `[west, south, minAltitude, east, north, maxAltitude]` — and
|
|
1746
|
-
* `@turf/bbox` returns a collection's own member verbatim rather than
|
|
1747
|
-
* recomputing it. A consumer reading such a box as four values takes the
|
|
1748
|
-
* altitude for a longitude and the east edge for a latitude; that is how a 3D
|
|
1749
|
-
* feed reached MapLibre as an out-of-range latitude (#2358).
|
|
1750
|
-
*
|
|
1751
|
-
* Returns null when there is no usable horizontal extent: a box of any other
|
|
1752
|
-
* length, or one carrying a non-finite value anywhere — an empty collection, or
|
|
1753
|
-
* one whose features all have a null geometry, bboxes to ±Infinity, and a box
|
|
1754
|
-
* whose altitudes are not finite is not one to trust the rest of.
|
|
1755
|
-
*
|
|
1756
|
-
* @param box - A GeoJSON or Turf bounding box, of four or six values.
|
|
1757
|
-
*/
|
|
1758
|
-
function horizontalBbox(box) {
|
|
1759
|
-
if (!box) return null;
|
|
1760
|
-
if (box.length !== 4 && box.length !== 6) return null;
|
|
1761
|
-
if (!box.every((value) => Number.isFinite(value))) return null;
|
|
1762
|
-
return box.length === 6 ? [
|
|
1763
|
-
box[0],
|
|
1764
|
-
box[1],
|
|
1765
|
-
box[3],
|
|
1766
|
-
box[4]
|
|
1767
|
-
] : [
|
|
1768
|
-
box[0],
|
|
1769
|
-
box[1],
|
|
1770
|
-
box[2],
|
|
1771
|
-
box[3]
|
|
1772
|
-
];
|
|
1773
|
-
}
|
|
1774
|
-
const hasZCache = /* @__PURE__ */ new WeakMap();
|
|
1775
|
-
const hasFiniteZCache = /* @__PURE__ */ new WeakMap();
|
|
1776
|
-
/**
|
|
1777
|
-
* Returns true when any coordinate in the GeoJSON carries a finite, non-zero
|
|
1778
|
-
* Z value. All-zero Z arrays are treated as flat data because rendering them
|
|
1779
|
-
* "in 3D" would be indistinguishable from the 2D result. The result is
|
|
1780
|
-
* cached per GeoJSON object, so repeated calls on the same data are free.
|
|
1781
|
-
*
|
|
1782
|
-
* @param geojson - Any GeoJSON object (geometry, feature, or collection).
|
|
1783
|
-
* @param includeZero - Accept zero elevations when explicit absolute-height placement matters.
|
|
1784
|
-
*/
|
|
1785
|
-
function geojsonHasZCoordinates(geojson, includeZero = false) {
|
|
1786
|
-
if (!geojson) return false;
|
|
1787
|
-
const cache = includeZero ? hasFiniteZCache : hasZCache;
|
|
1788
|
-
const cached = cache.get(geojson);
|
|
1789
|
-
if (cached !== void 0) return cached;
|
|
1790
|
-
const result = someGeometry(geojson, (geometry) => somePosition(geometry, (position) => {
|
|
1791
|
-
const z = position[2];
|
|
1792
|
-
return typeof z === "number" && Number.isFinite(z) && (includeZero || z !== 0);
|
|
1793
|
-
}));
|
|
1794
|
-
cache.set(geojson, result);
|
|
1795
|
-
return result;
|
|
1796
|
-
}
|
|
1797
|
-
/**
|
|
1798
|
-
* Returns a copy of the GeoJSON with every coordinate's Z value mapped to
|
|
1799
|
-
* `z * verticalScale + offset` (missing or non-finite Z treated as 0). Used
|
|
1800
|
-
* to apply vertical exaggeration and a constant altitude offset before 3D
|
|
1801
|
-
* rendering. The identity transform (`verticalScale === 1 && offset === 0`)
|
|
1802
|
-
* still produces a copy so the non-finite-Z sanitization applies on every
|
|
1803
|
-
* path — WebGL does not handle NaN positions gracefully.
|
|
1804
|
-
*
|
|
1805
|
-
* @param geojson - The feature collection to transform.
|
|
1806
|
-
* @param verticalScale - Multiplier applied to each Z value.
|
|
1807
|
-
* @param offset - Constant altitude in meters added after scaling.
|
|
1808
|
-
*/
|
|
1809
|
-
function transformGeojsonElevation(geojson, verticalScale, offset) {
|
|
1810
|
-
const mapPosition = (position) => [
|
|
1811
|
-
position[0],
|
|
1812
|
-
position[1],
|
|
1813
|
-
(typeof position[2] === "number" && Number.isFinite(position[2]) ? position[2] : 0) * verticalScale + offset
|
|
1814
|
-
];
|
|
1815
|
-
return {
|
|
1816
|
-
...geojson,
|
|
1817
|
-
features: geojson.features.map((feature) => ({
|
|
1818
|
-
...feature,
|
|
1819
|
-
geometry: mapGeometryPositions(feature.geometry, mapPosition)
|
|
1820
|
-
}))
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
function someGeometry(geojson, predicate) {
|
|
1824
|
-
switch (geojson.type) {
|
|
1825
|
-
case "FeatureCollection": return geojson.features.some((feature) => feature.geometry ? someGeometry(feature.geometry, predicate) : false);
|
|
1826
|
-
case "Feature": return geojson.geometry ? someGeometry(geojson.geometry, predicate) : false;
|
|
1827
|
-
case "GeometryCollection": return geojson.geometries.some((geometry) => someGeometry(geometry, predicate));
|
|
1828
|
-
default: return predicate(geojson);
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
function somePosition(geometry, predicate) {
|
|
1832
|
-
switch (geometry.type) {
|
|
1833
|
-
case "Point": return predicate(geometry.coordinates);
|
|
1834
|
-
case "MultiPoint":
|
|
1835
|
-
case "LineString": return geometry.coordinates.some(predicate);
|
|
1836
|
-
case "MultiLineString":
|
|
1837
|
-
case "Polygon": return geometry.coordinates.some((ring) => ring.some(predicate));
|
|
1838
|
-
case "MultiPolygon": return geometry.coordinates.some((polygon) => polygon.some((ring) => ring.some(predicate)));
|
|
1839
|
-
case "GeometryCollection": return geometry.geometries.some((child) => somePosition(child, predicate));
|
|
1840
|
-
default: return false;
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
function mapGeometryPositions(geometry, mapPosition) {
|
|
1844
|
-
if (!geometry) return geometry;
|
|
1845
|
-
switch (geometry.type) {
|
|
1846
|
-
case "Point": return {
|
|
1847
|
-
...geometry,
|
|
1848
|
-
coordinates: mapPosition(geometry.coordinates)
|
|
1849
|
-
};
|
|
1850
|
-
case "MultiPoint":
|
|
1851
|
-
case "LineString": return {
|
|
1852
|
-
...geometry,
|
|
1853
|
-
coordinates: geometry.coordinates.map(mapPosition)
|
|
1854
|
-
};
|
|
1855
|
-
case "MultiLineString":
|
|
1856
|
-
case "Polygon": return {
|
|
1857
|
-
...geometry,
|
|
1858
|
-
coordinates: geometry.coordinates.map((ring) => ring.map(mapPosition))
|
|
1859
|
-
};
|
|
1860
|
-
case "MultiPolygon": return {
|
|
1861
|
-
...geometry,
|
|
1862
|
-
coordinates: geometry.coordinates.map((polygon) => polygon.map((ring) => ring.map(mapPosition)))
|
|
1863
|
-
};
|
|
1864
|
-
case "GeometryCollection": return {
|
|
1865
|
-
...geometry,
|
|
1866
|
-
geometries: geometry.geometries.map((child) => mapGeometryPositions(child, mapPosition))
|
|
1867
|
-
};
|
|
1868
|
-
default: return geometry;
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
//#endregion
|
|
1872
1240
|
//#region src/color-ramp.ts
|
|
1873
1241
|
/** The built-in color ramps offered by the style panels. */
|
|
1874
1242
|
const VECTOR_COLOR_RAMPS = [
|
|
@@ -2372,269 +1740,6 @@ function isAbsoluteFilesystemPath(path) {
|
|
|
2372
1740
|
return path.startsWith("/") || /^[a-z]:[\\/]/i.test(path);
|
|
2373
1741
|
}
|
|
2374
1742
|
//#endregion
|
|
2375
|
-
//#region src/polyline.ts
|
|
2376
|
-
/**
|
|
2377
|
-
* Pure TypeScript Google Encoded Polyline algorithm codec.
|
|
2378
|
-
*
|
|
2379
|
-
* Supports arbitrary precision:
|
|
2380
|
-
* - precision = 5: Google Maps standard, OSRM, HERE (factor 1e5, ~1.1m precision)
|
|
2381
|
-
* - precision = 6: Valhalla, Mapbox, Open Source Routing Machine 6 (factor 1e6, ~0.11m precision)
|
|
2382
|
-
*
|
|
2383
|
-
* Coordinates are represented as `[lon, lat]` pairs in GeoJSON format.
|
|
2384
|
-
*/
|
|
2385
|
-
/**
|
|
2386
|
-
* Unescapes backslashes in a polyline string.
|
|
2387
|
-
*
|
|
2388
|
-
* When polyline strings are copied from JSON payloads, log files, or source code,
|
|
2389
|
-
* backslashes are frequently double-escaped as `\\` or formatted with escape sequences.
|
|
2390
|
-
*
|
|
2391
|
-
* @param str - Input string with potentially escaped backslashes.
|
|
2392
|
-
* @returns Cleaned polyline string.
|
|
2393
|
-
*/
|
|
2394
|
-
function unescapePolyline(str) {
|
|
2395
|
-
if (!str || typeof str !== "string") return "";
|
|
2396
|
-
return str.replace(/\\\\/g, "\\").replace(/\\"/g, "\"").replace(/\\'/g, "'");
|
|
2397
|
-
}
|
|
2398
|
-
/**
|
|
2399
|
-
* Decodes an encoded polyline string into an array of `[lon, lat]` coordinates,
|
|
2400
|
-
* along with a `complete` flag indicating whether the entire string was consumed
|
|
2401
|
-
* as complete coordinate pairs without truncation or malformed characters.
|
|
2402
|
-
*
|
|
2403
|
-
* @param encoded - The ASCII-encoded polyline string.
|
|
2404
|
-
* @param precision - Number of decimal digits (default: 5).
|
|
2405
|
-
* @param unescape - Whether to unescape double-escaped backslashes before decoding (default: false).
|
|
2406
|
-
* @returns Object with decoded `coordinates` and `complete` boolean status.
|
|
2407
|
-
*/
|
|
2408
|
-
function decodePolylineDetailed(encoded, precision = 5, unescape = false) {
|
|
2409
|
-
if (!encoded || typeof encoded !== "string") return {
|
|
2410
|
-
coordinates: [],
|
|
2411
|
-
complete: true
|
|
2412
|
-
};
|
|
2413
|
-
const cleanEncoded = unescape ? unescapePolyline(encoded) : encoded;
|
|
2414
|
-
const factor = 10 ** precision;
|
|
2415
|
-
const len = cleanEncoded.length;
|
|
2416
|
-
const coordinates = [];
|
|
2417
|
-
let index = 0;
|
|
2418
|
-
let lat = 0;
|
|
2419
|
-
let lon = 0;
|
|
2420
|
-
while (index < len) {
|
|
2421
|
-
let shift = 0;
|
|
2422
|
-
let result = 0;
|
|
2423
|
-
let byte;
|
|
2424
|
-
let latComplete = false;
|
|
2425
|
-
do {
|
|
2426
|
-
if (index >= len) return {
|
|
2427
|
-
coordinates,
|
|
2428
|
-
complete: false
|
|
2429
|
-
};
|
|
2430
|
-
byte = cleanEncoded.charCodeAt(index++) - 63;
|
|
2431
|
-
if (byte < 0 || byte > 63) return {
|
|
2432
|
-
coordinates,
|
|
2433
|
-
complete: false
|
|
2434
|
-
};
|
|
2435
|
-
if (shift > 30 || shift === 30 && (byte & 31) > 3) return {
|
|
2436
|
-
coordinates,
|
|
2437
|
-
complete: false
|
|
2438
|
-
};
|
|
2439
|
-
result |= (byte & 31) << shift;
|
|
2440
|
-
shift += 5;
|
|
2441
|
-
if (byte < 32) {
|
|
2442
|
-
latComplete = true;
|
|
2443
|
-
break;
|
|
2444
|
-
}
|
|
2445
|
-
} while (byte >= 32);
|
|
2446
|
-
if (!latComplete) return {
|
|
2447
|
-
coordinates,
|
|
2448
|
-
complete: false
|
|
2449
|
-
};
|
|
2450
|
-
lat += result & 1 ? ~(result >>> 1) : result >>> 1;
|
|
2451
|
-
shift = 0;
|
|
2452
|
-
result = 0;
|
|
2453
|
-
let lonComplete = false;
|
|
2454
|
-
do {
|
|
2455
|
-
if (index >= len) return {
|
|
2456
|
-
coordinates,
|
|
2457
|
-
complete: false
|
|
2458
|
-
};
|
|
2459
|
-
byte = cleanEncoded.charCodeAt(index++) - 63;
|
|
2460
|
-
if (byte < 0 || byte > 63) return {
|
|
2461
|
-
coordinates,
|
|
2462
|
-
complete: false
|
|
2463
|
-
};
|
|
2464
|
-
if (shift > 30 || shift === 30 && (byte & 31) > 3) return {
|
|
2465
|
-
coordinates,
|
|
2466
|
-
complete: false
|
|
2467
|
-
};
|
|
2468
|
-
result |= (byte & 31) << shift;
|
|
2469
|
-
shift += 5;
|
|
2470
|
-
if (byte < 32) {
|
|
2471
|
-
lonComplete = true;
|
|
2472
|
-
break;
|
|
2473
|
-
}
|
|
2474
|
-
} while (byte >= 32);
|
|
2475
|
-
if (!lonComplete) return {
|
|
2476
|
-
coordinates,
|
|
2477
|
-
complete: false
|
|
2478
|
-
};
|
|
2479
|
-
lon += result & 1 ? ~(result >>> 1) : result >>> 1;
|
|
2480
|
-
coordinates.push([lon / factor, lat / factor]);
|
|
2481
|
-
}
|
|
2482
|
-
return {
|
|
2483
|
-
coordinates,
|
|
2484
|
-
complete: true
|
|
2485
|
-
};
|
|
2486
|
-
}
|
|
2487
|
-
/**
|
|
2488
|
-
* Decodes an encoded polyline string into an array of `[lon, lat]` coordinates.
|
|
2489
|
-
*
|
|
2490
|
-
* @param encoded - The ASCII-encoded polyline string.
|
|
2491
|
-
* @param precision - Number of decimal digits (default: 5).
|
|
2492
|
-
* @param unescape - Whether to unescape double-escaped backslashes before decoding (default: false).
|
|
2493
|
-
* @returns Array of `[lon, lat]` coordinate pairs in GeoJSON order.
|
|
2494
|
-
*/
|
|
2495
|
-
function decodePolyline(encoded, precision = 5, unescape = false) {
|
|
2496
|
-
return decodePolylineDetailed(encoded, precision, unescape).coordinates;
|
|
2497
|
-
}
|
|
2498
|
-
/**
|
|
2499
|
-
* Encodes a single integer delta value into polyline character chunks.
|
|
2500
|
-
*/
|
|
2501
|
-
function encodeSignedNumber(num) {
|
|
2502
|
-
let sgnNum = num < 0 ? ~(num << 1) : num << 1;
|
|
2503
|
-
let out = "";
|
|
2504
|
-
while (sgnNum >= 32) {
|
|
2505
|
-
out += String.fromCharCode((32 | sgnNum & 31) + 63);
|
|
2506
|
-
sgnNum >>>= 5;
|
|
2507
|
-
}
|
|
2508
|
-
out += String.fromCharCode(sgnNum + 63);
|
|
2509
|
-
return out;
|
|
2510
|
-
}
|
|
2511
|
-
/**
|
|
2512
|
-
* Encodes an array of `[lon, lat]` coordinates into a Google Encoded Polyline string.
|
|
2513
|
-
*
|
|
2514
|
-
* @param coordinates - Array of `[lon, lat]` coordinate pairs.
|
|
2515
|
-
* @param precision - Number of decimal digits (default: 5).
|
|
2516
|
-
* @returns The encoded polyline string.
|
|
2517
|
-
*/
|
|
2518
|
-
function encodePolyline(coordinates, precision = 5) {
|
|
2519
|
-
if (!Array.isArray(coordinates) || coordinates.length === 0) return "";
|
|
2520
|
-
const factor = 10 ** precision;
|
|
2521
|
-
let output = "";
|
|
2522
|
-
let prevLat = 0;
|
|
2523
|
-
let prevLon = 0;
|
|
2524
|
-
for (const [lon, lat] of coordinates) {
|
|
2525
|
-
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
|
|
2526
|
-
const roundLat = Math.round(lat * factor);
|
|
2527
|
-
const roundLon = Math.round(lon * factor);
|
|
2528
|
-
const deltaLat = roundLat - prevLat;
|
|
2529
|
-
const deltaLon = roundLon - prevLon;
|
|
2530
|
-
output += encodeSignedNumber(deltaLat);
|
|
2531
|
-
output += encodeSignedNumber(deltaLon);
|
|
2532
|
-
prevLat = roundLat;
|
|
2533
|
-
prevLon = roundLon;
|
|
2534
|
-
}
|
|
2535
|
-
return output;
|
|
2536
|
-
}
|
|
2537
|
-
/**
|
|
2538
|
-
* Decodes an encoded polyline string into a GeoJSON `Feature<LineString>`.
|
|
2539
|
-
*
|
|
2540
|
-
* @param encoded - The polyline string.
|
|
2541
|
-
* @param precision - Coordinate precision (default: 5).
|
|
2542
|
-
* @param properties - Optional feature properties.
|
|
2543
|
-
* @returns A GeoJSON LineString Feature.
|
|
2544
|
-
*/
|
|
2545
|
-
function polylineStrToGeoJSON(encoded, precision = 5, properties = {}) {
|
|
2546
|
-
return {
|
|
2547
|
-
type: "Feature",
|
|
2548
|
-
properties: { ...properties },
|
|
2549
|
-
geometry: {
|
|
2550
|
-
type: "LineString",
|
|
2551
|
-
coordinates: decodePolyline(encoded, precision)
|
|
2552
|
-
}
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
/**
|
|
2556
|
-
* Encodes a GeoJSON LineString, MultiLineString, Feature, or FeatureCollection into encoded polyline string(s).
|
|
2557
|
-
*
|
|
2558
|
-
* @param geojson - The GeoJSON input geometry or feature.
|
|
2559
|
-
* @param precision - Coordinate precision (default: 5).
|
|
2560
|
-
* @returns An encoded polyline string for LineString, or an array of strings for MultiLineString / FeatureCollection.
|
|
2561
|
-
*/
|
|
2562
|
-
function geoJSONToPolylineStr(geojson, precision = 5) {
|
|
2563
|
-
if (!geojson) return "";
|
|
2564
|
-
if (geojson.type === "Feature") return geoJSONToPolylineStr(geojson.geometry, precision);
|
|
2565
|
-
if (geojson.type === "FeatureCollection") {
|
|
2566
|
-
const results = [];
|
|
2567
|
-
for (const feature of geojson.features) {
|
|
2568
|
-
const res = geoJSONToPolylineStr(feature.geometry, precision);
|
|
2569
|
-
if (Array.isArray(res)) results.push(...res);
|
|
2570
|
-
else if (res) results.push(res);
|
|
2571
|
-
}
|
|
2572
|
-
return results;
|
|
2573
|
-
}
|
|
2574
|
-
if (geojson.type === "LineString") return encodePolyline(geojson.coordinates, precision);
|
|
2575
|
-
if (geojson.type === "MultiLineString") return geojson.coordinates.map((lineCoords) => encodePolyline(lineCoords, precision));
|
|
2576
|
-
return "";
|
|
2577
|
-
}
|
|
2578
|
-
/**
|
|
2579
|
-
* Parses and batch-decodes multiple polylines from a text block or file content.
|
|
2580
|
-
*
|
|
2581
|
-
* @param input - Multiline string or delimited text containing encoded polylines.
|
|
2582
|
-
* @param options - Decoding and parsing configuration.
|
|
2583
|
-
* @returns A FeatureCollection containing LineString (or MultiLineString) features.
|
|
2584
|
-
*/
|
|
2585
|
-
function batchDecodePolylines(input, options = {}) {
|
|
2586
|
-
if (!input || typeof input !== "string") return {
|
|
2587
|
-
type: "FeatureCollection",
|
|
2588
|
-
features: []
|
|
2589
|
-
};
|
|
2590
|
-
const { precision = 5, unescape = true, delimiter = /\r?\n/, asMultiLine = false, baseProperties = {} } = options;
|
|
2591
|
-
const rawChunks = input.split(delimiter);
|
|
2592
|
-
const validLineCoords = [];
|
|
2593
|
-
const features = [];
|
|
2594
|
-
for (let i = 0; i < rawChunks.length; i++) {
|
|
2595
|
-
const raw = rawChunks[i].trim();
|
|
2596
|
-
if (!raw) continue;
|
|
2597
|
-
const cleaned = raw.replace(/^["']|["']$/g, "").trim();
|
|
2598
|
-
if (!cleaned) continue;
|
|
2599
|
-
const decodeResult = decodePolylineDetailed(cleaned, precision, unescape);
|
|
2600
|
-
if (decodeResult.complete && decodeResult.coordinates.length >= 2) {
|
|
2601
|
-
const coords = decodeResult.coordinates;
|
|
2602
|
-
validLineCoords.push(coords);
|
|
2603
|
-
features.push({
|
|
2604
|
-
type: "Feature",
|
|
2605
|
-
properties: {
|
|
2606
|
-
...baseProperties,
|
|
2607
|
-
line_index: features.length + 1,
|
|
2608
|
-
point_count: coords.length
|
|
2609
|
-
},
|
|
2610
|
-
geometry: {
|
|
2611
|
-
type: "LineString",
|
|
2612
|
-
coordinates: coords
|
|
2613
|
-
}
|
|
2614
|
-
});
|
|
2615
|
-
}
|
|
2616
|
-
}
|
|
2617
|
-
if (asMultiLine && validLineCoords.length > 0) return {
|
|
2618
|
-
type: "FeatureCollection",
|
|
2619
|
-
features: [{
|
|
2620
|
-
type: "Feature",
|
|
2621
|
-
properties: {
|
|
2622
|
-
...baseProperties,
|
|
2623
|
-
line_count: validLineCoords.length,
|
|
2624
|
-
point_count: validLineCoords.reduce((acc, c) => acc + c.length, 0)
|
|
2625
|
-
},
|
|
2626
|
-
geometry: {
|
|
2627
|
-
type: "MultiLineString",
|
|
2628
|
-
coordinates: validLineCoords
|
|
2629
|
-
}
|
|
2630
|
-
}]
|
|
2631
|
-
};
|
|
2632
|
-
return {
|
|
2633
|
-
type: "FeatureCollection",
|
|
2634
|
-
features
|
|
2635
|
-
};
|
|
2636
|
-
}
|
|
2637
|
-
//#endregion
|
|
2638
1743
|
//#region src/runtime-env.ts
|
|
2639
1744
|
const buildEnv = typeof __GEOLIBRE_BUILD_ENV__ === "undefined" ? {} : __GEOLIBRE_BUILD_ENV__;
|
|
2640
1745
|
/**
|
|
@@ -3057,511 +2162,63 @@ function toComparableNumber(value) {
|
|
|
3057
2162
|
const parsed = Date.parse(trimmed);
|
|
3058
2163
|
if (Number.isFinite(parsed)) return parsed;
|
|
3059
2164
|
}
|
|
3060
|
-
return null;
|
|
3061
|
-
}
|
|
3062
|
-
/**
|
|
3063
|
-
* Error thrown when a Valhalla request returns a non-2xx response. Carries the
|
|
3064
|
-
* HTTP status so callers can branch on it (e.g. a 4xx rejection) without
|
|
3065
|
-
* pattern-matching the message string.
|
|
3066
|
-
*/
|
|
3067
|
-
var RoutingRequestError = class extends Error {
|
|
3068
|
-
status;
|
|
3069
|
-
constructor(status, statusText) {
|
|
3070
|
-
super(`Routing request failed (${status} ${statusText})`);
|
|
3071
|
-
this.name = "RoutingRequestError";
|
|
3072
|
-
this.status = status;
|
|
3073
|
-
}
|
|
3074
|
-
};
|
|
3075
|
-
async function postJson(url, body, signal) {
|
|
3076
|
-
const response = await fetch(url, {
|
|
3077
|
-
method: "POST",
|
|
3078
|
-
headers: { "Content-Type": "application/json" },
|
|
3079
|
-
body: JSON.stringify(body),
|
|
3080
|
-
signal
|
|
3081
|
-
});
|
|
3082
|
-
if (!response.ok) throw new RoutingRequestError(response.status, response.statusText);
|
|
3083
|
-
return response.json();
|
|
3084
|
-
}
|
|
3085
|
-
/**
|
|
3086
|
-
* Requests an isochrone from the Valhalla server.
|
|
3087
|
-
*
|
|
3088
|
-
* @param endpoint - The Valhalla base URL.
|
|
3089
|
-
* @param body - The request body from {@link buildIsochroneRequest}.
|
|
3090
|
-
* @param signal - Optional abort signal.
|
|
3091
|
-
* @returns The Valhalla GeoJSON response.
|
|
3092
|
-
*/
|
|
3093
|
-
function requestIsochrone(endpoint, body, signal) {
|
|
3094
|
-
return postJson(`${stripTrailingSlash(endpoint)}/isochrone`, body, signal);
|
|
3095
|
-
}
|
|
3096
|
-
/**
|
|
3097
|
-
* Requests an OD cost matrix from the Valhalla server.
|
|
3098
|
-
*
|
|
3099
|
-
* @param endpoint - The Valhalla base URL.
|
|
3100
|
-
* @param body - The request body from {@link buildMatrixRequest}.
|
|
3101
|
-
* @param signal - Optional abort signal.
|
|
3102
|
-
* @returns The Valhalla matrix response.
|
|
3103
|
-
*/
|
|
3104
|
-
function requestMatrix(endpoint, body, signal) {
|
|
3105
|
-
return postJson(`${stripTrailingSlash(endpoint)}/sources_to_targets`, body, signal);
|
|
3106
|
-
}
|
|
3107
|
-
/**
|
|
3108
|
-
* Requests a route through ordered waypoints from the Valhalla server.
|
|
3109
|
-
*
|
|
3110
|
-
* @param endpoint - The Valhalla base URL.
|
|
3111
|
-
* @param body - The request body from {@link buildRouteRequest}.
|
|
3112
|
-
* @param signal - Optional abort signal.
|
|
3113
|
-
* @returns The Valhalla `/route` response.
|
|
3114
|
-
*/
|
|
3115
|
-
function requestRoute(endpoint, body, signal) {
|
|
3116
|
-
return postJson(`${stripTrailingSlash(endpoint)}/route`, body, signal);
|
|
3117
|
-
}
|
|
3118
|
-
//#endregion
|
|
3119
|
-
//#region src/selection.ts
|
|
3120
|
-
/** Selection modes in display order. */
|
|
3121
|
-
const SELECTION_MODES = [
|
|
3122
|
-
"new",
|
|
3123
|
-
"add",
|
|
3124
|
-
"remove",
|
|
3125
|
-
"intersect"
|
|
3126
|
-
];
|
|
3127
|
-
/**
|
|
3128
|
-
* The id under which a feature participates in the selection model. Mirrors
|
|
3129
|
-
* the convention shared by the attribute table and the map highlight
|
|
3130
|
-
* (`String(feature.id ?? index)`): the feature's own `id` when present,
|
|
3131
|
-
* otherwise its index in the layer's feature array.
|
|
3132
|
-
*/
|
|
3133
|
-
function featureSelectionId(feature, index) {
|
|
3134
|
-
return String(feature.id ?? index);
|
|
3135
|
-
}
|
|
3136
|
-
/**
|
|
3137
|
-
* Combines the current selection with a freshly matched id set according to
|
|
3138
|
-
* `mode`. The result is deduplicated and order-stable: "new" follows the
|
|
3139
|
-
* matched order, "add" appends unseen matches after the current ids, and
|
|
3140
|
-
* "remove"/"intersect" preserve the current selection's order.
|
|
3141
|
-
*/
|
|
3142
|
-
function applySelectionMode(current, matched, mode) {
|
|
3143
|
-
const matchedSet = new Set(matched);
|
|
3144
|
-
switch (mode) {
|
|
3145
|
-
case "new": return Array.from(matchedSet);
|
|
3146
|
-
case "add": {
|
|
3147
|
-
const merged = new Set(current);
|
|
3148
|
-
for (const id of matched) merged.add(id);
|
|
3149
|
-
return Array.from(merged);
|
|
3150
|
-
}
|
|
3151
|
-
case "remove": return Array.from(new Set(current.filter((id) => !matchedSet.has(id))));
|
|
3152
|
-
case "intersect": return Array.from(new Set(current.filter((id) => matchedSet.has(id))));
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
/**
|
|
3156
|
-
* The complement of `current` within `allIds` (QGIS "Invert selection"):
|
|
3157
|
-
* every feature id not currently selected, in layer order. With an empty
|
|
3158
|
-
* selection this selects everything.
|
|
3159
|
-
*/
|
|
3160
|
-
function invertSelection(allIds, current) {
|
|
3161
|
-
const selected = new Set(current);
|
|
3162
|
-
return allIds.filter((id) => !selected.has(id));
|
|
3163
|
-
}
|
|
3164
|
-
//#endregion
|
|
3165
|
-
//#region src/expressions.ts
|
|
3166
|
-
const fn = (name, snippet, docKey) => ({
|
|
3167
|
-
name,
|
|
3168
|
-
snippet,
|
|
3169
|
-
docKey
|
|
3170
|
-
});
|
|
3171
|
-
/**
|
|
3172
|
-
* The curated function reference, grouped by category. Snippets are valid,
|
|
3173
|
-
* standalone-compilable MapLibre expressions (enforced by tests) so inserting
|
|
3174
|
-
* one into an empty editor always yields a working starting point.
|
|
3175
|
-
*/
|
|
3176
|
-
const EXPRESSION_FUNCTION_CATEGORIES = [
|
|
3177
|
-
{
|
|
3178
|
-
key: "data",
|
|
3179
|
-
functions: [
|
|
3180
|
-
fn("get", "[\"get\", \"field\"]", "get"),
|
|
3181
|
-
fn("has", "[\"has\", \"field\"]", "has"),
|
|
3182
|
-
fn("id", "[\"id\"]", "id"),
|
|
3183
|
-
fn("properties", "[\"properties\"]", "properties"),
|
|
3184
|
-
fn("geometry-type", "[\"geometry-type\"]", "geometryType"),
|
|
3185
|
-
fn("at", "[\"at\", 0, [\"literal\", [1, 2, 3]]]", "at"),
|
|
3186
|
-
fn("in", "[\"in\", \"value\", [\"get\", \"field\"]]", "in"),
|
|
3187
|
-
fn("index-of", "[\"index-of\", \"value\", [\"get\", \"field\"]]", "indexOf"),
|
|
3188
|
-
fn("length", "[\"length\", [\"get\", \"field\"]]", "length"),
|
|
3189
|
-
fn("literal", "[\"literal\", [\"a\", \"b\"]]", "literal"),
|
|
3190
|
-
fn("typeof", "[\"typeof\", [\"get\", \"field\"]]", "typeofOp")
|
|
3191
|
-
]
|
|
3192
|
-
},
|
|
3193
|
-
{
|
|
3194
|
-
key: "decision",
|
|
3195
|
-
functions: [
|
|
3196
|
-
fn("case", "[\"case\", [\"==\", [\"get\", \"field\"], \"value\"], \"#ff0000\", \"#cccccc\"]", "caseOp"),
|
|
3197
|
-
fn("match", "[\"match\", [\"get\", \"field\"], \"a\", \"#2563eb\", \"b\", \"#16a34a\", \"#94a3b8\"]", "match"),
|
|
3198
|
-
fn("coalesce", "[\"coalesce\", [\"get\", \"field\"], \"fallback\"]", "coalesce"),
|
|
3199
|
-
fn("==", "[\"==\", [\"get\", \"field\"], \"value\"]", "eq"),
|
|
3200
|
-
fn("!=", "[\"!=\", [\"get\", \"field\"], \"value\"]", "neq"),
|
|
3201
|
-
fn(">", "[\">\", [\"get\", \"field\"], 0]", "gt"),
|
|
3202
|
-
fn(">=", "[\">=\", [\"get\", \"field\"], 0]", "gte"),
|
|
3203
|
-
fn("<", "[\"<\", [\"get\", \"field\"], 0]", "lt"),
|
|
3204
|
-
fn("<=", "[\"<=\", [\"get\", \"field\"], 0]", "lte"),
|
|
3205
|
-
fn("all", "[\"all\", [\">\", [\"get\", \"field\"], 0], [\"<\", [\"get\", \"field\"], 100]]", "all"),
|
|
3206
|
-
fn("any", "[\"any\", [\"==\", [\"get\", \"field\"], \"a\"], [\"==\", [\"get\", \"field\"], \"b\"]]", "any"),
|
|
3207
|
-
fn("!", "[\"!\", [\"has\", \"field\"]]", "not")
|
|
3208
|
-
]
|
|
3209
|
-
},
|
|
3210
|
-
{
|
|
3211
|
-
key: "math",
|
|
3212
|
-
functions: [
|
|
3213
|
-
fn("+", "[\"+\", [\"get\", \"field\"], 1]", "add"),
|
|
3214
|
-
fn("-", "[\"-\", [\"get\", \"field\"], 1]", "subtract"),
|
|
3215
|
-
fn("*", "[\"*\", [\"get\", \"field\"], 2]", "multiply"),
|
|
3216
|
-
fn("/", "[\"/\", [\"get\", \"field\"], 2]", "divide"),
|
|
3217
|
-
fn("%", "[\"%\", [\"get\", \"field\"], 2]", "modulo"),
|
|
3218
|
-
fn("^", "[\"^\", [\"get\", \"field\"], 2]", "power"),
|
|
3219
|
-
fn("abs", "[\"abs\", [\"get\", \"field\"]]", "abs"),
|
|
3220
|
-
fn("ceil", "[\"ceil\", [\"get\", \"field\"]]", "ceil"),
|
|
3221
|
-
fn("floor", "[\"floor\", [\"get\", \"field\"]]", "floor"),
|
|
3222
|
-
fn("round", "[\"round\", [\"get\", \"field\"]]", "round"),
|
|
3223
|
-
fn("sqrt", "[\"sqrt\", [\"get\", \"field\"]]", "sqrt"),
|
|
3224
|
-
fn("ln", "[\"ln\", [\"get\", \"field\"]]", "ln"),
|
|
3225
|
-
fn("log10", "[\"log10\", [\"get\", \"field\"]]", "log10"),
|
|
3226
|
-
fn("min", "[\"min\", [\"get\", \"field\"], 0]", "min"),
|
|
3227
|
-
fn("max", "[\"max\", [\"get\", \"field\"], 0]", "max")
|
|
3228
|
-
]
|
|
3229
|
-
},
|
|
3230
|
-
{
|
|
3231
|
-
key: "string",
|
|
3232
|
-
functions: [
|
|
3233
|
-
fn("concat", "[\"concat\", [\"get\", \"field\"], \" suffix\"]", "concat"),
|
|
3234
|
-
fn("upcase", "[\"upcase\", [\"get\", \"field\"]]", "upcase"),
|
|
3235
|
-
fn("downcase", "[\"downcase\", [\"get\", \"field\"]]", "downcase"),
|
|
3236
|
-
fn("slice", "[\"slice\", [\"get\", \"field\"], 0, 3]", "slice"),
|
|
3237
|
-
fn("number-format", "[\"number-format\", [\"get\", \"field\"], {\"locale\": \"en-US\", \"max-fraction-digits\": 2}]", "numberFormat")
|
|
3238
|
-
]
|
|
3239
|
-
},
|
|
3240
|
-
{
|
|
3241
|
-
key: "conversion",
|
|
3242
|
-
functions: [
|
|
3243
|
-
fn("to-number", "[\"to-number\", [\"get\", \"field\"]]", "toNumber"),
|
|
3244
|
-
fn("to-string", "[\"to-string\", [\"get\", \"field\"]]", "toString"),
|
|
3245
|
-
fn("to-boolean", "[\"to-boolean\", [\"get\", \"field\"]]", "toBoolean"),
|
|
3246
|
-
fn("to-color", "[\"to-color\", \"#2563eb\"]", "toColor")
|
|
3247
|
-
]
|
|
3248
|
-
},
|
|
3249
|
-
{
|
|
3250
|
-
key: "camera",
|
|
3251
|
-
functions: [
|
|
3252
|
-
fn("zoom", "[\"zoom\"]", "zoom"),
|
|
3253
|
-
fn("interpolate", "[\"interpolate\", [\"linear\"], [\"zoom\"], 5, [\"to-color\", \"#2563eb\"], 12, [\"to-color\", \"#dc2626\"]]", "interpolate"),
|
|
3254
|
-
fn("step", "[\"step\", [\"zoom\"], \"#2563eb\", 10, \"#dc2626\"]", "step"),
|
|
3255
|
-
fn("rgb", "[\"rgb\", 37, 99, 235]", "rgb"),
|
|
3256
|
-
fn("rgba", "[\"rgba\", 37, 99, 235, 0.5]", "rgba")
|
|
3257
|
-
]
|
|
3258
|
-
}
|
|
3259
|
-
];
|
|
3260
|
-
/**
|
|
3261
|
-
* Strips commas that directly precede a closing bracket/brace outside of
|
|
3262
|
-
* strings, so user-typed expressions with trailing commas still parse.
|
|
3263
|
-
* Shared by {@link parseJsonExpression} (vector-color) and the builder.
|
|
3264
|
-
*/
|
|
3265
|
-
function removeTrailingJsonCommas(value) {
|
|
3266
|
-
let result = "";
|
|
3267
|
-
let inString = false;
|
|
3268
|
-
let escaped = false;
|
|
3269
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
3270
|
-
const char = value[index];
|
|
3271
|
-
if (inString) {
|
|
3272
|
-
result += char;
|
|
3273
|
-
if (escaped) escaped = false;
|
|
3274
|
-
else if (char === "\\") escaped = true;
|
|
3275
|
-
else if (char === "\"") inString = false;
|
|
3276
|
-
continue;
|
|
3277
|
-
}
|
|
3278
|
-
if (char === "\"") {
|
|
3279
|
-
inString = true;
|
|
3280
|
-
result += char;
|
|
3281
|
-
continue;
|
|
3282
|
-
}
|
|
3283
|
-
if (char === ",") {
|
|
3284
|
-
const nextSignificant = value.slice(index + 1).match(/\S/)?.[0];
|
|
3285
|
-
if (nextSignificant === "]" || nextSignificant === "}") continue;
|
|
3286
|
-
}
|
|
3287
|
-
result += char;
|
|
3288
|
-
}
|
|
3289
|
-
return result;
|
|
3290
|
-
}
|
|
3291
|
-
/**
|
|
3292
|
-
* The `rootKey` `createExpression` (style-spec v26+) requires as its second
|
|
3293
|
-
* argument — a location label it only surfaces when attaching a runtime
|
|
3294
|
-
* warning to `console.warn` during evaluation. These expressions come from the
|
|
3295
|
-
* Expression Builder dialog, not a style JSON layer, so a generic label is the
|
|
3296
|
-
* honest value; it never appears in the parsing errors we report.
|
|
3297
|
-
*/
|
|
3298
|
-
const EXPRESSION_ROOT_KEY = "expression";
|
|
3299
|
-
/**
|
|
3300
|
-
* Builds the minimal property spec that makes `createExpression` enforce a
|
|
3301
|
-
* result type while still allowing zoom- and feature-dependent input.
|
|
3302
|
-
*
|
|
3303
|
-
* This mirrors the (unexported) `StylePropertySpecification` shape that
|
|
3304
|
-
* `@maplibre/maplibre-gl-style-spec` expects — the cast below means the
|
|
3305
|
-
* compiler cannot catch a change to that contract, so whenever the
|
|
3306
|
-
* style-spec package is bumped (including Dependabot PRs) re-check that
|
|
3307
|
-
* expected-type enforcement still works, and that the property spec is still
|
|
3308
|
-
* `createExpression`'s third parameter (v26 inserted a `rootKey` at index 1,
|
|
3309
|
-
* shifting it from `[1]` to `[2]`). The "enforces an expected result type"
|
|
3310
|
-
* test in `tests/expressions.test.ts` fails if this shape stops being honored.
|
|
3311
|
-
*/
|
|
3312
|
-
function propertySpecFor(expectedType) {
|
|
3313
|
-
return {
|
|
3314
|
-
type: expectedType,
|
|
3315
|
-
"property-type": "data-driven",
|
|
3316
|
-
expression: { parameters: ["zoom", "feature"] }
|
|
3317
|
-
};
|
|
3318
|
-
}
|
|
3319
|
-
/**
|
|
3320
|
-
* Validates a user-entered MapLibre expression: tolerant JSON parse, shape
|
|
3321
|
-
* checks, then a real compile through the MapLibre style spec so operator
|
|
3322
|
-
* misuse (wrong arity, unknown operator, type mismatch) is caught in the
|
|
3323
|
-
* dialog instead of silently failing on the map. Variables are substituted
|
|
3324
|
-
* before compiling (so tokens type-check as their values), and
|
|
3325
|
-
* `expectedType` additionally enforces the destination's result type. The
|
|
3326
|
-
* returned `parsed` array is the raw, unsubstituted parse. An empty/blank
|
|
3327
|
-
* source is valid (surfaces treat it as "no expression").
|
|
3328
|
-
*/
|
|
3329
|
-
function validateMapExpression(source, options = {}) {
|
|
3330
|
-
return compileMapExpression(source, options).validation;
|
|
3331
|
-
}
|
|
3332
|
-
/**
|
|
3333
|
-
* The single parse + substitute + compile pass shared by validation and
|
|
3334
|
-
* preview evaluation, so a preview never compiles the same source twice.
|
|
3335
|
-
* `expression` is set only when the validation succeeded on a non-empty
|
|
3336
|
-
* source.
|
|
3337
|
-
*/
|
|
3338
|
-
function compileMapExpression(source, options) {
|
|
3339
|
-
const trimmed = source.trim();
|
|
3340
|
-
if (!trimmed) return { validation: {
|
|
3341
|
-
ok: true,
|
|
3342
|
-
errors: []
|
|
3343
|
-
} };
|
|
3344
|
-
let parsed;
|
|
3345
|
-
try {
|
|
3346
|
-
parsed = JSON.parse(removeTrailingJsonCommas(trimmed));
|
|
3347
|
-
} catch (error) {
|
|
3348
|
-
return { validation: {
|
|
3349
|
-
ok: false,
|
|
3350
|
-
code: "not-json",
|
|
3351
|
-
errors: [error instanceof Error ? error.message : "Invalid JSON"]
|
|
3352
|
-
} };
|
|
3353
|
-
}
|
|
3354
|
-
if (!Array.isArray(parsed)) return { validation: {
|
|
3355
|
-
ok: false,
|
|
3356
|
-
code: "not-array",
|
|
3357
|
-
errors: []
|
|
3358
|
-
} };
|
|
3359
|
-
if (typeof parsed[0] !== "string") return { validation: {
|
|
3360
|
-
ok: false,
|
|
3361
|
-
code: "not-operator",
|
|
3362
|
-
errors: [],
|
|
3363
|
-
parsed
|
|
3364
|
-
} };
|
|
3365
|
-
const substituted = substituteExpressionVariables(parsed, options.variables ?? []);
|
|
3366
|
-
const compiled = createExpression(substituted, EXPRESSION_ROOT_KEY, options.expectedType ? propertySpecFor(options.expectedType) : void 0);
|
|
3367
|
-
if (compiled.result === "error") return { validation: {
|
|
3368
|
-
ok: false,
|
|
3369
|
-
code: "compile",
|
|
3370
|
-
errors: compiled.value.map((issue) => issue.key ? `${issue.key}: ${issue.message}` : issue.message),
|
|
3371
|
-
parsed
|
|
3372
|
-
} };
|
|
3373
|
-
return {
|
|
3374
|
-
validation: {
|
|
3375
|
-
ok: true,
|
|
3376
|
-
errors: [],
|
|
3377
|
-
parsed
|
|
3378
|
-
},
|
|
3379
|
-
expression: compiled.value
|
|
3380
|
-
};
|
|
3381
|
-
}
|
|
3382
|
-
/**
|
|
3383
|
-
* Replaces every string node that exactly matches a known `@token` with that
|
|
3384
|
-
* variable's value, returning a new structure (the input is not mutated).
|
|
3385
|
-
* Unknown `@` strings are left untouched so field values that happen to start
|
|
3386
|
-
* with `@` never get mangled.
|
|
3387
|
-
*/
|
|
3388
|
-
function substituteExpressionVariables(node, variables) {
|
|
3389
|
-
if (typeof node === "string" && node.startsWith("@")) {
|
|
3390
|
-
const match = variables.find((variable) => variable.token === node);
|
|
3391
|
-
return match ? match.value : node;
|
|
3392
|
-
}
|
|
3393
|
-
if (Array.isArray(node)) return node.map((entry) => substituteExpressionVariables(entry, variables));
|
|
3394
|
-
if (node && typeof node === "object") return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, substituteExpressionVariables(value, variables)]));
|
|
3395
|
-
return node;
|
|
3396
|
-
}
|
|
3397
|
-
/**
|
|
3398
|
-
* Evaluates an expression source against a sample feature for the builder's
|
|
3399
|
-
* live preview. Variables are substituted first, then the expression is
|
|
3400
|
-
* compiled with the MapLibre style spec and evaluated with the given zoom.
|
|
3401
|
-
* Runtime failures (e.g. `to-color` on garbage) come back as errors instead
|
|
3402
|
-
* of throwing.
|
|
3403
|
-
*/
|
|
3404
|
-
function evaluateMapExpression(source, options = {}) {
|
|
3405
|
-
const trimmed = source.trim();
|
|
3406
|
-
if (!trimmed) return { kind: "empty" };
|
|
3407
|
-
const { validation, expression } = compileMapExpression(trimmed, {
|
|
3408
|
-
variables: options.variables,
|
|
3409
|
-
expectedType: options.expectedType
|
|
3410
|
-
});
|
|
3411
|
-
if (!validation.ok || !expression) return {
|
|
3412
|
-
kind: "error",
|
|
3413
|
-
errors: validation.errors
|
|
3414
|
-
};
|
|
3415
|
-
const feature = options.feature ?? null;
|
|
3416
|
-
const geometryType = feature?.geometry?.type ?? "Unknown";
|
|
3417
|
-
try {
|
|
3418
|
-
return {
|
|
3419
|
-
kind: "value",
|
|
3420
|
-
value: expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {
|
|
3421
|
-
type: geometryType,
|
|
3422
|
-
properties: feature?.properties ?? {},
|
|
3423
|
-
...feature?.id !== void 0 ? { id: feature.id } : {},
|
|
3424
|
-
geometry: feature?.geometry
|
|
3425
|
-
})
|
|
3426
|
-
};
|
|
3427
|
-
} catch (error) {
|
|
3428
|
-
return {
|
|
3429
|
-
kind: "error",
|
|
3430
|
-
errors: [error instanceof Error ? error.message : String(error)]
|
|
3431
|
-
};
|
|
3432
|
-
}
|
|
3433
|
-
}
|
|
3434
|
-
/**
|
|
3435
|
-
* Compiles an expression source once for bulk per-feature evaluation (virtual
|
|
3436
|
-
* fields, and any other caller that runs one expression over a whole layer).
|
|
3437
|
-
* Unlike {@link evaluateMapExpression} the compile cost is paid once, not per
|
|
3438
|
-
* feature. An empty source is reported as a failure with no errors — there is
|
|
3439
|
-
* nothing to evaluate.
|
|
3440
|
-
*/
|
|
3441
|
-
function compileFeatureExpression(source, options = {}) {
|
|
3442
|
-
const trimmed = source.trim();
|
|
3443
|
-
if (!trimmed) return {
|
|
3444
|
-
ok: false,
|
|
3445
|
-
errors: []
|
|
3446
|
-
};
|
|
3447
|
-
const { validation, expression } = compileMapExpression(trimmed, {
|
|
3448
|
-
variables: options.variables,
|
|
3449
|
-
expectedType: options.expectedType
|
|
3450
|
-
});
|
|
3451
|
-
if (!validation.ok || !expression) return {
|
|
3452
|
-
ok: false,
|
|
3453
|
-
errors: validation.errors
|
|
3454
|
-
};
|
|
3455
|
-
const compiledZoom = options.zoom ?? 0;
|
|
3456
|
-
return {
|
|
3457
|
-
ok: true,
|
|
3458
|
-
errors: [],
|
|
3459
|
-
evaluate: (feature, zoom = compiledZoom) => expression.evaluateWithoutErrorHandling({ zoom }, {
|
|
3460
|
-
type: feature.geometry?.type ?? "Unknown",
|
|
3461
|
-
properties: feature.properties ?? {},
|
|
3462
|
-
...feature.id !== void 0 ? { id: feature.id } : {},
|
|
3463
|
-
geometry: feature.geometry
|
|
3464
|
-
})
|
|
3465
|
-
};
|
|
3466
|
-
}
|
|
3467
|
-
/**
|
|
3468
|
-
* Evaluates a boolean expression against every feature and returns the
|
|
3469
|
-
* selection ids of the matches (Select by Expression). Unlike
|
|
3470
|
-
* {@link evaluateMapExpression} this compiles the source once and reuses the
|
|
3471
|
-
* compiled expression across the whole array, so it stays fast on large
|
|
3472
|
-
* layers. An empty source is reported as `ok: false` with no errors — there
|
|
3473
|
-
* is nothing to select by.
|
|
3474
|
-
*/
|
|
3475
|
-
function matchFeaturesByExpression(features, source, options = {}) {
|
|
3476
|
-
const trimmed = source.trim();
|
|
3477
|
-
if (!trimmed) return {
|
|
3478
|
-
ok: false,
|
|
3479
|
-
errors: [],
|
|
3480
|
-
ids: [],
|
|
3481
|
-
errorCount: 0
|
|
3482
|
-
};
|
|
3483
|
-
const { validation, expression } = compileMapExpression(trimmed, {
|
|
3484
|
-
variables: options.variables,
|
|
3485
|
-
expectedType: "boolean"
|
|
3486
|
-
});
|
|
3487
|
-
if (!validation.ok || !expression) return {
|
|
3488
|
-
ok: false,
|
|
3489
|
-
errors: validation.errors,
|
|
3490
|
-
ids: [],
|
|
3491
|
-
errorCount: 0
|
|
3492
|
-
};
|
|
3493
|
-
const ids = [];
|
|
3494
|
-
let errorCount = 0;
|
|
3495
|
-
features.forEach((feature, index) => {
|
|
3496
|
-
try {
|
|
3497
|
-
if (expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {
|
|
3498
|
-
type: feature.geometry?.type ?? "Unknown",
|
|
3499
|
-
properties: feature.properties ?? {},
|
|
3500
|
-
...feature.id !== void 0 ? { id: feature.id } : {},
|
|
3501
|
-
geometry: feature.geometry
|
|
3502
|
-
}) === true) ids.push(featureSelectionId(feature, index));
|
|
3503
|
-
} catch {
|
|
3504
|
-
errorCount += 1;
|
|
3505
|
-
}
|
|
2165
|
+
return null;
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* Error thrown when a Valhalla request returns a non-2xx response. Carries the
|
|
2169
|
+
* HTTP status so callers can branch on it (e.g. a 4xx rejection) without
|
|
2170
|
+
* pattern-matching the message string.
|
|
2171
|
+
*/
|
|
2172
|
+
var RoutingRequestError = class extends Error {
|
|
2173
|
+
status;
|
|
2174
|
+
constructor(status, statusText) {
|
|
2175
|
+
super(`Routing request failed (${status} ${statusText})`);
|
|
2176
|
+
this.name = "RoutingRequestError";
|
|
2177
|
+
this.status = status;
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
async function postJson(url, body, signal) {
|
|
2181
|
+
const response = await fetch(url, {
|
|
2182
|
+
method: "POST",
|
|
2183
|
+
headers: { "Content-Type": "application/json" },
|
|
2184
|
+
body: JSON.stringify(body),
|
|
2185
|
+
signal
|
|
3506
2186
|
});
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
errors: [],
|
|
3510
|
-
ids,
|
|
3511
|
-
errorCount
|
|
3512
|
-
};
|
|
2187
|
+
if (!response.ok) throw new RoutingRequestError(response.status, response.statusText);
|
|
2188
|
+
return response.json();
|
|
3513
2189
|
}
|
|
3514
2190
|
/**
|
|
3515
|
-
*
|
|
3516
|
-
*
|
|
3517
|
-
*
|
|
3518
|
-
*
|
|
3519
|
-
*
|
|
3520
|
-
*
|
|
2191
|
+
* Requests an isochrone from the Valhalla server.
|
|
2192
|
+
*
|
|
2193
|
+
* @param endpoint - The Valhalla base URL.
|
|
2194
|
+
* @param body - The request body from {@link buildIsochroneRequest}.
|
|
2195
|
+
* @param signal - Optional abort signal.
|
|
2196
|
+
* @returns The Valhalla GeoJSON response.
|
|
3521
2197
|
*/
|
|
3522
|
-
function
|
|
3523
|
-
return
|
|
2198
|
+
function requestIsochrone(endpoint, body, signal) {
|
|
2199
|
+
return postJson(`${stripTrailingSlash(endpoint)}/isochrone`, body, signal);
|
|
3524
2200
|
}
|
|
3525
2201
|
/**
|
|
3526
|
-
*
|
|
3527
|
-
*
|
|
3528
|
-
*
|
|
2202
|
+
* Requests an OD cost matrix from the Valhalla server.
|
|
2203
|
+
*
|
|
2204
|
+
* @param endpoint - The Valhalla base URL.
|
|
2205
|
+
* @param body - The request body from {@link buildMatrixRequest}.
|
|
2206
|
+
* @param signal - Optional abort signal.
|
|
2207
|
+
* @returns The Valhalla matrix response.
|
|
3529
2208
|
*/
|
|
3530
|
-
function
|
|
3531
|
-
|
|
3532
|
-
if (isStyleSpecColor(value)) {
|
|
3533
|
-
const color = value;
|
|
3534
|
-
const alpha = color.a;
|
|
3535
|
-
const channel = (component) => Math.round((alpha === 0 ? 0 : component / alpha) * 255);
|
|
3536
|
-
return `rgba(${channel(color.r)}, ${channel(color.g)}, ${channel(color.b)}, ${Number(alpha.toFixed(3))})`;
|
|
3537
|
-
}
|
|
3538
|
-
try {
|
|
3539
|
-
return JSON.stringify(value) ?? String(value);
|
|
3540
|
-
} catch {
|
|
3541
|
-
return String(value);
|
|
3542
|
-
}
|
|
2209
|
+
function requestMatrix(endpoint, body, signal) {
|
|
2210
|
+
return postJson(`${stripTrailingSlash(endpoint)}/sources_to_targets`, body, signal);
|
|
3543
2211
|
}
|
|
3544
2212
|
/**
|
|
3545
|
-
*
|
|
3546
|
-
*
|
|
3547
|
-
*
|
|
2213
|
+
* Requests a route through ordered waypoints from the Valhalla server.
|
|
2214
|
+
*
|
|
2215
|
+
* @param endpoint - The Valhalla base URL.
|
|
2216
|
+
* @param body - The request body from {@link buildRouteRequest}.
|
|
2217
|
+
* @param signal - Optional abort signal.
|
|
2218
|
+
* @returns The Valhalla `/route` response.
|
|
3548
2219
|
*/
|
|
3549
|
-
function
|
|
3550
|
-
|
|
3551
|
-
for (const name of names) result[name] = "unknown";
|
|
3552
|
-
for (const feature of features) {
|
|
3553
|
-
const properties = feature.properties;
|
|
3554
|
-
if (!properties) continue;
|
|
3555
|
-
for (const name of names) {
|
|
3556
|
-
const value = properties[name];
|
|
3557
|
-
if (value === null || value === void 0) continue;
|
|
3558
|
-
const valueType = typeof value;
|
|
3559
|
-
const fieldType = valueType === "string" || valueType === "number" || valueType === "boolean" ? valueType : "mixed";
|
|
3560
|
-
if (result[name] === "unknown") result[name] = fieldType;
|
|
3561
|
-
else if (result[name] !== fieldType) result[name] = "mixed";
|
|
3562
|
-
}
|
|
3563
|
-
}
|
|
3564
|
-
return result;
|
|
2220
|
+
function requestRoute(endpoint, body, signal) {
|
|
2221
|
+
return postJson(`${stripTrailingSlash(endpoint)}/route`, body, signal);
|
|
3565
2222
|
}
|
|
3566
2223
|
//#endregion
|
|
3567
2224
|
//#region src/vector-color.ts
|
|
@@ -4895,6 +3552,429 @@ function featureForEvaluation(properties, feature) {
|
|
|
4895
3552
|
};
|
|
4896
3553
|
}
|
|
4897
3554
|
//#endregion
|
|
3555
|
+
//#region src/layer-descriptive-metadata.ts
|
|
3556
|
+
/** STAC specification version the Item export targets. */
|
|
3557
|
+
const STAC_VERSION = "1.0.0";
|
|
3558
|
+
/** Schema URL of the STAC processing extension, declared for `processing:lineage`. */
|
|
3559
|
+
const STAC_PROCESSING_EXTENSION = "https://stac-extensions.github.io/processing/v1.1.0/schema.json";
|
|
3560
|
+
/** URL schemes a metadata link (or asset) may use. */
|
|
3561
|
+
const ALLOWED_LINK_PROTOCOLS = /* @__PURE__ */ new Set([
|
|
3562
|
+
"http:",
|
|
3563
|
+
"https:",
|
|
3564
|
+
"ftp:",
|
|
3565
|
+
"s3:",
|
|
3566
|
+
"gs:",
|
|
3567
|
+
"mailto:"
|
|
3568
|
+
]);
|
|
3569
|
+
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3570
|
+
const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
3571
|
+
const DATE_TIME_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?$/i;
|
|
3572
|
+
/** The STAC 1.0 `license` pattern (an SPDX identifier, `various` or `proprietary`). */
|
|
3573
|
+
const STAC_LICENSE_PATTERN = /^[\w\-.+]+$/;
|
|
3574
|
+
/**
|
|
3575
|
+
* Trim a value to a non-empty string, or `undefined`.
|
|
3576
|
+
*
|
|
3577
|
+
* @param value - Any value.
|
|
3578
|
+
* @returns The trimmed string, or `undefined` for a non-string or blank value.
|
|
3579
|
+
*/
|
|
3580
|
+
function cleanString(value) {
|
|
3581
|
+
if (typeof value !== "string") return void 0;
|
|
3582
|
+
const trimmed = value.trim();
|
|
3583
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Split comma-separated keyword text into a clean keyword list.
|
|
3587
|
+
*
|
|
3588
|
+
* @param text - Text such as `"roads, transport, osm"`.
|
|
3589
|
+
* @returns The trimmed, non-empty keywords with case-insensitive duplicates removed.
|
|
3590
|
+
*/
|
|
3591
|
+
function parseMetadataKeywords(text) {
|
|
3592
|
+
return normalizeKeywords(text.split(","));
|
|
3593
|
+
}
|
|
3594
|
+
function normalizeKeywords(value) {
|
|
3595
|
+
if (!Array.isArray(value)) return [];
|
|
3596
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3597
|
+
const keywords = [];
|
|
3598
|
+
for (const entry of value) {
|
|
3599
|
+
const keyword = cleanString(entry);
|
|
3600
|
+
if (!keyword) continue;
|
|
3601
|
+
const key = keyword.toLowerCase();
|
|
3602
|
+
if (seen.has(key)) continue;
|
|
3603
|
+
seen.add(key);
|
|
3604
|
+
keywords.push(keyword);
|
|
3605
|
+
}
|
|
3606
|
+
return keywords;
|
|
3607
|
+
}
|
|
3608
|
+
function normalizeContact(value) {
|
|
3609
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3610
|
+
const raw = value;
|
|
3611
|
+
const contact = {};
|
|
3612
|
+
const name = cleanString(raw.name);
|
|
3613
|
+
const email = cleanString(raw.email);
|
|
3614
|
+
const organization = cleanString(raw.organization);
|
|
3615
|
+
if (name) contact.name = name;
|
|
3616
|
+
if (email) contact.email = email;
|
|
3617
|
+
if (organization) contact.organization = organization;
|
|
3618
|
+
return Object.keys(contact).length > 0 ? contact : void 0;
|
|
3619
|
+
}
|
|
3620
|
+
function normalizeTemporalExtent(value) {
|
|
3621
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3622
|
+
const raw = value;
|
|
3623
|
+
const extent = {};
|
|
3624
|
+
const start = cleanString(raw.start);
|
|
3625
|
+
const end = cleanString(raw.end);
|
|
3626
|
+
if (start) extent.start = start;
|
|
3627
|
+
if (end) extent.end = end;
|
|
3628
|
+
return Object.keys(extent).length > 0 ? extent : void 0;
|
|
3629
|
+
}
|
|
3630
|
+
function normalizeLinks(value) {
|
|
3631
|
+
if (!Array.isArray(value)) return [];
|
|
3632
|
+
const links = [];
|
|
3633
|
+
for (const entry of value) {
|
|
3634
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
3635
|
+
const raw = entry;
|
|
3636
|
+
const href = cleanString(raw.href);
|
|
3637
|
+
if (!href) continue;
|
|
3638
|
+
const link = { href };
|
|
3639
|
+
const rel = cleanString(raw.rel);
|
|
3640
|
+
const title = cleanString(raw.title);
|
|
3641
|
+
if (rel) link.rel = rel;
|
|
3642
|
+
if (title) link.title = title;
|
|
3643
|
+
links.push(link);
|
|
3644
|
+
}
|
|
3645
|
+
return links;
|
|
3646
|
+
}
|
|
3647
|
+
/**
|
|
3648
|
+
* Normalize a raw descriptive-metadata value (from a project file, a plugin or
|
|
3649
|
+
* the Metadata dialog): keep only string fields, trim them, and drop empty
|
|
3650
|
+
* fields, empty keywords and links without an address.
|
|
3651
|
+
*
|
|
3652
|
+
* Values are not validated here — a malformed email or date in a hand-edited
|
|
3653
|
+
* file is kept so the dialog can show and fix it rather than silently lose it.
|
|
3654
|
+
*
|
|
3655
|
+
* @param value - Any value.
|
|
3656
|
+
* @returns The cleaned record, or `undefined` when nothing is left, so an
|
|
3657
|
+
* empty record is never written to a project.
|
|
3658
|
+
*/
|
|
3659
|
+
function normalizeLayerDescriptiveMetadata(value) {
|
|
3660
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
3661
|
+
const raw = value;
|
|
3662
|
+
const metadata = {};
|
|
3663
|
+
for (const key of [
|
|
3664
|
+
"title",
|
|
3665
|
+
"abstract",
|
|
3666
|
+
"license",
|
|
3667
|
+
"attribution",
|
|
3668
|
+
"lineage"
|
|
3669
|
+
]) {
|
|
3670
|
+
const text = cleanString(raw[key]);
|
|
3671
|
+
if (text) metadata[key] = text;
|
|
3672
|
+
}
|
|
3673
|
+
const keywords = normalizeKeywords(raw.keywords);
|
|
3674
|
+
if (keywords.length > 0) metadata.keywords = keywords;
|
|
3675
|
+
const contact = normalizeContact(raw.contact);
|
|
3676
|
+
if (contact) metadata.contact = contact;
|
|
3677
|
+
const temporalExtent = normalizeTemporalExtent(raw.temporalExtent);
|
|
3678
|
+
if (temporalExtent) metadata.temporalExtent = temporalExtent;
|
|
3679
|
+
const links = normalizeLinks(raw.links);
|
|
3680
|
+
if (links.length > 0) metadata.links = links;
|
|
3681
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
3682
|
+
}
|
|
3683
|
+
/**
|
|
3684
|
+
* Whether a string is a plausible email address (`local@domain.tld`).
|
|
3685
|
+
*
|
|
3686
|
+
* @param value - The candidate address.
|
|
3687
|
+
* @returns `true` when it has the shape of an email address.
|
|
3688
|
+
*/
|
|
3689
|
+
function isValidMetadataEmail(value) {
|
|
3690
|
+
return EMAIL_PATTERN.test(value.trim());
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* Whether a string is an absolute URL with a scheme a metadata link may use
|
|
3694
|
+
* (http, https, ftp, s3, gs, mailto). Script and data URLs are rejected.
|
|
3695
|
+
*
|
|
3696
|
+
* @param value - The candidate URL.
|
|
3697
|
+
* @returns `true` when it parses as an allowed absolute URL.
|
|
3698
|
+
*/
|
|
3699
|
+
function isValidMetadataUrl(value) {
|
|
3700
|
+
try {
|
|
3701
|
+
const url = new URL(value.trim());
|
|
3702
|
+
return ALLOWED_LINK_PROTOCOLS.has(url.protocol);
|
|
3703
|
+
} catch {
|
|
3704
|
+
return false;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
/**
|
|
3708
|
+
* Whether the calendar fields name a real day (rejecting e.g. February 30).
|
|
3709
|
+
*
|
|
3710
|
+
* @param year - Four-digit year.
|
|
3711
|
+
* @param month - Month, 1-12.
|
|
3712
|
+
* @param day - Day of month.
|
|
3713
|
+
* @returns `true` when the date exists.
|
|
3714
|
+
*/
|
|
3715
|
+
function isRealDate(year, month, day) {
|
|
3716
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
3717
|
+
date.setUTCFullYear(year, month - 1, day);
|
|
3718
|
+
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
|
3719
|
+
}
|
|
3720
|
+
/**
|
|
3721
|
+
* Whether a string is an ISO 8601 date (`YYYY-MM-DD`) or date-time
|
|
3722
|
+
* (`YYYY-MM-DDTHH:MM[:SS[.fff]][Z|±HH:MM]`) naming a real instant.
|
|
3723
|
+
*
|
|
3724
|
+
* @param value - The candidate date.
|
|
3725
|
+
* @returns `true` when it is a valid ISO 8601 date or date-time.
|
|
3726
|
+
*/
|
|
3727
|
+
function isValidMetadataDate(value) {
|
|
3728
|
+
const text = value.trim();
|
|
3729
|
+
const dateOnly = DATE_ONLY_PATTERN.exec(text);
|
|
3730
|
+
if (dateOnly) return isRealDate(Number(dateOnly[1]), Number(dateOnly[2]), Number(dateOnly[3]));
|
|
3731
|
+
const dateTime = DATE_TIME_PATTERN.exec(text);
|
|
3732
|
+
if (!dateTime) return false;
|
|
3733
|
+
if (!isRealDate(Number(dateTime[1]), Number(dateTime[2]), Number(dateTime[3]))) return false;
|
|
3734
|
+
const hours = Number(dateTime[4]);
|
|
3735
|
+
const minutes = Number(dateTime[5]);
|
|
3736
|
+
const seconds = dateTime[6] === void 0 ? 0 : Number(dateTime[6]);
|
|
3737
|
+
return hours < 24 && minutes < 60 && seconds < 60;
|
|
3738
|
+
}
|
|
3739
|
+
/**
|
|
3740
|
+
* Convert a validated metadata date to an RFC 3339 UTC date-time, as STAC
|
|
3741
|
+
* requires. A date-only bound covers its whole day: a start becomes the first
|
|
3742
|
+
* second of the day, an end the last. A date-time without an offset is read
|
|
3743
|
+
* as UTC so the export does not depend on the exporting machine's time zone.
|
|
3744
|
+
*
|
|
3745
|
+
* @param value - A date accepted by {@link isValidMetadataDate}.
|
|
3746
|
+
* @param bound - Whether the value starts or ends an interval.
|
|
3747
|
+
* @returns The RFC 3339 date-time, or `null` for an invalid date.
|
|
3748
|
+
*/
|
|
3749
|
+
function metadataDateToRfc3339(value, bound) {
|
|
3750
|
+
const text = value.trim();
|
|
3751
|
+
if (!isValidMetadataDate(text)) return null;
|
|
3752
|
+
if (DATE_ONLY_PATTERN.test(text)) return `${text}T${bound === "start" ? "00:00:00" : "23:59:59"}Z`;
|
|
3753
|
+
const normalized = text.toUpperCase().replace(" ", "T").replace(/([+-]\d{2})(\d{2})$/, "$1:$2");
|
|
3754
|
+
const hasOffset = /(Z|[+-]\d{2}:\d{2})$/.test(normalized);
|
|
3755
|
+
const parsed = new Date(hasOffset ? normalized : `${normalized}Z`);
|
|
3756
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Validate the fields of a descriptive-metadata record that have a format:
|
|
3760
|
+
* the contact email, the temporal-extent dates (and their order), and the
|
|
3761
|
+
* link URLs. Free-text fields are not checked.
|
|
3762
|
+
*
|
|
3763
|
+
* @param metadata - The record to check (normalized or a dialog draft).
|
|
3764
|
+
* @returns Every failure found; empty when the record is valid.
|
|
3765
|
+
*/
|
|
3766
|
+
function validateLayerDescriptiveMetadata(metadata) {
|
|
3767
|
+
const issues = [];
|
|
3768
|
+
if (!metadata) return issues;
|
|
3769
|
+
const email = cleanString(metadata.contact?.email);
|
|
3770
|
+
if (email && !isValidMetadataEmail(email)) issues.push({
|
|
3771
|
+
field: "contact.email",
|
|
3772
|
+
code: "email"
|
|
3773
|
+
});
|
|
3774
|
+
const start = cleanString(metadata.temporalExtent?.start);
|
|
3775
|
+
const end = cleanString(metadata.temporalExtent?.end);
|
|
3776
|
+
const startValid = start ? isValidMetadataDate(start) : true;
|
|
3777
|
+
const endValid = end ? isValidMetadataDate(end) : true;
|
|
3778
|
+
if (!startValid) issues.push({
|
|
3779
|
+
field: "temporalExtent.start",
|
|
3780
|
+
code: "date"
|
|
3781
|
+
});
|
|
3782
|
+
if (!endValid) issues.push({
|
|
3783
|
+
field: "temporalExtent.end",
|
|
3784
|
+
code: "date"
|
|
3785
|
+
});
|
|
3786
|
+
if (start && end && startValid && endValid) {
|
|
3787
|
+
if (Date.parse(metadataDateToRfc3339(start, "start") ?? "") > Date.parse(metadataDateToRfc3339(end, "end") ?? "")) issues.push({
|
|
3788
|
+
field: "temporalExtent.end",
|
|
3789
|
+
code: "dateOrder"
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
(metadata.links ?? []).forEach((link, index) => {
|
|
3793
|
+
const href = cleanString(link?.href);
|
|
3794
|
+
if (href && !isValidMetadataUrl(href)) issues.push({
|
|
3795
|
+
field: `links.${index}.href`,
|
|
3796
|
+
code: "url"
|
|
3797
|
+
});
|
|
3798
|
+
});
|
|
3799
|
+
return issues;
|
|
3800
|
+
}
|
|
3801
|
+
/**
|
|
3802
|
+
* Validate a WGS84 bounding box.
|
|
3803
|
+
*
|
|
3804
|
+
* @param bbox - Candidate `[west, south, east, north]`.
|
|
3805
|
+
* @returns The box, or `null` when it is not four finite in-range numbers.
|
|
3806
|
+
*/
|
|
3807
|
+
function validBbox(bbox) {
|
|
3808
|
+
if (!bbox || bbox.length !== 4) return null;
|
|
3809
|
+
if (!bbox.every((value) => typeof value === "number" && Number.isFinite(value))) return null;
|
|
3810
|
+
const [west, south, east, north] = bbox;
|
|
3811
|
+
if (south < -90 || north > 90 || south > north) return null;
|
|
3812
|
+
if (west < -180 || west > 180 || east < -180 || east > 180) return null;
|
|
3813
|
+
return [
|
|
3814
|
+
west,
|
|
3815
|
+
south,
|
|
3816
|
+
east,
|
|
3817
|
+
north
|
|
3818
|
+
];
|
|
3819
|
+
}
|
|
3820
|
+
/**
|
|
3821
|
+
* The GeoJSON geometry covering a bounding box: a Point for a degenerate box,
|
|
3822
|
+
* a MultiPolygon split at the antimeridian when `west > east` (the STAC
|
|
3823
|
+
* convention for a box crossing it), otherwise a Polygon.
|
|
3824
|
+
*
|
|
3825
|
+
* @param bbox - A box accepted by {@link validBbox}.
|
|
3826
|
+
* @returns The footprint geometry.
|
|
3827
|
+
*/
|
|
3828
|
+
function bboxGeometry([west, south, east, north]) {
|
|
3829
|
+
if (west === east && south === north) return {
|
|
3830
|
+
type: "Point",
|
|
3831
|
+
coordinates: [west, south]
|
|
3832
|
+
};
|
|
3833
|
+
if (west === east || south === north) return {
|
|
3834
|
+
type: "LineString",
|
|
3835
|
+
coordinates: [[west, south], [east, north]]
|
|
3836
|
+
};
|
|
3837
|
+
const ring = (w, e) => [
|
|
3838
|
+
[w, south],
|
|
3839
|
+
[e, south],
|
|
3840
|
+
[e, north],
|
|
3841
|
+
[w, north],
|
|
3842
|
+
[w, south]
|
|
3843
|
+
];
|
|
3844
|
+
if (west > east) return {
|
|
3845
|
+
type: "MultiPolygon",
|
|
3846
|
+
coordinates: [[ring(west, 180)], [ring(-180, east)]]
|
|
3847
|
+
};
|
|
3848
|
+
return {
|
|
3849
|
+
type: "Polygon",
|
|
3850
|
+
coordinates: [ring(west, east)]
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
/** Media types by data-file extension, for the Item's data asset. */
|
|
3854
|
+
const ASSET_MEDIA_TYPES = {
|
|
3855
|
+
geojson: "application/geo+json",
|
|
3856
|
+
json: "application/json",
|
|
3857
|
+
tif: "image/tiff; application=geotiff",
|
|
3858
|
+
tiff: "image/tiff; application=geotiff",
|
|
3859
|
+
parquet: "application/vnd.apache.parquet",
|
|
3860
|
+
geoparquet: "application/vnd.apache.parquet",
|
|
3861
|
+
fgb: "application/vnd.flatgeobuf",
|
|
3862
|
+
pmtiles: "application/vnd.pmtiles",
|
|
3863
|
+
gpkg: "application/geopackage+sqlite3",
|
|
3864
|
+
kml: "application/vnd.google-earth.kml+xml",
|
|
3865
|
+
kmz: "application/vnd.google-earth.kmz",
|
|
3866
|
+
csv: "text/csv",
|
|
3867
|
+
zip: "application/zip",
|
|
3868
|
+
las: "application/vnd.las",
|
|
3869
|
+
laz: "application/vnd.laszip",
|
|
3870
|
+
zarr: "application/vnd+zarr"
|
|
3871
|
+
};
|
|
3872
|
+
/**
|
|
3873
|
+
* Guess the media type of a layer's data asset from the URL's extension, or
|
|
3874
|
+
* the layer type when the URL has none (a COG is flagged cloud-optimized).
|
|
3875
|
+
*
|
|
3876
|
+
* @param href - The asset URL.
|
|
3877
|
+
* @param layerType - The layer's type.
|
|
3878
|
+
* @returns The media type, or `undefined` when it cannot be told.
|
|
3879
|
+
*/
|
|
3880
|
+
function stacAssetMediaType(href, layerType) {
|
|
3881
|
+
let path = href;
|
|
3882
|
+
try {
|
|
3883
|
+
path = new URL(href).pathname;
|
|
3884
|
+
} catch {}
|
|
3885
|
+
const extension = /\.([a-z0-9]+)$/i.exec(path.replace(/\/+$/, ""))?.[1]?.toLowerCase();
|
|
3886
|
+
if (layerType === "cog" && (extension === "tif" || extension === "tiff" || !extension)) return "image/tiff; application=geotiff; profile=cloud-optimized";
|
|
3887
|
+
if (extension && ASSET_MEDIA_TYPES[extension]) return ASSET_MEDIA_TYPES[extension];
|
|
3888
|
+
if (layerType === "geojson" && !extension) return "application/geo+json";
|
|
3889
|
+
}
|
|
3890
|
+
/**
|
|
3891
|
+
* Build a STAC 1.0 Item describing a layer from its descriptive metadata.
|
|
3892
|
+
*
|
|
3893
|
+
* Mapping: `id` is the layer id; `geometry`/`bbox` come from the WGS84 extent
|
|
3894
|
+
* (a `null` geometry, as STAC allows, when the extent is unknown); `title`
|
|
3895
|
+
* falls back to the layer name; `abstract` becomes `description`; the contact
|
|
3896
|
+
* becomes a `producer` provider; `lineage` becomes `processing:lineage` (with
|
|
3897
|
+
* the processing extension declared); the temporal extent becomes `datetime`
|
|
3898
|
+
* or `start_datetime`/`end_datetime` (with `datetime` set to the export time
|
|
3899
|
+
* when none is recorded, since STAC requires one); links keep their `rel`
|
|
3900
|
+
* (default `related`); and a source URL becomes the `data` asset.
|
|
3901
|
+
*
|
|
3902
|
+
* A license that is not an SPDX-style identifier (STAC 1.0's pattern) is
|
|
3903
|
+
* exported as `proprietary`, with the original text kept in
|
|
3904
|
+
* `geolibre:license`; the attribution is kept in `geolibre:attribution`.
|
|
3905
|
+
*
|
|
3906
|
+
* @param layer - The layer to describe.
|
|
3907
|
+
* @param options - Extent, asset URL and clock; see {@link LayerStacItemOptions}.
|
|
3908
|
+
* @returns The STAC Item, ready to `JSON.stringify`.
|
|
3909
|
+
*/
|
|
3910
|
+
function buildLayerStacItem(layer, options = {}) {
|
|
3911
|
+
const metadata = normalizeLayerDescriptiveMetadata(layer.descriptiveMetadata) ?? {};
|
|
3912
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
3913
|
+
const properties = { datetime: null };
|
|
3914
|
+
properties.title = metadata.title ?? layer.name;
|
|
3915
|
+
if (metadata.abstract) properties.description = metadata.abstract;
|
|
3916
|
+
if (metadata.keywords?.length) properties.keywords = [...metadata.keywords];
|
|
3917
|
+
const start = metadata.temporalExtent?.start ? metadataDateToRfc3339(metadata.temporalExtent.start, "start") : null;
|
|
3918
|
+
const end = metadata.temporalExtent?.end ? metadataDateToRfc3339(metadata.temporalExtent.end, "end") : null;
|
|
3919
|
+
if (start && end && Date.parse(start) !== Date.parse(end)) {
|
|
3920
|
+
properties.start_datetime = start;
|
|
3921
|
+
properties.end_datetime = end;
|
|
3922
|
+
} else properties.datetime = start ?? end ?? now.toISOString();
|
|
3923
|
+
if (metadata.license) {
|
|
3924
|
+
if (STAC_LICENSE_PATTERN.test(metadata.license)) properties.license = metadata.license;
|
|
3925
|
+
else {
|
|
3926
|
+
properties.license = "proprietary";
|
|
3927
|
+
properties["geolibre:license"] = metadata.license;
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
const contact = metadata.contact;
|
|
3931
|
+
const providerName = contact?.organization ?? contact?.name ?? contact?.email;
|
|
3932
|
+
if (contact && providerName) {
|
|
3933
|
+
const person = [contact.organization && contact.name ? contact.name : void 0, contact.email].filter(Boolean);
|
|
3934
|
+
const provider = {
|
|
3935
|
+
name: providerName,
|
|
3936
|
+
roles: ["producer"]
|
|
3937
|
+
};
|
|
3938
|
+
if (person.length > 0) provider.description = `Contact: ${person.join(", ")}`;
|
|
3939
|
+
properties.providers = [provider];
|
|
3940
|
+
}
|
|
3941
|
+
if (metadata.attribution) properties["geolibre:attribution"] = metadata.attribution;
|
|
3942
|
+
const extensions = [];
|
|
3943
|
+
if (metadata.lineage) {
|
|
3944
|
+
properties["processing:lineage"] = metadata.lineage;
|
|
3945
|
+
extensions.push(STAC_PROCESSING_EXTENSION);
|
|
3946
|
+
}
|
|
3947
|
+
const links = (metadata.links ?? []).filter((link) => isValidMetadataUrl(link.href)).map((link) => ({
|
|
3948
|
+
href: link.href,
|
|
3949
|
+
rel: link.rel ?? "related",
|
|
3950
|
+
...link.title ? { title: link.title } : {}
|
|
3951
|
+
}));
|
|
3952
|
+
const assets = {};
|
|
3953
|
+
const assetHref = cleanString(options.assetHref);
|
|
3954
|
+
if (assetHref) {
|
|
3955
|
+
const templated = /\{[a-z]+\}/i.test(assetHref);
|
|
3956
|
+
const type = templated ? void 0 : stacAssetMediaType(assetHref, layer.type);
|
|
3957
|
+
assets[templated ? "tiles" : "data"] = {
|
|
3958
|
+
href: assetHref,
|
|
3959
|
+
...type ? { type } : {},
|
|
3960
|
+
title: properties.title,
|
|
3961
|
+
roles: [templated ? "tiles" : "data"]
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
const bbox = validBbox(options.bbox);
|
|
3965
|
+
return {
|
|
3966
|
+
type: "Feature",
|
|
3967
|
+
stac_version: STAC_VERSION,
|
|
3968
|
+
stac_extensions: extensions,
|
|
3969
|
+
id: layer.id,
|
|
3970
|
+
geometry: bbox ? bboxGeometry(bbox) : null,
|
|
3971
|
+
...bbox ? { bbox } : {},
|
|
3972
|
+
properties,
|
|
3973
|
+
links,
|
|
3974
|
+
assets
|
|
3975
|
+
};
|
|
3976
|
+
}
|
|
3977
|
+
//#endregion
|
|
4898
3978
|
//#region src/popup.ts
|
|
4899
3979
|
/** Match an inline base64 raster image (excludes SVG, which can carry scripts). */
|
|
4900
3980
|
const INLINE_IMAGE_DATA_URL = /^data:image\/(?!svg)[\w.+-]+;base64,/i;
|
|
@@ -5201,515 +4281,42 @@ function resolveConfiguredPopupTitle(properties, popup, options = {}) {
|
|
|
5201
4281
|
const text = stringifyPopupValue(evaluatePopupExpression(source, properties, options)).trim();
|
|
5202
4282
|
if (text) return text;
|
|
5203
4283
|
}
|
|
5204
|
-
const field = trimmedString(popup?.titleField);
|
|
5205
|
-
if (field && !options.fieldVisibility?.[field] && !isInternalPopupField(field)) {
|
|
5206
|
-
const text = stringifyPopupValue(properties[field]).trim();
|
|
5207
|
-
if (text) return text;
|
|
5208
|
-
}
|
|
5209
|
-
return null;
|
|
5210
|
-
}
|
|
5211
|
-
/**
|
|
5212
|
-
* The popup body text when the author supplied a body expression, or `null`
|
|
5213
|
-
* when there is none (or it failed) and the field rows should render instead.
|
|
5214
|
-
*/
|
|
5215
|
-
function resolvePopupBody(properties, popup, options = {}) {
|
|
5216
|
-
const source = trimmedString(popup?.bodyExpression);
|
|
5217
|
-
if (!source) return null;
|
|
5218
|
-
const preview = evaluatePopupExpression(source, properties, options);
|
|
5219
|
-
if (preview === void 0) return null;
|
|
5220
|
-
const text = stringifyPopupValue(preview);
|
|
5221
|
-
return text.trim() ? text : null;
|
|
5222
|
-
}
|
|
5223
|
-
/**
|
|
5224
|
-
* Whether a value should render as an inline image thumbnail under `"auto"`.
|
|
5225
|
-
* Kept here so the renderer and the designer's preview agree on what counts.
|
|
5226
|
-
*/
|
|
5227
|
-
function isInlineImageValue(value) {
|
|
5228
|
-
return typeof value === "string" && INLINE_IMAGE_DATA_URL.test(value);
|
|
5229
|
-
}
|
|
5230
|
-
/**
|
|
5231
|
-
* Whether a URL is safe to put in a popup `href`/`src`. Only http(s) and
|
|
5232
|
-
* inline raster data URLs: a `javascript:` or `data:text/html` value coming
|
|
5233
|
-
* from an untrusted GeoJSON must never become a live link.
|
|
5234
|
-
*/
|
|
5235
|
-
function isSafePopupUrl(value, allowDataImage = false) {
|
|
5236
|
-
if (typeof value !== "string") return false;
|
|
5237
|
-
const trimmed = value.trim();
|
|
5238
|
-
if (/^https?:\/\//i.test(trimmed)) return true;
|
|
5239
|
-
return allowDataImage && INLINE_IMAGE_DATA_URL.test(trimmed);
|
|
5240
|
-
}
|
|
5241
|
-
//#endregion
|
|
5242
|
-
//#region src/virtual-fields.ts
|
|
5243
|
-
/**
|
|
5244
|
-
* Virtual fields (QGIS Field Calculator → "Create virtual field", issue
|
|
5245
|
-
* #1321): expression-backed columns that recompute live instead of being
|
|
5246
|
-
* written once as static values.
|
|
5247
|
-
*
|
|
5248
|
-
* The engine mirrors the persistent-joins engine (`joins.ts`): computed
|
|
5249
|
-
* values are materialized into feature properties, so every consumer of
|
|
5250
|
-
* attributes — the attribute table, Expression Builder, data-driven styling,
|
|
5251
|
-
* labels, selection, export — sees the column with no further wiring.
|
|
5252
|
-
* Idempotency uses the same bookkeeping: each applied field records the
|
|
5253
|
-
* column it actually added (`addedField`), applying always strips those
|
|
5254
|
-
* first, and an existing column always wins a name collision (the virtual
|
|
5255
|
-
* field is skipped entirely), so stripping exactly restores the pre-apply
|
|
5256
|
-
* properties.
|
|
5257
|
-
*
|
|
5258
|
-
* Expressions are declarative MapLibre expressions compiled through the
|
|
5259
|
-
* style spec — never arbitrary code — so persisting them in `.geolibre.json`
|
|
5260
|
-
* and re-evaluating them on project load is safe (unlike the attribute
|
|
5261
|
-
* table's Field Calculator, whose JavaScript evaluator must never run
|
|
5262
|
-
* persisted expressions). Fields apply in list order and each sees the
|
|
5263
|
-
* columns materialized by earlier fields, so a virtual field can build on
|
|
5264
|
-
* another. Zoom-dependent expressions evaluate at zoom 0 and `@` variables
|
|
5265
|
-
* are not substituted: a virtual field is a property of the data, not of the
|
|
5266
|
-
* current view.
|
|
5267
|
-
*
|
|
5268
|
-
* Known trade-off, shared with the joins engine (see `joins.ts`): when a
|
|
5269
|
-
* layer's data is replaced wholesale, the replacement is stripped with the
|
|
5270
|
-
* *previous* bookkeeping. That is required because a replacement can also be
|
|
5271
|
-
* a write-back of the derived output (the attribute table round-trips
|
|
5272
|
-
* `layer.geojson`), which must not freeze a stale computed value into base
|
|
5273
|
-
* data — but it means a replacement dataset's own column whose name matches a
|
|
5274
|
-
* previously-materialized virtual column is replaced by the expression's
|
|
5275
|
-
* value for that column. Renaming or removing the virtual field first keeps
|
|
5276
|
-
* such collisions from arising.
|
|
5277
|
-
*/
|
|
5278
|
-
/**
|
|
5279
|
-
* Remove every column previously added by `fields` (per their `addedField`
|
|
5280
|
-
* bookkeeping) from a copy of `features`, restoring the pre-apply properties.
|
|
5281
|
-
* Features without any tracked column are returned unchanged (same
|
|
5282
|
-
* reference).
|
|
5283
|
-
*/
|
|
5284
|
-
function stripVirtualFieldColumns(features, fields) {
|
|
5285
|
-
const tracked = /* @__PURE__ */ new Set();
|
|
5286
|
-
for (const field of fields ?? []) if (field.addedField) tracked.add(field.addedField);
|
|
5287
|
-
if (tracked.size === 0) return features;
|
|
5288
|
-
return features.map((feature) => {
|
|
5289
|
-
const props = feature.properties;
|
|
5290
|
-
if (!props) return feature;
|
|
5291
|
-
let hasTracked = false;
|
|
5292
|
-
for (const key of tracked) if (key in props) {
|
|
5293
|
-
hasTracked = true;
|
|
5294
|
-
break;
|
|
5295
|
-
}
|
|
5296
|
-
if (!hasTracked) return feature;
|
|
5297
|
-
const next = {};
|
|
5298
|
-
for (const [key, value] of Object.entries(props)) if (!tracked.has(key)) next[key] = value;
|
|
5299
|
-
return {
|
|
5300
|
-
...feature,
|
|
5301
|
-
properties: next
|
|
5302
|
-
};
|
|
5303
|
-
});
|
|
5304
|
-
}
|
|
5305
|
-
/**
|
|
5306
|
-
* Normalize an evaluated expression value into a property value: `undefined`
|
|
5307
|
-
* and non-finite numbers become null (a clean empty cell), style-spec Color
|
|
5308
|
-
* instances become their rgba() string (a Color object would neither
|
|
5309
|
-
* serialize nor display usefully), everything else passes through.
|
|
5310
|
-
*/
|
|
5311
|
-
function normalizeComputedValue(value) {
|
|
5312
|
-
if (value === void 0) return null;
|
|
5313
|
-
if (typeof value === "number" && !Number.isFinite(value)) return null;
|
|
5314
|
-
if (isStyleSpecColor(value)) return formatExpressionPreviewValue(value);
|
|
5315
|
-
return value;
|
|
5316
|
-
}
|
|
5317
|
-
/**
|
|
5318
|
-
* Apply `fields` in order to base (already-stripped) `features`, returning
|
|
5319
|
-
* new feature objects with each computed column merged into their properties.
|
|
5320
|
-
*
|
|
5321
|
-
* Semantics per field: a disabled field, an empty/colliding output name, or
|
|
5322
|
-
* an expression that fails to compile contributes nothing (with the compile
|
|
5323
|
-
* error recorded on the field); a runtime evaluation failure nulls that one
|
|
5324
|
-
* feature's cell and increments the field's `errorCount` instead of aborting
|
|
5325
|
-
* the column. Fields evaluate sequentially, so a later field's expression
|
|
5326
|
-
* can `["get", ...]` an earlier field's column.
|
|
5327
|
-
*/
|
|
5328
|
-
function applyLayerVirtualFields(features, fields) {
|
|
5329
|
-
const fieldList = fields ?? [];
|
|
5330
|
-
const clearBookkeeping = (field) => ({
|
|
5331
|
-
...field,
|
|
5332
|
-
addedField: void 0,
|
|
5333
|
-
error: void 0,
|
|
5334
|
-
errorCount: void 0
|
|
5335
|
-
});
|
|
5336
|
-
if (fieldList.filter((field) => field.enabled !== false).length === 0) return {
|
|
5337
|
-
features,
|
|
5338
|
-
fields: fieldList.map(clearBookkeeping)
|
|
5339
|
-
};
|
|
5340
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
5341
|
-
for (const feature of features) for (const key of Object.keys(feature.properties ?? {})) usedNames.add(key);
|
|
5342
|
-
const out = features.map((feature) => ({
|
|
5343
|
-
...feature,
|
|
5344
|
-
properties: { ...feature.properties ?? {} }
|
|
5345
|
-
}));
|
|
5346
|
-
return {
|
|
5347
|
-
features: out,
|
|
5348
|
-
fields: fieldList.map((field) => {
|
|
5349
|
-
if (field.enabled === false) return clearBookkeeping(field);
|
|
5350
|
-
const name = field.name.trim();
|
|
5351
|
-
if (!name || usedNames.has(name)) return clearBookkeeping(field);
|
|
5352
|
-
const compiled = compileFeatureExpression(field.expression);
|
|
5353
|
-
if (!compiled.ok || !compiled.evaluate) return {
|
|
5354
|
-
...clearBookkeeping(field),
|
|
5355
|
-
error: compiled.errors[0] ?? "Invalid expression"
|
|
5356
|
-
};
|
|
5357
|
-
usedNames.add(name);
|
|
5358
|
-
let errorCount = 0;
|
|
5359
|
-
for (const feature of out) {
|
|
5360
|
-
let value = null;
|
|
5361
|
-
try {
|
|
5362
|
-
value = normalizeComputedValue(compiled.evaluate(feature));
|
|
5363
|
-
} catch {
|
|
5364
|
-
errorCount += 1;
|
|
5365
|
-
}
|
|
5366
|
-
feature.properties[name] = value;
|
|
5367
|
-
}
|
|
5368
|
-
return {
|
|
5369
|
-
...clearBookkeeping(field),
|
|
5370
|
-
addedField: name,
|
|
5371
|
-
...errorCount > 0 ? { errorCount } : {}
|
|
5372
|
-
};
|
|
5373
|
-
})
|
|
5374
|
-
};
|
|
5375
|
-
}
|
|
5376
|
-
//#endregion
|
|
5377
|
-
//#region src/joins.ts
|
|
5378
|
-
/**
|
|
5379
|
-
* Persistent attribute joins (QGIS Layer Properties → Joins), issue #1315.
|
|
5380
|
-
*
|
|
5381
|
-
* A layer's `joins` are live left joins that materialize columns from another
|
|
5382
|
-
* layer's attribute table into this layer's feature properties. Materializing
|
|
5383
|
-
* (rather than resolving lazily) means every consumer of attributes — the
|
|
5384
|
-
* attribute table, Expression Builder, data-driven styling, labels, diagrams,
|
|
5385
|
-
* export — sees the joined columns with no further wiring.
|
|
5386
|
-
*
|
|
5387
|
-
* Idempotency without a duplicate base copy: each applied join records the
|
|
5388
|
-
* output column names it added (`addedFields`), and applying joins always
|
|
5389
|
-
* strips those first. Base columns win every name collision (a joined column
|
|
5390
|
-
* whose output name already exists is skipped entirely), so stripping the
|
|
5391
|
-
* added columns exactly restores the pre-join properties.
|
|
5392
|
-
*
|
|
5393
|
-
* Known trade-off of that bookkeeping: when a layer's data is replaced
|
|
5394
|
-
* wholesale (file reload), the replacement is stripped with the *previous*
|
|
5395
|
-
* `addedFields`. That is required because a replacement can also be a
|
|
5396
|
-
* write-back of the joined output (the attribute table round-trips
|
|
5397
|
-
* `layer.geojson`), which must not freeze stale joined values into base data —
|
|
5398
|
-
* but it means a reloaded base column whose name collides with a
|
|
5399
|
-
* previously-joined output column is replaced by the join's value for that
|
|
5400
|
-
* column. A field-name prefix (the QGIS convention the UI suggests) keeps
|
|
5401
|
-
* such collisions from arising.
|
|
5402
|
-
*
|
|
5403
|
-
* The data is already in memory as JS feature objects, so the join is a plain
|
|
5404
|
-
* hash join here rather than the DuckDB-WASM SQL statement sketched in the
|
|
5405
|
-
* issue — the result is identical for equality keys and the engine stays
|
|
5406
|
-
* synchronous and dependency-free. Key semantics deliberately mirror the
|
|
5407
|
-
* Processing → Vector attribute join (and the sidecar's `_attribute_join_key`):
|
|
5408
|
-
* empty values never match, and non-empty values compare stringified.
|
|
5409
|
-
*/
|
|
5410
|
-
/** Canonical JSON for object values (sorted keys), matching the processing engine. */
|
|
5411
|
-
function stableStringify(value) {
|
|
5412
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
5413
|
-
if (value !== null && typeof value === "object") return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
5414
|
-
return JSON.stringify(value) ?? "null";
|
|
5415
|
-
}
|
|
5416
|
-
function valueToString(value) {
|
|
5417
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
5418
|
-
if (value !== null && typeof value === "object") return stableStringify(value);
|
|
5419
|
-
return String(value);
|
|
5420
|
-
}
|
|
5421
|
-
/**
|
|
5422
|
-
* Match key for a persistent join: empty values (null/undefined/NaN/empty
|
|
5423
|
-
* string) never match a row, mirroring a SQL/pandas NaN join key. Non-empty
|
|
5424
|
-
* values are keyed by their string form, so a numeric `5` and the string `"5"`
|
|
5425
|
-
* join while a zero-padded code like `"01001"` only matches another `"01001"`.
|
|
5426
|
-
* Kept in sync with the Processing attribute join's `attributeJoinKey`.
|
|
5427
|
-
*/
|
|
5428
|
-
function layerJoinKey(value) {
|
|
5429
|
-
return value === null || value === void 0 || typeof value === "number" && Number.isNaN(value) || valueToString(value) === "" ? null : valueToString(value);
|
|
5430
|
-
}
|
|
5431
|
-
/** GeoJSON columns are own properties; inherited object members are not data. */
|
|
5432
|
-
function fieldValue(properties, field) {
|
|
5433
|
-
return properties && Object.hasOwn(properties, field) ? properties[field] : void 0;
|
|
5434
|
-
}
|
|
5435
|
-
/**
|
|
5436
|
-
* Remove every column previously added by `joins` (per their `addedFields`
|
|
5437
|
-
* bookkeeping) from a copy of `features`, restoring the pre-join properties.
|
|
5438
|
-
* Features without any tracked column are returned unchanged (same reference).
|
|
5439
|
-
*/
|
|
5440
|
-
function stripJoinFields(features, joins) {
|
|
5441
|
-
const tracked = /* @__PURE__ */ new Set();
|
|
5442
|
-
for (const join of joins ?? []) for (const field of join.addedFields ?? []) tracked.add(field);
|
|
5443
|
-
if (tracked.size === 0) return features;
|
|
5444
|
-
return features.map((feature) => {
|
|
5445
|
-
const props = feature.properties;
|
|
5446
|
-
if (!props) return feature;
|
|
5447
|
-
let hasTracked = false;
|
|
5448
|
-
for (const key of tracked) if (key in props) {
|
|
5449
|
-
hasTracked = true;
|
|
5450
|
-
break;
|
|
5451
|
-
}
|
|
5452
|
-
if (!hasTracked) return feature;
|
|
5453
|
-
const next = {};
|
|
5454
|
-
for (const [key, value] of Object.entries(props)) if (!tracked.has(key)) next[key] = value;
|
|
5455
|
-
return {
|
|
5456
|
-
...feature,
|
|
5457
|
-
properties: next
|
|
5458
|
-
};
|
|
5459
|
-
});
|
|
5460
|
-
}
|
|
5461
|
-
/**
|
|
5462
|
-
* Apply `joins` in order to base (already-stripped) `features`, returning new
|
|
5463
|
-
* feature objects with the joined columns merged into their properties.
|
|
5464
|
-
*
|
|
5465
|
-
* Semantics per join: left join, first matching join row wins, and joined
|
|
5466
|
-
* columns are null-filled on unmatched features so the schema stays
|
|
5467
|
-
* consistent. An output name (prefix + field) that collides with an existing
|
|
5468
|
-
* column — from the base data or an earlier join — drops that one column;
|
|
5469
|
-
* the join's other columns still apply. A disabled join, or one whose source
|
|
5470
|
-
* cannot be resolved, contributes nothing and gets empty `addedFields` and no
|
|
5471
|
-
* stats.
|
|
5472
|
-
*
|
|
5473
|
-
* @param features - The layer's base features (strip previous joins first).
|
|
5474
|
-
* @param joins - Join definitions in application order.
|
|
5475
|
-
* @param resolveSource - Maps a join's `joinLayerId` to that layer's current
|
|
5476
|
-
* feature collection, or `undefined` when the layer is gone (or is the
|
|
5477
|
-
* target itself; self-joins are refused by the caller).
|
|
5478
|
-
*/
|
|
5479
|
-
function applyLayerJoins(features, joins, resolveSource) {
|
|
5480
|
-
const joinList = joins ?? [];
|
|
5481
|
-
const resolved = /* @__PURE__ */ new Map();
|
|
5482
|
-
const resolveOnce = (joinLayerId) => {
|
|
5483
|
-
if (!resolved.has(joinLayerId)) resolved.set(joinLayerId, resolveSource(joinLayerId));
|
|
5484
|
-
return resolved.get(joinLayerId);
|
|
5485
|
-
};
|
|
5486
|
-
if (joinList.filter((join) => join.enabled !== false && resolveOnce(join.joinLayerId)).length === 0) return {
|
|
5487
|
-
features,
|
|
5488
|
-
joins: joinList.map((join) => ({
|
|
5489
|
-
...join,
|
|
5490
|
-
addedFields: [],
|
|
5491
|
-
stats: void 0
|
|
5492
|
-
}))
|
|
5493
|
-
};
|
|
5494
|
-
const out = features.map((feature) => ({
|
|
5495
|
-
...feature,
|
|
5496
|
-
properties: { ...feature.properties ?? {} }
|
|
5497
|
-
}));
|
|
5498
|
-
const usedNames = /* @__PURE__ */ new Set();
|
|
5499
|
-
for (const feature of features) for (const key of Object.keys(feature.properties ?? {})) usedNames.add(key);
|
|
5500
|
-
return {
|
|
5501
|
-
features: out,
|
|
5502
|
-
joins: joinList.map((join) => {
|
|
5503
|
-
const source = join.enabled === false ? void 0 : resolveOnce(join.joinLayerId);
|
|
5504
|
-
if (!source) return {
|
|
5505
|
-
...join,
|
|
5506
|
-
addedFields: [],
|
|
5507
|
-
stats: void 0
|
|
5508
|
-
};
|
|
5509
|
-
const joinFeatures = (source.features ?? []).filter(Boolean);
|
|
5510
|
-
const joinKeysOrder = [];
|
|
5511
|
-
const joinKeySet = /* @__PURE__ */ new Set();
|
|
5512
|
-
for (const jf of joinFeatures) for (const key of Object.keys(jf.properties ?? {})) if (!joinKeySet.has(key)) {
|
|
5513
|
-
joinKeySet.add(key);
|
|
5514
|
-
joinKeysOrder.push(key);
|
|
5515
|
-
}
|
|
5516
|
-
const requested = join.fields !== void 0 ? join.fields.filter((field) => joinKeySet.has(field)) : joinKeysOrder.filter((key) => key !== join.joinField);
|
|
5517
|
-
const prefix = join.prefix ?? "";
|
|
5518
|
-
const fieldPairs = [];
|
|
5519
|
-
for (const field of requested) {
|
|
5520
|
-
const outputName = prefix + field;
|
|
5521
|
-
if (usedNames.has(outputName)) continue;
|
|
5522
|
-
usedNames.add(outputName);
|
|
5523
|
-
fieldPairs.push([field, outputName]);
|
|
5524
|
-
}
|
|
5525
|
-
const lookup = /* @__PURE__ */ new Map();
|
|
5526
|
-
for (const jf of joinFeatures) {
|
|
5527
|
-
const key = layerJoinKey(fieldValue(jf.properties, join.joinField));
|
|
5528
|
-
if (key === null || lookup.has(key)) continue;
|
|
5529
|
-
lookup.set(key, jf.properties ?? {});
|
|
5530
|
-
}
|
|
5531
|
-
let matched = 0;
|
|
5532
|
-
const targetKeys = /* @__PURE__ */ new Set();
|
|
5533
|
-
for (const feature of out) {
|
|
5534
|
-
const props = feature.properties;
|
|
5535
|
-
const key = layerJoinKey(fieldValue(props, join.targetField));
|
|
5536
|
-
if (key !== null) targetKeys.add(key);
|
|
5537
|
-
const row = key === null ? void 0 : lookup.get(key);
|
|
5538
|
-
if (row) {
|
|
5539
|
-
matched += 1;
|
|
5540
|
-
for (const [field, outputName] of fieldPairs) props[outputName] = fieldValue(row, field) ?? null;
|
|
5541
|
-
} else for (const [, outputName] of fieldPairs) props[outputName] = null;
|
|
5542
|
-
}
|
|
5543
|
-
let unmatchedJoin = 0;
|
|
5544
|
-
for (const jf of joinFeatures) {
|
|
5545
|
-
const key = layerJoinKey(fieldValue(jf.properties, join.joinField));
|
|
5546
|
-
if (key !== null && !targetKeys.has(key)) unmatchedJoin += 1;
|
|
5547
|
-
}
|
|
5548
|
-
return {
|
|
5549
|
-
...join,
|
|
5550
|
-
addedFields: fieldPairs.map(([, outputName]) => outputName),
|
|
5551
|
-
stats: {
|
|
5552
|
-
matchedCount: matched,
|
|
5553
|
-
unmatchedTargetCount: out.length - matched,
|
|
5554
|
-
unmatchedJoinCount: unmatchedJoin
|
|
5555
|
-
}
|
|
5556
|
-
};
|
|
5557
|
-
})
|
|
5558
|
-
};
|
|
4284
|
+
const field = trimmedString(popup?.titleField);
|
|
4285
|
+
if (field && !options.fieldVisibility?.[field] && !isInternalPopupField(field)) {
|
|
4286
|
+
const text = stringifyPopupValue(properties[field]).trim();
|
|
4287
|
+
if (text) return text;
|
|
4288
|
+
}
|
|
4289
|
+
return null;
|
|
5559
4290
|
}
|
|
5560
4291
|
/**
|
|
5561
|
-
*
|
|
5562
|
-
*
|
|
5563
|
-
* sources from `allLayers`. Pass `nextJoins`/`nextVirtualFields` to replace
|
|
5564
|
-
* the definitions (the layer's current bookkeeping still drives the strip, so
|
|
5565
|
-
* removed definitions clean up after themselves); omit them to refresh the
|
|
5566
|
-
* existing definitions against current data. Layers without a feature
|
|
5567
|
-
* collection are returned unchanged.
|
|
4292
|
+
* The popup body text when the author supplied a body expression, or `null`
|
|
4293
|
+
* when there is none (or it failed) and the field rows should render instead.
|
|
5568
4294
|
*/
|
|
5569
|
-
function
|
|
5570
|
-
const
|
|
5571
|
-
|
|
5572
|
-
const
|
|
5573
|
-
if (
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
virtualFields: virtualFields.length > 0 ? virtualFields : void 0
|
|
5577
|
-
};
|
|
5578
|
-
const applied = applyLayerJoins(stripJoinFields(stripVirtualFieldColumns(geojson.features ?? [], layer.virtualFields), layer.joins), joins, (joinLayerId) => {
|
|
5579
|
-
if (joinLayerId === layer.id) return void 0;
|
|
5580
|
-
return allLayers.find((candidate) => candidate.id === joinLayerId)?.geojson;
|
|
5581
|
-
});
|
|
5582
|
-
const withVirtual = applyLayerVirtualFields(applied.features, virtualFields);
|
|
5583
|
-
return {
|
|
5584
|
-
...layer,
|
|
5585
|
-
geojson: {
|
|
5586
|
-
...geojson,
|
|
5587
|
-
features: withVirtual.features
|
|
5588
|
-
},
|
|
5589
|
-
joins: applied.joins.length > 0 ? applied.joins : void 0,
|
|
5590
|
-
virtualFields: withVirtual.fields.length > 0 ? withVirtual.fields : void 0
|
|
5591
|
-
};
|
|
4295
|
+
function resolvePopupBody(properties, popup, options = {}) {
|
|
4296
|
+
const source = trimmedString(popup?.bodyExpression);
|
|
4297
|
+
if (!source) return null;
|
|
4298
|
+
const preview = evaluatePopupExpression(source, properties, options);
|
|
4299
|
+
if (preview === void 0) return null;
|
|
4300
|
+
const text = stringifyPopupValue(preview);
|
|
4301
|
+
return text.trim() ? text : null;
|
|
5592
4302
|
}
|
|
5593
4303
|
/**
|
|
5594
|
-
*
|
|
5595
|
-
*
|
|
5596
|
-
* Disabled joins count too: re-enabling one must not spring a cycle. The Joins
|
|
5597
|
-
* UI uses this to keep a layer that already consumes the target — however
|
|
5598
|
-
* indirectly — out of the join-source picker, so circular joins cannot be
|
|
5599
|
-
* authored (the refresh cascade still breaks them defensively for hand-edited
|
|
5600
|
-
* projects).
|
|
4304
|
+
* Whether a value should render as an inline image thumbnail under `"auto"`.
|
|
4305
|
+
* Kept here so the renderer and the designer's preview agree on what counts.
|
|
5601
4306
|
*/
|
|
5602
|
-
function
|
|
5603
|
-
|
|
5604
|
-
const visited = /* @__PURE__ */ new Set();
|
|
5605
|
-
const queue = [layerId];
|
|
5606
|
-
while (queue.length > 0) {
|
|
5607
|
-
const current = byId.get(queue.shift());
|
|
5608
|
-
for (const join of current?.joins ?? []) {
|
|
5609
|
-
if (visited.has(join.joinLayerId)) continue;
|
|
5610
|
-
visited.add(join.joinLayerId);
|
|
5611
|
-
queue.push(join.joinLayerId);
|
|
5612
|
-
}
|
|
5613
|
-
}
|
|
5614
|
-
return visited;
|
|
5615
|
-
}
|
|
5616
|
-
/**
|
|
5617
|
-
* Order `candidates` (layers with joins) so every layer comes after the
|
|
5618
|
-
* candidates its joins consume — Kahn's algorithm over "join source → join
|
|
5619
|
-
* target" edges within the candidate set. When a cycle blocks progress
|
|
5620
|
-
* (possible only in hand-edited projects; the UI refuses circular joins), an
|
|
5621
|
-
* actual cycle member is forced through — found by walking unmet-source edges
|
|
5622
|
-
* until a node repeats — so consumers *downstream* of the cycle still order
|
|
5623
|
-
* after it rather than falling back ahead of it.
|
|
5624
|
-
*/
|
|
5625
|
-
function topologicalJoinOrder(candidates) {
|
|
5626
|
-
const candidateIds = new Set(candidates.map((layer) => layer.id));
|
|
5627
|
-
const indegree = /* @__PURE__ */ new Map();
|
|
5628
|
-
const dependents = /* @__PURE__ */ new Map();
|
|
5629
|
-
const sourcesOf = /* @__PURE__ */ new Map();
|
|
5630
|
-
for (const layer of candidates) indegree.set(layer.id, 0);
|
|
5631
|
-
for (const layer of candidates) for (const join of layer.joins ?? []) {
|
|
5632
|
-
if (join.enabled === false || join.joinLayerId === layer.id || !candidateIds.has(join.joinLayerId)) continue;
|
|
5633
|
-
indegree.set(layer.id, (indegree.get(layer.id) ?? 0) + 1);
|
|
5634
|
-
const list = dependents.get(join.joinLayerId) ?? [];
|
|
5635
|
-
list.push(layer.id);
|
|
5636
|
-
dependents.set(join.joinLayerId, list);
|
|
5637
|
-
const sources = sourcesOf.get(layer.id) ?? [];
|
|
5638
|
-
sources.push(join.joinLayerId);
|
|
5639
|
-
sourcesOf.set(layer.id, sources);
|
|
5640
|
-
}
|
|
5641
|
-
const order = [];
|
|
5642
|
-
const orderedSet = /* @__PURE__ */ new Set();
|
|
5643
|
-
const queue = candidates.filter((layer) => indegree.get(layer.id) === 0).map((layer) => layer.id);
|
|
5644
|
-
while (order.length < candidates.length) {
|
|
5645
|
-
if (queue.length === 0) {
|
|
5646
|
-
const start = candidates.find((layer) => !orderedSet.has(layer.id));
|
|
5647
|
-
if (!start) break;
|
|
5648
|
-
let cursor = start.id;
|
|
5649
|
-
const walked = /* @__PURE__ */ new Set();
|
|
5650
|
-
while (!walked.has(cursor)) {
|
|
5651
|
-
walked.add(cursor);
|
|
5652
|
-
const next = (sourcesOf.get(cursor) ?? []).find((source) => !orderedSet.has(source));
|
|
5653
|
-
if (next === void 0) break;
|
|
5654
|
-
cursor = next;
|
|
5655
|
-
}
|
|
5656
|
-
queue.push(cursor);
|
|
5657
|
-
}
|
|
5658
|
-
const id = queue.shift();
|
|
5659
|
-
if (orderedSet.has(id)) continue;
|
|
5660
|
-
orderedSet.add(id);
|
|
5661
|
-
order.push(id);
|
|
5662
|
-
for (const dependent of dependents.get(id) ?? []) {
|
|
5663
|
-
const remaining = (indegree.get(dependent) ?? 1) - 1;
|
|
5664
|
-
indegree.set(dependent, remaining);
|
|
5665
|
-
if (remaining === 0 && !orderedSet.has(dependent)) queue.push(dependent);
|
|
5666
|
-
}
|
|
5667
|
-
}
|
|
5668
|
-
return order;
|
|
5669
|
-
}
|
|
5670
|
-
/**
|
|
5671
|
-
* Refresh every layer whose joins (directly or transitively) consume the
|
|
5672
|
-
* layer identified by `seedId`, after that layer's data changed — or after it
|
|
5673
|
-
* was removed (a gone source resolves to nothing, so its frozen columns strip
|
|
5674
|
-
* away). The affected layers re-derive in topological dependency order, so a
|
|
5675
|
-
* layer joining two other affected layers sees both of them fresh regardless
|
|
5676
|
-
* of their positions in the layer panel. The seed itself is not touched.
|
|
5677
|
-
*/
|
|
5678
|
-
function cascadeLayerJoinRefresh(layers, seedId) {
|
|
5679
|
-
const affected = /* @__PURE__ */ new Set();
|
|
5680
|
-
const queue = [seedId];
|
|
5681
|
-
while (queue.length > 0) {
|
|
5682
|
-
const sourceId = queue.shift();
|
|
5683
|
-
for (const layer of layers) {
|
|
5684
|
-
if (layer.id === seedId || affected.has(layer.id)) continue;
|
|
5685
|
-
if (layer.joins?.some((join) => join.enabled !== false && join.joinLayerId === sourceId)) {
|
|
5686
|
-
affected.add(layer.id);
|
|
5687
|
-
queue.push(layer.id);
|
|
5688
|
-
}
|
|
5689
|
-
}
|
|
5690
|
-
}
|
|
5691
|
-
if (affected.size === 0) return layers;
|
|
5692
|
-
let current = layers;
|
|
5693
|
-
const order = topologicalJoinOrder(layers.filter((layer) => affected.has(layer.id)));
|
|
5694
|
-
for (const id of order) current = current.map((layer) => layer.id === id ? applyJoinsToLayer(layer, current) : layer);
|
|
5695
|
-
return current;
|
|
4307
|
+
function isInlineImageValue(value) {
|
|
4308
|
+
return typeof value === "string" && INLINE_IMAGE_DATA_URL.test(value);
|
|
5696
4309
|
}
|
|
5697
4310
|
/**
|
|
5698
|
-
*
|
|
5699
|
-
*
|
|
5700
|
-
*
|
|
5701
|
-
* hand-edited cycle is broken by forcing one of its members through first
|
|
5702
|
-
* (see {@link topologicalJoinOrder}), so consumers downstream of the cycle
|
|
5703
|
-
* still re-derive after it. Layers with virtual fields but no joins re-derive
|
|
5704
|
-
* too (their expressions run against the freshly loaded features). Layer sets
|
|
5705
|
-
* without joins or virtual fields pass through by reference.
|
|
4311
|
+
* Whether a URL is safe to put in a popup `href`/`src`. Only http(s) and
|
|
4312
|
+
* inline raster data URLs: a `javascript:` or `data:text/html` value coming
|
|
4313
|
+
* from an untrusted GeoJSON must never become a live link.
|
|
5706
4314
|
*/
|
|
5707
|
-
function
|
|
5708
|
-
|
|
5709
|
-
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
return current;
|
|
4315
|
+
function isSafePopupUrl(value, allowDataImage = false) {
|
|
4316
|
+
if (typeof value !== "string") return false;
|
|
4317
|
+
const trimmed = value.trim();
|
|
4318
|
+
if (/^https?:\/\//i.test(trimmed)) return true;
|
|
4319
|
+
return allowDataImage && INLINE_IMAGE_DATA_URL.test(trimmed);
|
|
5713
4320
|
}
|
|
5714
4321
|
//#endregion
|
|
5715
4322
|
//#region src/quick-filters.ts
|
|
@@ -8567,7 +7174,7 @@ const RASTER_APPEARANCE_STATE_KEYS = [
|
|
|
8567
7174
|
"stretch",
|
|
8568
7175
|
"gamma"
|
|
8569
7176
|
];
|
|
8570
|
-
function isPlainObject$
|
|
7177
|
+
function isPlainObject$4(value) {
|
|
8571
7178
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8572
7179
|
}
|
|
8573
7180
|
/**
|
|
@@ -8604,11 +7211,11 @@ function extractCopiedLayerStyle(layer) {
|
|
|
8604
7211
|
};
|
|
8605
7212
|
const rasterState = layer.metadata.rasterState;
|
|
8606
7213
|
const rasterSymbology = layer.metadata.rasterSymbology;
|
|
8607
|
-
const hasRasterSymbology = isPlainObject$
|
|
7214
|
+
const hasRasterSymbology = isPlainObject$4(rasterSymbology);
|
|
8608
7215
|
return {
|
|
8609
7216
|
kind,
|
|
8610
7217
|
sourceName: layer.name,
|
|
8611
|
-
rasterState: isPlainObject$
|
|
7218
|
+
rasterState: isPlainObject$4(rasterState) ? structuredClone(rasterState) : void 0,
|
|
8612
7219
|
hasRasterSymbology,
|
|
8613
7220
|
...hasRasterSymbology ? { rasterSymbology: structuredClone(rasterSymbology) } : {}
|
|
8614
7221
|
};
|
|
@@ -8625,7 +7232,7 @@ function extractCopiedLayerStyle(layer) {
|
|
|
8625
7232
|
function applyCopiedLayerStyle(target, copied) {
|
|
8626
7233
|
if (copyableLayerStyleKind(target) !== copied.kind) return null;
|
|
8627
7234
|
if (copied.kind === "vector") return { style: structuredClone(copied.style) };
|
|
8628
|
-
const targetState = isPlainObject$
|
|
7235
|
+
const targetState = isPlainObject$4(target.metadata.rasterState) ? target.metadata.rasterState : {};
|
|
8629
7236
|
const source = copied.rasterState ?? {};
|
|
8630
7237
|
const expectedRescaleLength = rescaleEntryCount(targetState);
|
|
8631
7238
|
const mergedState = { ...targetState };
|
|
@@ -8657,7 +7264,7 @@ function rescaleEntryCount(state) {
|
|
|
8657
7264
|
const LAYER_STYLES_FILE_TYPE = "geolibre-layer-styles";
|
|
8658
7265
|
/** Format version written by {@link serializeLayerStylesFile}. */
|
|
8659
7266
|
const LAYER_STYLES_FILE_VERSION = 1;
|
|
8660
|
-
function isPlainObject$
|
|
7267
|
+
function isPlainObject$3(value) {
|
|
8661
7268
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8662
7269
|
}
|
|
8663
7270
|
/**
|
|
@@ -8683,7 +7290,7 @@ function extractLayerStyleEntries(layers) {
|
|
|
8683
7290
|
layerName: layer.name,
|
|
8684
7291
|
kind: "raster",
|
|
8685
7292
|
...copied.rasterState ? { rasterState: copied.rasterState } : {},
|
|
8686
|
-
...isPlainObject$
|
|
7293
|
+
...isPlainObject$3(copied.rasterSymbology) ? { rasterSymbology: copied.rasterSymbology } : {}
|
|
8687
7294
|
});
|
|
8688
7295
|
}
|
|
8689
7296
|
return entries;
|
|
@@ -8747,11 +7354,11 @@ function normalizeLayerStyleEntries(value) {
|
|
|
8747
7354
|
if (!Array.isArray(value)) return [];
|
|
8748
7355
|
const entries = [];
|
|
8749
7356
|
for (const item of value) {
|
|
8750
|
-
if (!isPlainObject$
|
|
7357
|
+
if (!isPlainObject$3(item)) continue;
|
|
8751
7358
|
const layerName = typeof item.layerName === "string" ? item.layerName : "";
|
|
8752
7359
|
if (!layerName.trim()) continue;
|
|
8753
7360
|
if (item.kind === "vector") {
|
|
8754
|
-
if (!isPlainObject$
|
|
7361
|
+
if (!isPlainObject$3(item.style)) continue;
|
|
8755
7362
|
entries.push({
|
|
8756
7363
|
layerName,
|
|
8757
7364
|
kind: "vector",
|
|
@@ -8763,8 +7370,8 @@ function normalizeLayerStyleEntries(value) {
|
|
|
8763
7370
|
} else if (item.kind === "raster") entries.push({
|
|
8764
7371
|
layerName,
|
|
8765
7372
|
kind: "raster",
|
|
8766
|
-
...isPlainObject$
|
|
8767
|
-
...isPlainObject$
|
|
7373
|
+
...isPlainObject$3(item.rasterState) ? { rasterState: sanitizeRasterAppearanceState(item.rasterState) } : {},
|
|
7374
|
+
...isPlainObject$3(item.rasterSymbology) ? { rasterSymbology: structuredClone(item.rasterSymbology) } : {}
|
|
8768
7375
|
});
|
|
8769
7376
|
}
|
|
8770
7377
|
return entries;
|
|
@@ -8784,7 +7391,7 @@ function parseLayerStylesFile(json) {
|
|
|
8784
7391
|
} catch {
|
|
8785
7392
|
throw new Error("Not a valid layer styles file (invalid JSON).");
|
|
8786
7393
|
}
|
|
8787
|
-
if (!isPlainObject$
|
|
7394
|
+
if (!isPlainObject$3(parsed) || parsed.type !== "geolibre-layer-styles") throw new Error("Not a valid layer styles file.");
|
|
8788
7395
|
if (parsed.version !== 1) throw new Error("Unsupported layer styles file version.");
|
|
8789
7396
|
const entries = normalizeLayerStyleEntries(parsed.styles);
|
|
8790
7397
|
if (entries.length === 0) throw new Error("The layer styles file holds no usable styles.");
|
|
@@ -8930,6 +7537,7 @@ const createLayersSlice = (set, get) => ({
|
|
|
8930
7537
|
}),
|
|
8931
7538
|
setLayerAttributeForm: (id, attributeForm) => get().updateLayer(id, { attributeForm }),
|
|
8932
7539
|
setLayerPopup: (id, popup) => get().updateLayer(id, { popup }),
|
|
7540
|
+
setLayerDescriptiveMetadata: (id, metadata) => get().updateLayer(id, { descriptiveMetadata: normalizeLayerDescriptiveMetadata(metadata) }),
|
|
8933
7541
|
setLayerEditorTracking: (id, editorTracking) => get().updateLayer(id, { editorTracking }),
|
|
8934
7542
|
setLayerVirtualFields: (id, fields) => set((s) => {
|
|
8935
7543
|
let layers = s.layers.map((l) => l.id === id ? applyJoinsToLayer(l, s.layers, void 0, fields) : l);
|
|
@@ -9707,6 +8315,16 @@ function isCredentialParam(name) {
|
|
|
9707
8315
|
const lowered = decoded.toLowerCase();
|
|
9708
8316
|
return URL_CREDENTIAL_PARAMS.has(normalizeCredentialName(lowered)) || lowered.startsWith("x-amz-");
|
|
9709
8317
|
}
|
|
8318
|
+
/**
|
|
8319
|
+
* Whether a URL query-parameter name carries a credential. This is the wider of
|
|
8320
|
+
* the two registries (see `URL_CREDENTIAL_PARAMS`). Exported so a caller that
|
|
8321
|
+
* must leave the parameter in place — the native diagnostics log rewrites its
|
|
8322
|
+
* value to `[redacted]` rather than dropping the pair — shares this registry
|
|
8323
|
+
* instead of keeping a second, narrower list that can drift from it.
|
|
8324
|
+
*/
|
|
8325
|
+
function isCredentialUrlParam(name) {
|
|
8326
|
+
return isCredentialParam(name);
|
|
8327
|
+
}
|
|
9710
8328
|
function redactParameterString(value) {
|
|
9711
8329
|
return value.split("&").filter((pair) => pair !== "" && !isCredentialParam(pair.split("=", 1)[0])).join("&");
|
|
9712
8330
|
}
|
|
@@ -9733,7 +8351,7 @@ function redactUrlCredentials(value) {
|
|
|
9733
8351
|
const keptFragment = fragment === void 0 || !fragment.includes("=") ? fragment : redactParameterString(fragment);
|
|
9734
8352
|
return base + (keptQuery ? `?${keptQuery}` : "") + (keptFragment ? `#${keptFragment}` : "");
|
|
9735
8353
|
}
|
|
9736
|
-
function isPlainObject$
|
|
8354
|
+
function isPlainObject$2(value) {
|
|
9737
8355
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9738
8356
|
}
|
|
9739
8357
|
function isGeoJsonPayload$1(value) {
|
|
@@ -9750,13 +8368,13 @@ function redactConfigurationValue(value, path, accumulator, depth = 0) {
|
|
|
9750
8368
|
return redacted;
|
|
9751
8369
|
}
|
|
9752
8370
|
if (Array.isArray(value)) return value.map((item, index) => redactConfigurationValue(item, `${path}[${index}]`, accumulator, depth + 1));
|
|
9753
|
-
if (!isPlainObject$
|
|
8371
|
+
if (!isPlainObject$2(value)) return value;
|
|
9754
8372
|
if (isGeoJsonPayload$1(value)) return structuredClone(value);
|
|
9755
8373
|
const result = {};
|
|
9756
8374
|
for (const [key, nested] of Object.entries(value)) {
|
|
9757
8375
|
const nestedPath = path ? `${path}.${key}` : key;
|
|
9758
8376
|
const normalizedKey = normalizeCredentialName(key);
|
|
9759
|
-
if ((normalizedKey === "requestheaders" || normalizedKey === "headers") && isPlainObject$
|
|
8377
|
+
if ((normalizedKey === "requestheaders" || normalizedKey === "headers") && isPlainObject$2(nested)) {
|
|
9760
8378
|
const kept = {};
|
|
9761
8379
|
const removed = {};
|
|
9762
8380
|
for (const [name, headerValue] of Object.entries(nested)) if (typeof headerValue === "string" && isHeaderReferenceOnly(headerValue)) kept[name] = headerValue;
|
|
@@ -9775,10 +8393,46 @@ function redactConfigurationValue(value, path, accumulator, depth = 0) {
|
|
|
9775
8393
|
}
|
|
9776
8394
|
function countLeafValues(value) {
|
|
9777
8395
|
if (Array.isArray(value)) return value.reduce((count, item) => count + countLeafValues(item), 0);
|
|
9778
|
-
if (isPlainObject$
|
|
8396
|
+
if (isPlainObject$2(value)) return Object.values(value).reduce((count, nested) => count + countLeafValues(nested), 0);
|
|
9779
8397
|
return 1;
|
|
9780
8398
|
}
|
|
9781
8399
|
/**
|
|
8400
|
+
* Settings keys that registry-listed external plugins declared publishable,
|
|
8401
|
+
* keyed by plugin id. `null` keeps the whole blob. Populated at runtime by the
|
|
8402
|
+
* host from the curated plugin registry, never from the project file itself,
|
|
8403
|
+
* so a project cannot vouch for its own state. The Python mirror in
|
|
8404
|
+
* `python/src/geolibre/project.py` only knows the static built-in list, so a
|
|
8405
|
+
* project redacted there still drops these plugins' state.
|
|
8406
|
+
*/
|
|
8407
|
+
const registryPublishableSettings = /* @__PURE__ */ new Map();
|
|
8408
|
+
/**
|
|
8409
|
+
* Replace the registry-declared publishable plugin settings.
|
|
8410
|
+
*
|
|
8411
|
+
* Built-in ids in {@link PUBLISHABLE_PLUGIN_SETTINGS} always win, so a registry
|
|
8412
|
+
* entry cannot widen a first-party plugin's allowlist.
|
|
8413
|
+
*
|
|
8414
|
+
* @param entries Plugin ids mapped to the settings keys to keep, or `null` to
|
|
8415
|
+
* keep the whole blob. Ids that declare nothing should be omitted.
|
|
8416
|
+
*/
|
|
8417
|
+
function setRegistryPublishableSettings(entries) {
|
|
8418
|
+
registryPublishableSettings.clear();
|
|
8419
|
+
for (const [id, keys] of entries) {
|
|
8420
|
+
if (Object.prototype.hasOwnProperty.call(PUBLISHABLE_PLUGIN_SETTINGS, id)) continue;
|
|
8421
|
+
registryPublishableSettings.set(id, keys);
|
|
8422
|
+
}
|
|
8423
|
+
}
|
|
8424
|
+
/**
|
|
8425
|
+
* Look up the publishable settings for a plugin id.
|
|
8426
|
+
*
|
|
8427
|
+
* @param id Plugin id.
|
|
8428
|
+
* @returns `undefined` when nothing is publishable, `null` to keep the whole
|
|
8429
|
+
* blob, otherwise the list of keys to keep.
|
|
8430
|
+
*/
|
|
8431
|
+
function publishableSettingsFor(id) {
|
|
8432
|
+
if (Object.prototype.hasOwnProperty.call(PUBLISHABLE_PLUGIN_SETTINGS, id)) return PUBLISHABLE_PLUGIN_SETTINGS[id];
|
|
8433
|
+
return registryPublishableSettings.get(id);
|
|
8434
|
+
}
|
|
8435
|
+
/**
|
|
9782
8436
|
* Return a detached project safe for any external egress.
|
|
9783
8437
|
*
|
|
9784
8438
|
* Environment variables and geocoder keys are always removed. Layer
|
|
@@ -9790,7 +8444,9 @@ function countLeafValues(value) {
|
|
|
9790
8444
|
* and carries no credentials, and dropping it stripped the legend, colorbar,
|
|
9791
8445
|
* and swipe from the exported map. Manifest URLs, activation, and control
|
|
9792
8446
|
* positions stay intact so recipients can still load and configure the plugin
|
|
9793
|
-
* themselves.
|
|
8447
|
+
* themselves. A registry-listed external plugin can opt in through the
|
|
8448
|
+
* `publishableSettings` field of its registry entry (see
|
|
8449
|
+
* {@link setRegistryPublishableSettings}); what it keeps is still swept below.
|
|
9794
8450
|
*/
|
|
9795
8451
|
function redactProjectCredentials(project) {
|
|
9796
8452
|
const accumulator = {
|
|
@@ -9854,7 +8510,7 @@ function redactProjectCredentials(project) {
|
|
|
9854
8510
|
const kept = {};
|
|
9855
8511
|
const dropped = {};
|
|
9856
8512
|
for (const [id, value] of Object.entries(settings)) {
|
|
9857
|
-
const allowed =
|
|
8513
|
+
const allowed = publishableSettingsFor(id);
|
|
9858
8514
|
if (allowed === void 0) {
|
|
9859
8515
|
dropped[id] = value;
|
|
9860
8516
|
continue;
|
|
@@ -9896,6 +8552,24 @@ function redactProjectCredentials(project) {
|
|
|
9896
8552
|
redactedCount: accumulator.count
|
|
9897
8553
|
};
|
|
9898
8554
|
}
|
|
8555
|
+
/**
|
|
8556
|
+
* Remove credentials from a single configuration value (a layer `source`,
|
|
8557
|
+
* `metadata`, or `sourcePath`) with the same rules the project redaction pass
|
|
8558
|
+
* applies to layer configuration: credential-named fields and non-reference
|
|
8559
|
+
* header values are dropped, and credential URL parameters are stripped from
|
|
8560
|
+
* every URL-shaped string.
|
|
8561
|
+
*
|
|
8562
|
+
* @param value - The configuration value to scrub. Not mutated.
|
|
8563
|
+
* @returns A detached, redacted copy.
|
|
8564
|
+
*/
|
|
8565
|
+
function redactConfigurationCredentials(value) {
|
|
8566
|
+
return redactConfigurationValue(value, "", {
|
|
8567
|
+
paths: [],
|
|
8568
|
+
fingerprints: [],
|
|
8569
|
+
count: 0,
|
|
8570
|
+
unfingerprintable: false
|
|
8571
|
+
});
|
|
8572
|
+
}
|
|
9899
8573
|
/** Convenience wrapper for callers that only need the safe project. */
|
|
9900
8574
|
function redactCredentials(project) {
|
|
9901
8575
|
return redactProjectCredentials(project).project;
|
|
@@ -10145,7 +8819,8 @@ function captureLayerLibraryEntry(layer, options) {
|
|
|
10145
8819
|
...layer.joins?.length ? { joins: structuredClone(layer.joins) } : {},
|
|
10146
8820
|
...layer.virtualFields?.length ? { virtualFields: structuredClone(layer.virtualFields) } : {},
|
|
10147
8821
|
...layer.attributeForm ? { attributeForm: structuredClone(layer.attributeForm) } : {},
|
|
10148
|
-
...layer.popup ? { popup: structuredClone(layer.popup) } : {}
|
|
8822
|
+
...layer.popup ? { popup: structuredClone(layer.popup) } : {},
|
|
8823
|
+
...layer.descriptiveMetadata ? { descriptiveMetadata: structuredClone(layer.descriptiveMetadata) } : {}
|
|
10149
8824
|
};
|
|
10150
8825
|
const withFeatures = embed && !controlPainted ? {
|
|
10151
8826
|
...base,
|
|
@@ -10202,7 +8877,8 @@ function planLayerLibraryAdd(entry, options) {
|
|
|
10202
8877
|
...entry.joins ? { joins: structuredClone(entry.joins) } : {},
|
|
10203
8878
|
...entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {},
|
|
10204
8879
|
...entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {},
|
|
10205
|
-
...entry.popup ? { popup: structuredClone(entry.popup) } : {}
|
|
8880
|
+
...entry.popup ? { popup: structuredClone(entry.popup) } : {},
|
|
8881
|
+
...entry.descriptiveMetadata ? { descriptiveMetadata: structuredClone(entry.descriptiveMetadata) } : {}
|
|
10206
8882
|
}
|
|
10207
8883
|
};
|
|
10208
8884
|
}
|
|
@@ -10221,7 +8897,8 @@ function layerLibraryConfigPatch(entry) {
|
|
|
10221
8897
|
...entry.joins ? { joins: structuredClone(entry.joins) } : {},
|
|
10222
8898
|
...entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {},
|
|
10223
8899
|
...entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {},
|
|
10224
|
-
...entry.popup ? { popup: structuredClone(entry.popup) } : {}
|
|
8900
|
+
...entry.popup ? { popup: structuredClone(entry.popup) } : {},
|
|
8901
|
+
...entry.descriptiveMetadata ? { descriptiveMetadata: structuredClone(entry.descriptiveMetadata) } : {}
|
|
10225
8902
|
};
|
|
10226
8903
|
}
|
|
10227
8904
|
/**
|
|
@@ -10391,11 +9068,13 @@ function normalizeLayerLibraryEntries(value) {
|
|
|
10391
9068
|
const virtualFields = validVirtualFields(candidate.virtualFields);
|
|
10392
9069
|
const attributeForm = validAttributeForm(candidate.attributeForm);
|
|
10393
9070
|
const popup = validPopup(candidate.popup);
|
|
9071
|
+
const descriptiveMetadata = normalizeLayerDescriptiveMetadata(candidate.descriptiveMetadata);
|
|
10394
9072
|
return {
|
|
10395
9073
|
...joins ? { joins } : {},
|
|
10396
9074
|
...virtualFields ? { virtualFields } : {},
|
|
10397
9075
|
...attributeForm ? { attributeForm } : {},
|
|
10398
|
-
...popup ? { popup } : {}
|
|
9076
|
+
...popup ? { popup } : {},
|
|
9077
|
+
...descriptiveMetadata ? { descriptiveMetadata } : {}
|
|
10399
9078
|
};
|
|
10400
9079
|
})(),
|
|
10401
9080
|
...geojson ? { geojson: structuredClone(geojson) } : {},
|
|
@@ -10771,11 +9450,58 @@ function createProjectLayerSerializationCache() {
|
|
|
10771
9450
|
* @returns The same text {@link serializeProject} returns for `project`.
|
|
10772
9451
|
*/
|
|
10773
9452
|
function serializeProjectWithLayerCache(project, layerSources, cache) {
|
|
9453
|
+
const steps = serializeProjectWithLayerCacheSteps(project, layerSources, cache);
|
|
9454
|
+
for (;;) {
|
|
9455
|
+
const step = steps.next();
|
|
9456
|
+
if (step.done) return step.value;
|
|
9457
|
+
}
|
|
9458
|
+
}
|
|
9459
|
+
/** Time a main-thread serialization slice may run before it yields (ms). */
|
|
9460
|
+
const SERIALIZE_SLICE_MS = 8;
|
|
9461
|
+
/** Resolve on a macrotask, letting input and paint run in between slices. */
|
|
9462
|
+
function yieldToMainThread() {
|
|
9463
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
9464
|
+
}
|
|
9465
|
+
/**
|
|
9466
|
+
* {@link serializeProjectWithLayerCache} that hands the main thread back
|
|
9467
|
+
* between layers, so autosave on a project with several large layers is many
|
|
9468
|
+
* short tasks instead of one 300-400 ms freeze (GeoLibre#2869).
|
|
9469
|
+
*
|
|
9470
|
+
* The cache makes a camera move free, so the cost lands on the first autosave
|
|
9471
|
+
* and on edits to a big layer: each layer that misses the cache is serialized
|
|
9472
|
+
* in its own slice, and the thread is released once a slice has run longer than
|
|
9473
|
+
* a few milliseconds. A single huge layer is still one slice. The result is
|
|
9474
|
+
* identical to the synchronous call. `project` and `layerSources` must not be
|
|
9475
|
+
* mutated while this runs; the store replaces records rather than editing them,
|
|
9476
|
+
* so a snapshot taken from it stays valid.
|
|
9477
|
+
*
|
|
9478
|
+
* @param project Project to serialize, as built from `layerSources`.
|
|
9479
|
+
* @param layerSources Immutable source record for each entry of
|
|
9480
|
+
* `project.layers`, in the same order, used as the cache key.
|
|
9481
|
+
* @param cache Cache shared between calls.
|
|
9482
|
+
* @param yieldFn Hands the thread back; replaced in tests.
|
|
9483
|
+
* @returns The same text {@link serializeProject} returns for `project`.
|
|
9484
|
+
*/
|
|
9485
|
+
async function serializeProjectWithLayerCacheAsync(project, layerSources, cache, yieldFn = yieldToMainThread) {
|
|
9486
|
+
const steps = serializeProjectWithLayerCacheSteps(project, layerSources, cache);
|
|
9487
|
+
let sliceStart = Date.now();
|
|
9488
|
+
for (;;) {
|
|
9489
|
+
const step = steps.next();
|
|
9490
|
+
if (step.done) return step.value;
|
|
9491
|
+
if (Date.now() - sliceStart >= SERIALIZE_SLICE_MS) {
|
|
9492
|
+
await yieldFn();
|
|
9493
|
+
sliceStart = Date.now();
|
|
9494
|
+
}
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
/** Shared body of the sync and async serializers; yields once per layer text built. */
|
|
9498
|
+
function* serializeProjectWithLayerCacheSteps(project, layerSources, cache) {
|
|
10774
9499
|
if (layerSources.length !== project.layers.length) return serializeProject(project);
|
|
10775
9500
|
const layers = project.layers.map(portableLayer);
|
|
10776
9501
|
if (new Set(layers).size !== layers.length) return serializeProject(project);
|
|
10777
9502
|
const presets = /* @__PURE__ */ new Map();
|
|
10778
|
-
layers.
|
|
9503
|
+
for (let index = 0; index < layers.length; index += 1) {
|
|
9504
|
+
const layer = layers[index];
|
|
10779
9505
|
const source = layerSources[index];
|
|
10780
9506
|
const cached = cache.get(source);
|
|
10781
9507
|
let text = cached?.index === index ? cached.text : void 0;
|
|
@@ -10785,9 +9511,10 @@ function serializeProjectWithLayerCache(project, layerSources, cache) {
|
|
|
10785
9511
|
index,
|
|
10786
9512
|
text
|
|
10787
9513
|
});
|
|
9514
|
+
yield;
|
|
10788
9515
|
}
|
|
10789
9516
|
presets.set(layer, text);
|
|
10790
|
-
}
|
|
9517
|
+
}
|
|
10791
9518
|
return serializeProjectValue({
|
|
10792
9519
|
...project,
|
|
10793
9520
|
layers
|
|
@@ -11551,12 +10278,26 @@ function normalizeProjectPreferences(preferences) {
|
|
|
11551
10278
|
arcgisBasemap: normalizeString(map.arcgisBasemap) || void 0,
|
|
11552
10279
|
cesiumBasemap: normalizeCesiumBasemap(map.cesiumBasemap),
|
|
11553
10280
|
terrainEnabled: normalizeBoolean(map.terrainEnabled, DEFAULT_PROJECT_PREFERENCES.map.terrainEnabled),
|
|
11554
|
-
coordinateFormat: typeof map.coordinateFormat === "string" ? map.coordinateFormat : DEFAULT_PROJECT_PREFERENCES.map.coordinateFormat
|
|
10281
|
+
coordinateFormat: typeof map.coordinateFormat === "string" ? map.coordinateFormat : DEFAULT_PROJECT_PREFERENCES.map.coordinateFormat,
|
|
10282
|
+
coordinateEpsgCode: normalizeEpsgCode(map.coordinateEpsgCode),
|
|
10283
|
+
zoomToNewLayers: normalizeBoolean(map.zoomToNewLayers, DEFAULT_PROJECT_PREFERENCES.map.zoomToNewLayers)
|
|
11555
10284
|
},
|
|
11556
10285
|
environmentVariables: Array.isArray(candidate.environmentVariables) ? candidate.environmentVariables.map(normalizeEnvironmentVariable).filter((variable) => Boolean(variable)) : [],
|
|
11557
10286
|
geocoding: normalizeGeocodingPreferences(candidate.geocoding)
|
|
11558
10287
|
};
|
|
11559
10288
|
}
|
|
10289
|
+
/**
|
|
10290
|
+
* Keep a stored EPSG code only when it is a positive integer.
|
|
10291
|
+
*
|
|
10292
|
+
* Args:
|
|
10293
|
+
* value: The stored value, of unknown shape.
|
|
10294
|
+
*
|
|
10295
|
+
* Returns:
|
|
10296
|
+
* The code, or undefined for anything else.
|
|
10297
|
+
*/
|
|
10298
|
+
function normalizeEpsgCode(value) {
|
|
10299
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
|
|
10300
|
+
}
|
|
11560
10301
|
function normalizeGeocodingPreferences(geocoding) {
|
|
11561
10302
|
if (!geocoding || typeof geocoding !== "object") return {
|
|
11562
10303
|
...DEFAULT_PROJECT_PREFERENCES.geocoding,
|
|
@@ -11678,12 +10419,12 @@ function isJsonCompatible(value) {
|
|
|
11678
10419
|
case "number": return Number.isFinite(value);
|
|
11679
10420
|
case "object":
|
|
11680
10421
|
if (Array.isArray(value)) return value.every(isJsonCompatible);
|
|
11681
|
-
if (!isPlainObject(value)) return false;
|
|
10422
|
+
if (!isPlainObject$1(value)) return false;
|
|
11682
10423
|
return Object.values(value).every(isJsonCompatible);
|
|
11683
10424
|
default: return false;
|
|
11684
10425
|
}
|
|
11685
10426
|
}
|
|
11686
|
-
function isPlainObject(value) {
|
|
10427
|
+
function isPlainObject$1(value) {
|
|
11687
10428
|
const prototype = Object.getPrototypeOf(value);
|
|
11688
10429
|
return prototype === Object.prototype || prototype === null;
|
|
11689
10430
|
}
|
|
@@ -11728,8 +10469,9 @@ function portableLayer(layer) {
|
|
|
11728
10469
|
}
|
|
11729
10470
|
function normalizeLayer(layer) {
|
|
11730
10471
|
layer = withoutLocalRasterBytes(layer);
|
|
11731
|
-
const { capabilities: rawCapabilities, filterExpression: rawFilterExpression, ...rest } = layer;
|
|
10472
|
+
const { capabilities: rawCapabilities, filterExpression: rawFilterExpression, descriptiveMetadata: rawDescriptiveMetadata, ...rest } = layer;
|
|
11732
10473
|
const capabilities = normalizeLayerCapabilities(rawCapabilities);
|
|
10474
|
+
const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);
|
|
11733
10475
|
const filterExpression = Array.isArray(rawFilterExpression) && rawFilterExpression.length > 0 && validateMapExpression(JSON.stringify(rawFilterExpression), { expectedType: "boolean" }).ok ? rawFilterExpression : void 0;
|
|
11734
10476
|
return {
|
|
11735
10477
|
...rest,
|
|
@@ -11742,7 +10484,8 @@ function normalizeLayer(layer) {
|
|
|
11742
10484
|
metadata: layer.metadata ?? {},
|
|
11743
10485
|
source: layer.source ?? {},
|
|
11744
10486
|
...capabilities ? { capabilities } : {},
|
|
11745
|
-
...filterExpression ? { filterExpression } : {}
|
|
10487
|
+
...filterExpression ? { filterExpression } : {},
|
|
10488
|
+
...descriptiveMetadata ? { descriptiveMetadata } : {}
|
|
11746
10489
|
};
|
|
11747
10490
|
}
|
|
11748
10491
|
function normalizeProjectComments(rawComments) {
|
|
@@ -11859,7 +10602,29 @@ function hasRestorableSourceUrl(layer) {
|
|
|
11859
10602
|
return typeof sourceUrl === "string" && sourceUrl.trim() !== "" || typeof originalUrl === "string" && originalUrl.trim() !== "";
|
|
11860
10603
|
}
|
|
11861
10604
|
function prepareLayerForSave(layer) {
|
|
10605
|
+
const hasGeometryEdits = layer.metadata.geometryEdited === true;
|
|
11862
10606
|
layer = portableLayer(layer);
|
|
10607
|
+
let wfsCredentialsRedacted = layer.metadata.wfsCredentialsRedacted === true;
|
|
10608
|
+
if (layer.metadata.sourceKind === "wfs-getfeature") {
|
|
10609
|
+
const sourceUrl = typeof layer.source.url === "string" ? redactUrlCredentials(layer.source.url) : layer.source.url;
|
|
10610
|
+
const sourcePath = typeof layer.sourcePath === "string" ? redactUrlCredentials(layer.sourcePath) : layer.sourcePath;
|
|
10611
|
+
const originalUrl = typeof layer.metadata.originalUrl === "string" ? redactUrlCredentials(layer.metadata.originalUrl) : layer.metadata.originalUrl;
|
|
10612
|
+
const stripped = sourceUrl !== layer.source.url || sourcePath !== layer.sourcePath || originalUrl !== layer.metadata.originalUrl;
|
|
10613
|
+
wfsCredentialsRedacted ||= stripped;
|
|
10614
|
+
if (stripped) layer = {
|
|
10615
|
+
...layer,
|
|
10616
|
+
source: sourceUrl === layer.source.url ? layer.source : {
|
|
10617
|
+
...layer.source,
|
|
10618
|
+
url: sourceUrl
|
|
10619
|
+
},
|
|
10620
|
+
sourcePath,
|
|
10621
|
+
metadata: {
|
|
10622
|
+
...layer.metadata,
|
|
10623
|
+
...originalUrl === layer.metadata.originalUrl ? {} : { originalUrl },
|
|
10624
|
+
wfsCredentialsRedacted: true
|
|
10625
|
+
}
|
|
10626
|
+
};
|
|
10627
|
+
}
|
|
11863
10628
|
if (layer.metadata.geometryEdited !== void 0) {
|
|
11864
10629
|
const { geometryEdited: _geometryEdited, ...metadata } = layer.metadata;
|
|
11865
10630
|
layer = {
|
|
@@ -11875,6 +10640,14 @@ function prepareLayerForSave(layer) {
|
|
|
11875
10640
|
const { embedFilter: _embedFilter, ...rest } = layer;
|
|
11876
10641
|
layer = rest;
|
|
11877
10642
|
}
|
|
10643
|
+
if (layer.descriptiveMetadata !== void 0) {
|
|
10644
|
+
const { descriptiveMetadata: rawDescriptiveMetadata, ...rest } = layer;
|
|
10645
|
+
const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);
|
|
10646
|
+
layer = descriptiveMetadata ? {
|
|
10647
|
+
...rest,
|
|
10648
|
+
descriptiveMetadata
|
|
10649
|
+
} : rest;
|
|
10650
|
+
}
|
|
11878
10651
|
if (layer.geojson && layer.metadata.transientGeojson === true) {
|
|
11879
10652
|
const { geojson: _geojson, ...rest } = layer;
|
|
11880
10653
|
layer = rest;
|
|
@@ -11891,6 +10664,26 @@ function prepareLayerForSave(layer) {
|
|
|
11891
10664
|
const { geojson: _geojson, ...rest } = layer;
|
|
11892
10665
|
layer = rest;
|
|
11893
10666
|
}
|
|
10667
|
+
const wfsSourceUrl = layer.source.url;
|
|
10668
|
+
let hasHttpWfsSource = false;
|
|
10669
|
+
if (typeof wfsSourceUrl === "string") try {
|
|
10670
|
+
const url = new URL(wfsSourceUrl);
|
|
10671
|
+
hasHttpWfsSource = url.protocol === "http:" || url.protocol === "https:";
|
|
10672
|
+
} catch {
|
|
10673
|
+
hasHttpWfsSource = false;
|
|
10674
|
+
}
|
|
10675
|
+
if (layer.type === "geojson" && layer.metadata.sourceKind === "wfs-getfeature" && layer.geojson && hasHttpWfsSource) {
|
|
10676
|
+
if (hasGeometryEdits) {
|
|
10677
|
+
const { url: _url, ...source } = layer.source;
|
|
10678
|
+
layer = {
|
|
10679
|
+
...layer,
|
|
10680
|
+
source
|
|
10681
|
+
};
|
|
10682
|
+
} else if (!wfsCredentialsRedacted) {
|
|
10683
|
+
const { geojson: _geojson, ...rest } = layer;
|
|
10684
|
+
layer = rest;
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
11894
10687
|
if (layer.geojson && layer.metadata.sourceKind === "maplibre-gl-vector-adopted" && hasRestorableSourceUrl(layer)) {
|
|
11895
10688
|
const { geojson: _geojson, ...rest } = layer;
|
|
11896
10689
|
layer = rest;
|
|
@@ -11947,18 +10740,35 @@ function portableWmsTileUrl(tile) {
|
|
|
11947
10740
|
return tile;
|
|
11948
10741
|
}
|
|
11949
10742
|
}
|
|
11950
|
-
|
|
11951
|
-
|
|
10743
|
+
/**
|
|
10744
|
+
* The store form of one project layer: its style completed from
|
|
10745
|
+
* {@link DEFAULT_LAYER_STYLE} and the project's top-level `styles` entry.
|
|
10746
|
+
*
|
|
10747
|
+
* Legacy and externally-authored projects can carry a partial top-level style
|
|
10748
|
+
* alongside newer fields on the layer itself. Those layer fields are kept,
|
|
10749
|
+
* while the top-level copy stays authoritative where it explicitly supplies a
|
|
10750
|
+
* value.
|
|
10751
|
+
*
|
|
10752
|
+
* @param project - The project the layer belongs to (for its `styles` map).
|
|
10753
|
+
* @param layer - One of `project.layers`.
|
|
10754
|
+
* @returns A new layer record ready for the store.
|
|
10755
|
+
*/
|
|
10756
|
+
function hydrateProjectLayer(project, layer) {
|
|
10757
|
+
const topLevel = project.styles?.[layer.id];
|
|
10758
|
+
return {
|
|
11952
10759
|
...layer,
|
|
11953
|
-
style:
|
|
10760
|
+
style: topLevel ? {
|
|
11954
10761
|
...DEFAULT_LAYER_STYLE,
|
|
11955
10762
|
...layer.style,
|
|
11956
|
-
...
|
|
10763
|
+
...topLevel
|
|
11957
10764
|
} : {
|
|
11958
10765
|
...DEFAULT_LAYER_STYLE,
|
|
11959
10766
|
...layer.style
|
|
11960
10767
|
}
|
|
11961
|
-
}
|
|
10768
|
+
};
|
|
10769
|
+
}
|
|
10770
|
+
function applyProjectToStore(project) {
|
|
10771
|
+
const layers = project.layers.map((layer) => hydrateProjectLayer(project, layer));
|
|
11962
10772
|
const layerGroups = normalizeLayerGroups(project.layerGroups);
|
|
11963
10773
|
const validGroupIds = new Set(layerGroups.map((g) => g.id));
|
|
11964
10774
|
const normalizedLayers = normalizeGroupContiguity(layers.map((layer) => layer.groupId && !validGroupIds.has(layer.groupId) ? {
|
|
@@ -13035,6 +11845,7 @@ const createUiSlice = (set) => ({
|
|
|
13035
11845
|
notebookOpen: false,
|
|
13036
11846
|
assistantOpen: false,
|
|
13037
11847
|
attributeTableOpen: false,
|
|
11848
|
+
attributeTableEditLayerId: null,
|
|
13038
11849
|
rasterAttributeTableOpen: false,
|
|
13039
11850
|
dashboardOpen: false,
|
|
13040
11851
|
storymapPanelOpen: false,
|
|
@@ -13124,6 +11935,11 @@ const createUiSlice = (set) => ({
|
|
|
13124
11935
|
...s.ui,
|
|
13125
11936
|
attributeTableOpen: open
|
|
13126
11937
|
} })),
|
|
11938
|
+
requestAttributeTableEdit: (layerId) => set((s) => ({ ui: {
|
|
11939
|
+
...s.ui,
|
|
11940
|
+
attributeTableEditLayerId: layerId,
|
|
11941
|
+
attributeTableOpen: layerId === null ? s.ui.attributeTableOpen : true
|
|
11942
|
+
} })),
|
|
13127
11943
|
setRasterAttributeTableOpen: (open) => set((s) => ({ ui: {
|
|
13128
11944
|
...s.ui,
|
|
13129
11945
|
rasterAttributeTableOpen: open
|
|
@@ -13241,6 +12057,16 @@ useAppStore.subscribe((state) => {
|
|
|
13241
12057
|
setActiveEllipsoidId(id);
|
|
13242
12058
|
});
|
|
13243
12059
|
/**
|
|
12060
|
+
* Whether the map should fit to data the user just added, per the project's
|
|
12061
|
+
* Map Preferences. Add-data paths consult this before an automatic fit; an
|
|
12062
|
+
* explicit "Zoom to layer" request ignores it.
|
|
12063
|
+
*
|
|
12064
|
+
* @returns `true` unless the user turned off zooming to new layers.
|
|
12065
|
+
*/
|
|
12066
|
+
function shouldZoomToNewLayers() {
|
|
12067
|
+
return useAppStore.getState().preferences.map.zoomToNewLayers !== false;
|
|
12068
|
+
}
|
|
12069
|
+
/**
|
|
13244
12070
|
* React hook for consuming application capability state for a specific privilege.
|
|
13245
12071
|
*
|
|
13246
12072
|
* @param privilege - The privilege to check.
|
|
@@ -13276,7 +12102,8 @@ function useAppCapability(privilege) {
|
|
|
13276
12102
|
*/
|
|
13277
12103
|
function applyMatchedSelection(targetLayerId, matchedIds, mode) {
|
|
13278
12104
|
const store = useAppStore.getState();
|
|
13279
|
-
const
|
|
12105
|
+
const current = store.selectedLayerId === targetLayerId ? store.selectedFeatureIds : [];
|
|
12106
|
+
const next = applySelectionMode(current, matchedIds, mode);
|
|
13280
12107
|
if (store.selectedLayerId !== targetLayerId) store.selectLayer(targetLayerId);
|
|
13281
12108
|
store.selectFeatures(next);
|
|
13282
12109
|
return next.length;
|
|
@@ -13594,6 +12421,638 @@ function formatCameraAltitude(meters, unit) {
|
|
|
13594
12421
|
})} ${label}`;
|
|
13595
12422
|
}
|
|
13596
12423
|
//#endregion
|
|
12424
|
+
//#region src/project-diff.ts
|
|
12425
|
+
const DEFAULT_PREVIEW_LENGTH = 80;
|
|
12426
|
+
const CAMERA_EPSILON = 1e-6;
|
|
12427
|
+
/** Layer fields reported in their own group, never under `other`. */
|
|
12428
|
+
const LAYER_GROUPED_KEYS = /* @__PURE__ */ new Set([
|
|
12429
|
+
"id",
|
|
12430
|
+
"name",
|
|
12431
|
+
"type",
|
|
12432
|
+
"visible",
|
|
12433
|
+
"opacity",
|
|
12434
|
+
"style",
|
|
12435
|
+
"geojson",
|
|
12436
|
+
"filterExpression",
|
|
12437
|
+
"quickFilters",
|
|
12438
|
+
"source",
|
|
12439
|
+
"sourcePath",
|
|
12440
|
+
"beforeId"
|
|
12441
|
+
]);
|
|
12442
|
+
/** Top-level project fields reported in their own group, never as a section. */
|
|
12443
|
+
const PROJECT_GROUPED_KEYS = /* @__PURE__ */ new Set([
|
|
12444
|
+
"id",
|
|
12445
|
+
"version",
|
|
12446
|
+
"name",
|
|
12447
|
+
"mapView",
|
|
12448
|
+
"basemapStyleUrl",
|
|
12449
|
+
"basemapVisible",
|
|
12450
|
+
"basemapOpacity",
|
|
12451
|
+
"blankBackgroundColor",
|
|
12452
|
+
"primaryRenderer",
|
|
12453
|
+
"layers",
|
|
12454
|
+
"styles",
|
|
12455
|
+
"preferences",
|
|
12456
|
+
"plugins",
|
|
12457
|
+
"metadata",
|
|
12458
|
+
"selectedLayerId"
|
|
12459
|
+
]);
|
|
12460
|
+
function isPlainObject(value) {
|
|
12461
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12462
|
+
}
|
|
12463
|
+
/**
|
|
12464
|
+
* Deep structural equality for JSON-like values. A key whose value is
|
|
12465
|
+
* `undefined` is treated as absent, so an in-memory record compares equal to
|
|
12466
|
+
* its JSON round trip.
|
|
12467
|
+
*
|
|
12468
|
+
* @param a - First value.
|
|
12469
|
+
* @param b - Second value.
|
|
12470
|
+
* @returns True when the two values are structurally equal.
|
|
12471
|
+
*/
|
|
12472
|
+
function jsonValuesEqual(a, b) {
|
|
12473
|
+
if (a === b) return true;
|
|
12474
|
+
if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
12475
|
+
if (Array.isArray(a)) {
|
|
12476
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
12477
|
+
for (let i = 0; i < a.length; i++) if (!jsonValuesEqual(a[i], b[i])) return false;
|
|
12478
|
+
return true;
|
|
12479
|
+
}
|
|
12480
|
+
if (isPlainObject(a)) {
|
|
12481
|
+
if (!isPlainObject(b)) return false;
|
|
12482
|
+
for (const key of Object.keys(a)) {
|
|
12483
|
+
if (a[key] === void 0) continue;
|
|
12484
|
+
if (!jsonValuesEqual(a[key], b[key])) return false;
|
|
12485
|
+
}
|
|
12486
|
+
for (const key of Object.keys(b)) if (b[key] !== void 0 && a[key] === void 0) return false;
|
|
12487
|
+
return true;
|
|
12488
|
+
}
|
|
12489
|
+
return false;
|
|
12490
|
+
}
|
|
12491
|
+
function isBlank(value) {
|
|
12492
|
+
if (value === void 0 || value === null) return true;
|
|
12493
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
12494
|
+
return isPlainObject(value) && Object.values(value).every((item) => item === void 0);
|
|
12495
|
+
}
|
|
12496
|
+
/**
|
|
12497
|
+
* Equality for reporting: like {@link jsonValuesEqual}, but an empty array,
|
|
12498
|
+
* an empty object, `null` and an absent value are interchangeable, since the
|
|
12499
|
+
* save path omits empty sections that an in-memory project still carries.
|
|
12500
|
+
*/
|
|
12501
|
+
function sameForReport(a, b) {
|
|
12502
|
+
return jsonValuesEqual(a, b) || isBlank(a) && isBlank(b);
|
|
12503
|
+
}
|
|
12504
|
+
/**
|
|
12505
|
+
* Render a bounded JSON-ish preview of a value without serializing more of it
|
|
12506
|
+
* than the preview can show, so a large array or object costs no more than
|
|
12507
|
+
* `maxLength` characters of work.
|
|
12508
|
+
*
|
|
12509
|
+
* @param value - The value to preview.
|
|
12510
|
+
* @param maxLength - Maximum preview length; longer output ends with `…`.
|
|
12511
|
+
* @returns The preview, or `undefined` for `undefined`.
|
|
12512
|
+
*/
|
|
12513
|
+
function previewValue(value, maxLength = DEFAULT_PREVIEW_LENGTH) {
|
|
12514
|
+
if (value === void 0) return void 0;
|
|
12515
|
+
if (typeof value === "string") return value.length > maxLength ? `${value.slice(0, Math.max(0, maxLength - 1))}…` : value;
|
|
12516
|
+
let out = "";
|
|
12517
|
+
let truncated = false;
|
|
12518
|
+
const emit = (text) => {
|
|
12519
|
+
if (out.length + text.length > maxLength) {
|
|
12520
|
+
out += text.slice(0, Math.max(0, maxLength - out.length));
|
|
12521
|
+
truncated = true;
|
|
12522
|
+
return false;
|
|
12523
|
+
}
|
|
12524
|
+
out += text;
|
|
12525
|
+
return true;
|
|
12526
|
+
};
|
|
12527
|
+
const walk = (v) => {
|
|
12528
|
+
if (v === null || typeof v !== "object") {
|
|
12529
|
+
if (typeof v === "number" && !Number.isFinite(v)) return emit("null");
|
|
12530
|
+
if (typeof v === "string" && v.length > maxLength) return emit(JSON.stringify(v.slice(0, maxLength)));
|
|
12531
|
+
const text = typeof v === "bigint" ? v.toString() : JSON.stringify(v);
|
|
12532
|
+
return emit(text ?? "null");
|
|
12533
|
+
}
|
|
12534
|
+
if (Array.isArray(v)) {
|
|
12535
|
+
if (!emit("[")) return false;
|
|
12536
|
+
for (let i = 0; i < v.length; i++) {
|
|
12537
|
+
if (i > 0 && !emit(",")) return false;
|
|
12538
|
+
if (!walk(v[i] === void 0 ? null : v[i])) return false;
|
|
12539
|
+
}
|
|
12540
|
+
return emit("]");
|
|
12541
|
+
}
|
|
12542
|
+
if (!emit("{")) return false;
|
|
12543
|
+
let first = true;
|
|
12544
|
+
for (const [key, item] of Object.entries(v)) {
|
|
12545
|
+
if (item === void 0) continue;
|
|
12546
|
+
if (!first && !emit(",")) return false;
|
|
12547
|
+
first = false;
|
|
12548
|
+
if (!emit(`${JSON.stringify(key)}:`)) return false;
|
|
12549
|
+
if (!walk(item)) return false;
|
|
12550
|
+
}
|
|
12551
|
+
return emit("}");
|
|
12552
|
+
};
|
|
12553
|
+
walk(value);
|
|
12554
|
+
if (truncated) out = `${out.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
12555
|
+
return out;
|
|
12556
|
+
}
|
|
12557
|
+
/**
|
|
12558
|
+
* Diff two records key by key. Nested plain objects are descended into up to
|
|
12559
|
+
* `depth` levels (paths joined with `.`); arrays and deeper values are
|
|
12560
|
+
* compared whole.
|
|
12561
|
+
*/
|
|
12562
|
+
function diffRecords(before, after, options, out) {
|
|
12563
|
+
const a = before ?? {};
|
|
12564
|
+
const b = after ?? {};
|
|
12565
|
+
const prefix = options.prefix ?? "";
|
|
12566
|
+
const depth = options.depth ?? 1;
|
|
12567
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
12568
|
+
for (const key of keys) {
|
|
12569
|
+
if (options.skip?.has(key)) continue;
|
|
12570
|
+
const x = a[key];
|
|
12571
|
+
const y = b[key];
|
|
12572
|
+
if (sameForReport(x, y)) continue;
|
|
12573
|
+
if (depth > 0 && isPlainObject(x) && isPlainObject(y)) {
|
|
12574
|
+
diffRecords(x, y, {
|
|
12575
|
+
prefix: `${prefix}${key}.`,
|
|
12576
|
+
depth: depth - 1,
|
|
12577
|
+
maxLength: options.maxLength
|
|
12578
|
+
}, out);
|
|
12579
|
+
continue;
|
|
12580
|
+
}
|
|
12581
|
+
out.push({
|
|
12582
|
+
path: `${prefix}${key}`,
|
|
12583
|
+
before: previewValue(x, options.maxLength),
|
|
12584
|
+
after: previewValue(y, options.maxLength)
|
|
12585
|
+
});
|
|
12586
|
+
}
|
|
12587
|
+
}
|
|
12588
|
+
const featureHashCache = /* @__PURE__ */ new WeakMap();
|
|
12589
|
+
/** cyrb53: a fast, well-distributed 53-bit string hash. */
|
|
12590
|
+
function hashString(text, seed = 0) {
|
|
12591
|
+
let h1 = 3735928559 ^ seed;
|
|
12592
|
+
let h2 = 1103547991 ^ seed;
|
|
12593
|
+
for (let i = 0; i < text.length; i++) {
|
|
12594
|
+
const ch = text.charCodeAt(i);
|
|
12595
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
12596
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
12597
|
+
}
|
|
12598
|
+
h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
|
|
12599
|
+
h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
|
|
12600
|
+
h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
|
|
12601
|
+
h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
|
|
12602
|
+
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);
|
|
12603
|
+
}
|
|
12604
|
+
function hashFeature(feature) {
|
|
12605
|
+
if (typeof feature !== "object" || feature === null) {
|
|
12606
|
+
const h = hashString(JSON.stringify(feature ?? null));
|
|
12607
|
+
return {
|
|
12608
|
+
geometry: h,
|
|
12609
|
+
full: h
|
|
12610
|
+
};
|
|
12611
|
+
}
|
|
12612
|
+
const cached = featureHashCache.get(feature);
|
|
12613
|
+
if (cached) return cached;
|
|
12614
|
+
const { geometry, properties, id: _id, ...rest } = feature;
|
|
12615
|
+
const geometryHash = hashString(JSON.stringify(geometry ?? null));
|
|
12616
|
+
const result = {
|
|
12617
|
+
geometry: geometryHash,
|
|
12618
|
+
full: `${geometryHash}:${hashString(JSON.stringify(properties ?? null), 1)}:${Object.keys(rest).filter((key) => key !== "type" && rest[key] !== void 0).length > 0 ? hashString(JSON.stringify(rest), 2) : ""}`
|
|
12619
|
+
};
|
|
12620
|
+
featureHashCache.set(feature, result);
|
|
12621
|
+
return result;
|
|
12622
|
+
}
|
|
12623
|
+
function featureKey(feature) {
|
|
12624
|
+
if (typeof feature !== "object" || feature === null) return null;
|
|
12625
|
+
const id = feature.id;
|
|
12626
|
+
if (typeof id === "string") return `s:${id}`;
|
|
12627
|
+
if (typeof id === "number" && Number.isFinite(id)) return `n:${id}`;
|
|
12628
|
+
return null;
|
|
12629
|
+
}
|
|
12630
|
+
function indexFeatures(features) {
|
|
12631
|
+
const byId = /* @__PURE__ */ new Map();
|
|
12632
|
+
const byGeometry = /* @__PURE__ */ new Map();
|
|
12633
|
+
let keyed = 0;
|
|
12634
|
+
let unkeyed = 0;
|
|
12635
|
+
for (const feature of features) {
|
|
12636
|
+
const hash = hashFeature(feature);
|
|
12637
|
+
const key = featureKey(feature);
|
|
12638
|
+
if (key !== null && !byId.has(key)) {
|
|
12639
|
+
byId.set(key, hash.full);
|
|
12640
|
+
keyed++;
|
|
12641
|
+
continue;
|
|
12642
|
+
}
|
|
12643
|
+
const list = byGeometry.get(hash.geometry);
|
|
12644
|
+
if (list) list.push(hash.full);
|
|
12645
|
+
else byGeometry.set(hash.geometry, [hash.full]);
|
|
12646
|
+
unkeyed++;
|
|
12647
|
+
}
|
|
12648
|
+
return {
|
|
12649
|
+
byId,
|
|
12650
|
+
byGeometry,
|
|
12651
|
+
keyed,
|
|
12652
|
+
unkeyed
|
|
12653
|
+
};
|
|
12654
|
+
}
|
|
12655
|
+
/**
|
|
12656
|
+
* Count the features added, removed and modified between two feature
|
|
12657
|
+
* collections. Features are matched by `id` when they carry one; id-less
|
|
12658
|
+
* features are matched by geometry hash, so a feature whose properties
|
|
12659
|
+
* changed but whose geometry did not counts as modified, while a moved
|
|
12660
|
+
* id-less feature counts as one removal plus one addition.
|
|
12661
|
+
*
|
|
12662
|
+
* @param before - Earlier collection (absent counts as empty).
|
|
12663
|
+
* @param after - Later collection (absent counts as empty).
|
|
12664
|
+
* @returns The change counts.
|
|
12665
|
+
*/
|
|
12666
|
+
function diffFeatureCollections(before, after) {
|
|
12667
|
+
const a = indexFeatures(Array.isArray(before?.features) ? before.features : []);
|
|
12668
|
+
const b = indexFeatures(Array.isArray(after?.features) ? after.features : []);
|
|
12669
|
+
let added = 0;
|
|
12670
|
+
let removed = 0;
|
|
12671
|
+
let modified = 0;
|
|
12672
|
+
let unchanged = 0;
|
|
12673
|
+
for (const [key, hash] of a.byId) {
|
|
12674
|
+
const other = b.byId.get(key);
|
|
12675
|
+
if (other === void 0) removed++;
|
|
12676
|
+
else if (other === hash) unchanged++;
|
|
12677
|
+
else modified++;
|
|
12678
|
+
}
|
|
12679
|
+
for (const key of b.byId.keys()) if (!a.byId.has(key)) added++;
|
|
12680
|
+
const geometryKeys = /* @__PURE__ */ new Set([...a.byGeometry.keys(), ...b.byGeometry.keys()]);
|
|
12681
|
+
for (const geometry of geometryKeys) {
|
|
12682
|
+
const left = a.byGeometry.get(geometry) ?? [];
|
|
12683
|
+
const right = b.byGeometry.get(geometry) ?? [];
|
|
12684
|
+
const counts = /* @__PURE__ */ new Map();
|
|
12685
|
+
for (const full of left) counts.set(full, (counts.get(full) ?? 0) + 1);
|
|
12686
|
+
let same = 0;
|
|
12687
|
+
for (const full of right) {
|
|
12688
|
+
const n = counts.get(full) ?? 0;
|
|
12689
|
+
if (n > 0) {
|
|
12690
|
+
counts.set(full, n - 1);
|
|
12691
|
+
same++;
|
|
12692
|
+
}
|
|
12693
|
+
}
|
|
12694
|
+
const leftRest = left.length - same;
|
|
12695
|
+
const rightRest = right.length - same;
|
|
12696
|
+
const paired = Math.min(leftRest, rightRest);
|
|
12697
|
+
unchanged += same;
|
|
12698
|
+
modified += paired;
|
|
12699
|
+
removed += leftRest - paired;
|
|
12700
|
+
added += rightRest - paired;
|
|
12701
|
+
}
|
|
12702
|
+
const keyed = a.keyed + b.keyed;
|
|
12703
|
+
const matchedBy = a.unkeyed + b.unkeyed === 0 ? "id" : keyed === 0 ? "geometry" : "mixed";
|
|
12704
|
+
return {
|
|
12705
|
+
added,
|
|
12706
|
+
removed,
|
|
12707
|
+
modified,
|
|
12708
|
+
unchanged,
|
|
12709
|
+
matchedBy
|
|
12710
|
+
};
|
|
12711
|
+
}
|
|
12712
|
+
function effectiveStyle(project, layer) {
|
|
12713
|
+
const top = project.styles?.[layer.id];
|
|
12714
|
+
if (!top) return layer.style;
|
|
12715
|
+
return {
|
|
12716
|
+
...layer.style ?? {},
|
|
12717
|
+
...top
|
|
12718
|
+
};
|
|
12719
|
+
}
|
|
12720
|
+
function featureCountOf(layer) {
|
|
12721
|
+
return Array.isArray(layer.geojson?.features) ? layer.geojson.features.length : void 0;
|
|
12722
|
+
}
|
|
12723
|
+
function emptyLayerDiff(layer, status) {
|
|
12724
|
+
return {
|
|
12725
|
+
id: layer.id,
|
|
12726
|
+
name: layer.name,
|
|
12727
|
+
type: layer.type,
|
|
12728
|
+
status,
|
|
12729
|
+
style: [],
|
|
12730
|
+
labels: [],
|
|
12731
|
+
filter: [],
|
|
12732
|
+
source: [],
|
|
12733
|
+
other: []
|
|
12734
|
+
};
|
|
12735
|
+
}
|
|
12736
|
+
/**
|
|
12737
|
+
* Ids (of layers present in both orders) that moved: everything outside a
|
|
12738
|
+
* longest increasing subsequence of their later positions, so dragging one
|
|
12739
|
+
* layer reports that layer rather than every layer it passed.
|
|
12740
|
+
*/
|
|
12741
|
+
function movedLayerIds(beforeIds, afterIndex) {
|
|
12742
|
+
const common = beforeIds.filter((id) => afterIndex.has(id));
|
|
12743
|
+
const seq = common.map((id) => afterIndex.get(id));
|
|
12744
|
+
const tails = [];
|
|
12745
|
+
const prev = new Array(seq.length).fill(-1);
|
|
12746
|
+
for (let i = 0; i < seq.length; i++) {
|
|
12747
|
+
let lo = 0;
|
|
12748
|
+
let hi = tails.length;
|
|
12749
|
+
while (lo < hi) {
|
|
12750
|
+
const mid = lo + hi >> 1;
|
|
12751
|
+
if (seq[tails[mid]] < seq[i]) lo = mid + 1;
|
|
12752
|
+
else hi = mid;
|
|
12753
|
+
}
|
|
12754
|
+
if (lo > 0) prev[i] = tails[lo - 1];
|
|
12755
|
+
tails[lo] = i;
|
|
12756
|
+
}
|
|
12757
|
+
const stable = /* @__PURE__ */ new Set();
|
|
12758
|
+
for (let i = tails.length ? tails[tails.length - 1] : -1; i >= 0; i = prev[i]) stable.add(common[i]);
|
|
12759
|
+
return new Set(common.filter((id) => !stable.has(id)));
|
|
12760
|
+
}
|
|
12761
|
+
function diffLayer(beforeProject, afterProject, a, b, maxLength) {
|
|
12762
|
+
const diff = emptyLayerDiff(b, "changed");
|
|
12763
|
+
if (a.name !== b.name) diff.renamed = {
|
|
12764
|
+
before: a.name,
|
|
12765
|
+
after: b.name
|
|
12766
|
+
};
|
|
12767
|
+
if (Boolean(a.visible) !== Boolean(b.visible)) diff.visibility = {
|
|
12768
|
+
before: Boolean(a.visible),
|
|
12769
|
+
after: Boolean(b.visible)
|
|
12770
|
+
};
|
|
12771
|
+
if (!jsonValuesEqual(a.opacity, b.opacity)) diff.opacity = {
|
|
12772
|
+
before: a.opacity,
|
|
12773
|
+
after: b.opacity
|
|
12774
|
+
};
|
|
12775
|
+
const styleA = effectiveStyle(beforeProject, a);
|
|
12776
|
+
const styleB = effectiveStyle(afterProject, b);
|
|
12777
|
+
if (!jsonValuesEqual(styleA, styleB)) {
|
|
12778
|
+
diffRecords(styleA, styleB, {
|
|
12779
|
+
skip: /* @__PURE__ */ new Set(["labels"]),
|
|
12780
|
+
maxLength
|
|
12781
|
+
}, diff.style);
|
|
12782
|
+
const labelsA = styleA?.labels;
|
|
12783
|
+
const labelsB = styleB?.labels;
|
|
12784
|
+
if (!jsonValuesEqual(labelsA, labelsB)) diffRecords(isPlainObject(labelsA) ? labelsA : void 0, isPlainObject(labelsB) ? labelsB : void 0, { maxLength }, diff.labels);
|
|
12785
|
+
}
|
|
12786
|
+
diffRecords({
|
|
12787
|
+
filterExpression: a.filterExpression,
|
|
12788
|
+
quickFilters: a.quickFilters
|
|
12789
|
+
}, {
|
|
12790
|
+
filterExpression: b.filterExpression,
|
|
12791
|
+
quickFilters: b.quickFilters
|
|
12792
|
+
}, {
|
|
12793
|
+
depth: 0,
|
|
12794
|
+
maxLength
|
|
12795
|
+
}, diff.filter);
|
|
12796
|
+
if (a.type !== b.type) diff.source.push({
|
|
12797
|
+
path: "type",
|
|
12798
|
+
before: a.type,
|
|
12799
|
+
after: b.type
|
|
12800
|
+
});
|
|
12801
|
+
if (!jsonValuesEqual(a.source, b.source)) diffRecords(a.source, b.source, {
|
|
12802
|
+
prefix: "source.",
|
|
12803
|
+
depth: 0,
|
|
12804
|
+
maxLength
|
|
12805
|
+
}, diff.source);
|
|
12806
|
+
if (a.sourcePath !== b.sourcePath) diff.source.push({
|
|
12807
|
+
path: "sourcePath",
|
|
12808
|
+
before: previewValue(a.sourcePath, maxLength),
|
|
12809
|
+
after: previewValue(b.sourcePath, maxLength)
|
|
12810
|
+
});
|
|
12811
|
+
diffRecords(a, b, {
|
|
12812
|
+
skip: LAYER_GROUPED_KEYS,
|
|
12813
|
+
depth: 1,
|
|
12814
|
+
maxLength
|
|
12815
|
+
}, diff.other);
|
|
12816
|
+
if (a.geojson !== b.geojson && (a.geojson || b.geojson)) {
|
|
12817
|
+
const features = diffFeatureCollections(a.geojson, b.geojson);
|
|
12818
|
+
if (features.added || features.removed || features.modified) diff.features = features;
|
|
12819
|
+
}
|
|
12820
|
+
return diff;
|
|
12821
|
+
}
|
|
12822
|
+
function layerHasChanges(diff) {
|
|
12823
|
+
return Boolean(diff.renamed || diff.moved || diff.visibility || diff.opacity || diff.features || diff.style.length || diff.labels.length || diff.filter.length || diff.source.length || diff.other.length);
|
|
12824
|
+
}
|
|
12825
|
+
function layerChangeCount(diff) {
|
|
12826
|
+
return (diff.renamed ? 1 : 0) + (diff.moved ? 1 : 0) + (diff.visibility ? 1 : 0) + (diff.opacity ? 1 : 0) + (diff.features ? 1 : 0) + diff.style.length + diff.labels.length + diff.filter.length + diff.source.length + diff.other.length;
|
|
12827
|
+
}
|
|
12828
|
+
function diffPlugins(before, after, maxLength) {
|
|
12829
|
+
const activeA = new Set(before?.activePluginIds ?? []);
|
|
12830
|
+
const activeB = new Set(after?.activePluginIds ?? []);
|
|
12831
|
+
const settingsA = before?.settings ?? {};
|
|
12832
|
+
const settingsB = after?.settings ?? {};
|
|
12833
|
+
const positionsA = before?.mapControlPositions ?? {};
|
|
12834
|
+
const positionsB = after?.mapControlPositions ?? {};
|
|
12835
|
+
const ids = /* @__PURE__ */ new Set([
|
|
12836
|
+
...activeA,
|
|
12837
|
+
...activeB,
|
|
12838
|
+
...Object.keys(settingsA),
|
|
12839
|
+
...Object.keys(settingsB),
|
|
12840
|
+
...Object.keys(positionsA),
|
|
12841
|
+
...Object.keys(positionsB)
|
|
12842
|
+
]);
|
|
12843
|
+
const plugins = [];
|
|
12844
|
+
for (const id of [...ids].sort()) {
|
|
12845
|
+
const changes = [];
|
|
12846
|
+
const wasActive = activeA.has(id);
|
|
12847
|
+
const isActive = activeB.has(id);
|
|
12848
|
+
if (!jsonValuesEqual(positionsA[id], positionsB[id])) changes.push({
|
|
12849
|
+
path: "position",
|
|
12850
|
+
before: previewValue(positionsA[id], maxLength),
|
|
12851
|
+
after: previewValue(positionsB[id], maxLength)
|
|
12852
|
+
});
|
|
12853
|
+
const sa = settingsA[id];
|
|
12854
|
+
const sb = settingsB[id];
|
|
12855
|
+
if (!jsonValuesEqual(sa, sb)) {
|
|
12856
|
+
if (isPlainObject(sa) || isPlainObject(sb)) diffRecords(isPlainObject(sa) ? sa : void 0, isPlainObject(sb) ? sb : void 0, {
|
|
12857
|
+
prefix: "settings.",
|
|
12858
|
+
depth: 0,
|
|
12859
|
+
maxLength
|
|
12860
|
+
}, changes);
|
|
12861
|
+
else changes.push({
|
|
12862
|
+
path: "settings",
|
|
12863
|
+
before: previewValue(sa, maxLength),
|
|
12864
|
+
after: previewValue(sb, maxLength)
|
|
12865
|
+
});
|
|
12866
|
+
}
|
|
12867
|
+
if (wasActive !== isActive) plugins.push({
|
|
12868
|
+
id,
|
|
12869
|
+
status: isActive ? "added" : "removed",
|
|
12870
|
+
changes
|
|
12871
|
+
});
|
|
12872
|
+
else if (changes.length > 0) plugins.push({
|
|
12873
|
+
id,
|
|
12874
|
+
status: "changed",
|
|
12875
|
+
changes
|
|
12876
|
+
});
|
|
12877
|
+
}
|
|
12878
|
+
const urlsA = new Set(before?.manifestUrls ?? []);
|
|
12879
|
+
const urlsB = new Set(after?.manifestUrls ?? []);
|
|
12880
|
+
return {
|
|
12881
|
+
plugins,
|
|
12882
|
+
manifests: {
|
|
12883
|
+
added: [...urlsB].filter((url) => !urlsA.has(url)),
|
|
12884
|
+
removed: [...urlsA].filter((url) => !urlsB.has(url))
|
|
12885
|
+
}
|
|
12886
|
+
};
|
|
12887
|
+
}
|
|
12888
|
+
function numbersDiffer(a, b) {
|
|
12889
|
+
if (typeof a === "number" && typeof b === "number") return Math.abs(a - b) > CAMERA_EPSILON;
|
|
12890
|
+
return !jsonValuesEqual(a, b);
|
|
12891
|
+
}
|
|
12892
|
+
/**
|
|
12893
|
+
* Compute a structured summary of what changed from `before` to `after`.
|
|
12894
|
+
*
|
|
12895
|
+
* Layers are matched by id. Embedded GeoJSON is compared per feature (by id,
|
|
12896
|
+
* falling back to geometry hash) and each feature is hashed once per object,
|
|
12897
|
+
* so diffing the same parsed project against several others reuses its
|
|
12898
|
+
* hashes; callers keep and pass the same parsed project objects to benefit.
|
|
12899
|
+
*
|
|
12900
|
+
* @param before - The earlier project (e.g. an autosave snapshot).
|
|
12901
|
+
* @param after - The later project (e.g. the current project).
|
|
12902
|
+
* @param options - Preview length and other tuning.
|
|
12903
|
+
* @returns The grouped change summary.
|
|
12904
|
+
*/
|
|
12905
|
+
function diffProjects(before, after, options = {}) {
|
|
12906
|
+
const maxLength = options.maxPreviewLength ?? DEFAULT_PREVIEW_LENGTH;
|
|
12907
|
+
const layersA = Array.isArray(before.layers) ? before.layers : [];
|
|
12908
|
+
const layersB = Array.isArray(after.layers) ? after.layers : [];
|
|
12909
|
+
const byIdA = new Map(layersA.map((layer) => [layer.id, layer]));
|
|
12910
|
+
const byIdB = new Map(layersB.map((layer) => [layer.id, layer]));
|
|
12911
|
+
const indexA = new Map(layersA.map((layer, i) => [layer.id, i]));
|
|
12912
|
+
const indexB = new Map(layersB.map((layer, i) => [layer.id, i]));
|
|
12913
|
+
const moved = movedLayerIds(layersA.map((layer) => layer.id), indexB);
|
|
12914
|
+
const added = [];
|
|
12915
|
+
const removed = [];
|
|
12916
|
+
const changed = [];
|
|
12917
|
+
for (const layer of layersB) {
|
|
12918
|
+
if (byIdA.has(layer.id)) continue;
|
|
12919
|
+
const diff = emptyLayerDiff(layer, "added");
|
|
12920
|
+
const count = featureCountOf(layer);
|
|
12921
|
+
if (count !== void 0) diff.featureCount = count;
|
|
12922
|
+
added.push(diff);
|
|
12923
|
+
}
|
|
12924
|
+
for (const layer of layersA) {
|
|
12925
|
+
if (byIdB.has(layer.id)) continue;
|
|
12926
|
+
const diff = emptyLayerDiff(layer, "removed");
|
|
12927
|
+
const count = featureCountOf(layer);
|
|
12928
|
+
if (count !== void 0) diff.featureCount = count;
|
|
12929
|
+
removed.push(diff);
|
|
12930
|
+
}
|
|
12931
|
+
for (let i = layersB.length - 1; i >= 0; i--) {
|
|
12932
|
+
const b = layersB[i];
|
|
12933
|
+
const a = byIdA.get(b.id);
|
|
12934
|
+
if (!a) continue;
|
|
12935
|
+
const diff = a === b && before.styles?.[b.id] === after.styles?.[b.id] ? emptyLayerDiff(b, "changed") : diffLayer(before, after, a, b, maxLength);
|
|
12936
|
+
if (moved.has(b.id)) diff.moved = {
|
|
12937
|
+
before: indexA.get(b.id),
|
|
12938
|
+
after: indexB.get(b.id)
|
|
12939
|
+
};
|
|
12940
|
+
if (layerHasChanges(diff)) changed.push(diff);
|
|
12941
|
+
}
|
|
12942
|
+
const camera = [];
|
|
12943
|
+
const viewA = before.mapView ?? {};
|
|
12944
|
+
const viewB = after.mapView ?? {};
|
|
12945
|
+
const centerA = viewA.center ?? [];
|
|
12946
|
+
const centerB = viewB.center ?? [];
|
|
12947
|
+
if (numbersDiffer(centerA[0], centerB[0]) || numbersDiffer(centerA[1], centerB[1])) {
|
|
12948
|
+
const fmt = (c) => c.length === 2 && c.every((n) => typeof n === "number") ? `${c[0].toFixed(5)}, ${c[1].toFixed(5)}` : void 0;
|
|
12949
|
+
camera.push({
|
|
12950
|
+
path: "center",
|
|
12951
|
+
before: fmt(centerA),
|
|
12952
|
+
after: fmt(centerB)
|
|
12953
|
+
});
|
|
12954
|
+
}
|
|
12955
|
+
for (const key of [
|
|
12956
|
+
"zoom",
|
|
12957
|
+
"bearing",
|
|
12958
|
+
"pitch"
|
|
12959
|
+
]) if (numbersDiffer(viewA[key], viewB[key])) {
|
|
12960
|
+
const fmt = (n) => typeof n === "number" ? String(Math.round(n * 100) / 100) : previewValue(n, maxLength);
|
|
12961
|
+
camera.push({
|
|
12962
|
+
path: key,
|
|
12963
|
+
before: fmt(viewA[key]),
|
|
12964
|
+
after: fmt(viewB[key])
|
|
12965
|
+
});
|
|
12966
|
+
}
|
|
12967
|
+
const basemap = [];
|
|
12968
|
+
diffRecords({
|
|
12969
|
+
basemapStyleUrl: before.basemapStyleUrl,
|
|
12970
|
+
basemapVisible: before.basemapVisible,
|
|
12971
|
+
basemapOpacity: before.basemapOpacity,
|
|
12972
|
+
blankBackgroundColor: before.blankBackgroundColor ?? void 0,
|
|
12973
|
+
primaryRenderer: before.primaryRenderer
|
|
12974
|
+
}, {
|
|
12975
|
+
basemapStyleUrl: after.basemapStyleUrl,
|
|
12976
|
+
basemapVisible: after.basemapVisible,
|
|
12977
|
+
basemapOpacity: after.basemapOpacity,
|
|
12978
|
+
blankBackgroundColor: after.blankBackgroundColor ?? void 0,
|
|
12979
|
+
primaryRenderer: after.primaryRenderer
|
|
12980
|
+
}, {
|
|
12981
|
+
depth: 0,
|
|
12982
|
+
maxLength: Math.max(maxLength, 160)
|
|
12983
|
+
}, basemap);
|
|
12984
|
+
const projection = [];
|
|
12985
|
+
const prefsA = before.preferences ?? {};
|
|
12986
|
+
const prefsB = after.preferences ?? {};
|
|
12987
|
+
const mapA = isPlainObject(prefsA.map) ? prefsA.map : {};
|
|
12988
|
+
const mapB = isPlainObject(prefsB.map) ? prefsB.map : {};
|
|
12989
|
+
if (!jsonValuesEqual(mapA.projection, mapB.projection)) projection.push({
|
|
12990
|
+
path: "projection",
|
|
12991
|
+
before: previewValue(mapA.projection, maxLength),
|
|
12992
|
+
after: previewValue(mapB.projection, maxLength)
|
|
12993
|
+
});
|
|
12994
|
+
const preferences = [];
|
|
12995
|
+
if (!jsonValuesEqual(prefsA, prefsB)) diffRecords({
|
|
12996
|
+
...prefsA,
|
|
12997
|
+
map: {
|
|
12998
|
+
...mapA,
|
|
12999
|
+
projection: void 0
|
|
13000
|
+
}
|
|
13001
|
+
}, {
|
|
13002
|
+
...prefsB,
|
|
13003
|
+
map: {
|
|
13004
|
+
...mapB,
|
|
13005
|
+
projection: void 0
|
|
13006
|
+
}
|
|
13007
|
+
}, {
|
|
13008
|
+
depth: 1,
|
|
13009
|
+
maxLength
|
|
13010
|
+
}, preferences);
|
|
13011
|
+
const { plugins, manifests } = diffPlugins(before.plugins, after.plugins, maxLength);
|
|
13012
|
+
const metadata = [];
|
|
13013
|
+
if (before.name !== after.name) metadata.push({
|
|
13014
|
+
path: "name",
|
|
13015
|
+
before: previewValue(before.name, maxLength),
|
|
13016
|
+
after: previewValue(after.name, maxLength)
|
|
13017
|
+
});
|
|
13018
|
+
diffRecords(before.metadata, after.metadata, {
|
|
13019
|
+
depth: 0,
|
|
13020
|
+
maxLength
|
|
13021
|
+
}, metadata);
|
|
13022
|
+
metadata.sort((x, y) => metadataRank(x.path) - metadataRank(y.path));
|
|
13023
|
+
const sections = [];
|
|
13024
|
+
const projectA = before;
|
|
13025
|
+
const projectB = after;
|
|
13026
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(projectA), ...Object.keys(projectB)])) {
|
|
13027
|
+
if (PROJECT_GROUPED_KEYS.has(key)) continue;
|
|
13028
|
+
if (!sameForReport(projectA[key], projectB[key])) sections.push(key);
|
|
13029
|
+
}
|
|
13030
|
+
sections.sort();
|
|
13031
|
+
const changeCount = added.length + removed.length + changed.reduce((sum, diff) => sum + layerChangeCount(diff), 0) + camera.length + basemap.length + projection.length + preferences.length + plugins.reduce((sum, plugin) => sum + Math.max(1, plugin.changes.length), 0) + manifests.added.length + manifests.removed.length + metadata.length + sections.length;
|
|
13032
|
+
return {
|
|
13033
|
+
layers: {
|
|
13034
|
+
added,
|
|
13035
|
+
removed,
|
|
13036
|
+
changed,
|
|
13037
|
+
reordered: moved.size > 0
|
|
13038
|
+
},
|
|
13039
|
+
camera,
|
|
13040
|
+
basemap,
|
|
13041
|
+
projection,
|
|
13042
|
+
preferences,
|
|
13043
|
+
plugins,
|
|
13044
|
+
pluginManifests: manifests,
|
|
13045
|
+
metadata,
|
|
13046
|
+
sections,
|
|
13047
|
+
changeCount
|
|
13048
|
+
};
|
|
13049
|
+
}
|
|
13050
|
+
function metadataRank(path) {
|
|
13051
|
+
if (path === "name") return 0;
|
|
13052
|
+
if (path === "description") return 1;
|
|
13053
|
+
return 2;
|
|
13054
|
+
}
|
|
13055
|
+
//#endregion
|
|
13597
13056
|
//#region src/pixel-format.ts
|
|
13598
13057
|
/**
|
|
13599
13058
|
* Trim a float sample to something readable. A 32-bit raster value decoded to a
|
|
@@ -14198,6 +13657,27 @@ function storyVisibleLayers(layers, presenting, opacities) {
|
|
|
14198
13657
|
if (!layers.some((layer) => isStoryHiddenLayer(opacities, layer.id))) return layers;
|
|
14199
13658
|
return layers.filter((layer) => !isStoryHiddenLayer(opacities, layer.id));
|
|
14200
13659
|
}
|
|
13660
|
+
/**
|
|
13661
|
+
* Resolve a story chapter location to a full camera view.
|
|
13662
|
+
*
|
|
13663
|
+
* A location's `pitch` and `bearing` are optional in memory (a project file
|
|
13664
|
+
* fills them with 0 on load). An absent value keeps the camera's current one,
|
|
13665
|
+
* the same as MapLibre's camera methods do for an omitted option, so engines
|
|
13666
|
+
* whose camera API needs every field (Cesium, `MapEngine.applyView`) behave
|
|
13667
|
+
* like the MapLibre map.
|
|
13668
|
+
*
|
|
13669
|
+
* @param location The chapter's camera target.
|
|
13670
|
+
* @param current The camera's current view, used for an absent pitch/bearing.
|
|
13671
|
+
* @returns The location with `pitch` and `bearing` filled in.
|
|
13672
|
+
*/
|
|
13673
|
+
function storyLocationView(location, current) {
|
|
13674
|
+
return {
|
|
13675
|
+
center: location.center,
|
|
13676
|
+
zoom: location.zoom,
|
|
13677
|
+
pitch: location.pitch ?? current.pitch,
|
|
13678
|
+
bearing: location.bearing ?? current.bearing
|
|
13679
|
+
};
|
|
13680
|
+
}
|
|
14201
13681
|
//#endregion
|
|
14202
13682
|
//#region src/storymap-io.ts
|
|
14203
13683
|
/**
|
|
@@ -14264,8 +13744,8 @@ function serializeStoryMapCsv(storymap) {
|
|
|
14264
13744
|
String(c.location.center[0]),
|
|
14265
13745
|
String(c.location.center[1]),
|
|
14266
13746
|
String(c.location.zoom),
|
|
14267
|
-
String(c.location.pitch),
|
|
14268
|
-
String(c.location.bearing),
|
|
13747
|
+
String(c.location.pitch ?? ""),
|
|
13748
|
+
String(c.location.bearing ?? ""),
|
|
14269
13749
|
c.mapAnimation,
|
|
14270
13750
|
String(c.rotateAnimation),
|
|
14271
13751
|
JSON.stringify(c.onChapterEnter),
|
|
@@ -15876,6 +15356,6 @@ function currentEditorIdentity() {
|
|
|
15876
15356
|
return pickEditorIdentity(isActive ? selfName : void 0, readStoredAuthorName());
|
|
15877
15357
|
}
|
|
15878
15358
|
//#endregion
|
|
15879
|
-
export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CESIUM_BASEMAPS, CESIUM_BING_AERIAL_ASSET_ID, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID, CESIUM_ION_QUICK_PICKS, CESIUM_ION_SOURCE_KIND, CESIUM_KML_SOURCE_KIND, CESIUM_OSM_BUILDINGS_ASSET_ID, CHINA_BASEMAPS, CZML_QUICK_PICKS, CZML_SOURCE_KIND, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRIMARY_RENDERER, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, DEPLOYMENT_CAPABILITIES, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, IDENTIFY_ALL_LAYERS_ID, LABEL_NUMBER_LOCALES, LABEL_NUMBER_SAMPLE, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_STYLES_FILE_TYPE, LAYER_STYLES_FILE_VERSION, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, NO_LAYERS, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, POPUP_IMAGE_HEIGHT_RANGE, POPUP_MAX_WIDTH_RANGE, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, S3CorsError, S3_MAX_PRESIGN_SECONDS, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeLayerFilterExpression, activeQuickFilters, allowsCredentialHeaders, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, applyStoryLayerOpacity, attributeFormFieldLabel, attributeLinkUrl, availableCesiumBasemap, basemapToCesiumImagery, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, cesiumIonAssetId, cesiumIonAssetKind, cesiumKmlSource, changedPreferenceCredentials, circleRadiusValue, clampLabelDecimals, clampLatitude, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileLayerFilters, compileQuickFilter, compileQuickFilters, controlRendersLayer, copyableLayerStyleKind, createCesiumIonLayer, createCesiumKmlLayer, createCzmlLayer, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectLayerSerializationCache, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLayerEvaluator, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, czmlSource, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, documentLocale, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, encodeS3Key, ensureEditorTrackingFields, environmentVariableAccount, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, explainS3ReadError, extractCopiedLayerStyle, extractLayerStyleEntries, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, findLayerStyleEntryIndex, formatCameraAltitude, formatExpressionPreviewValue, formatLabelNumber, formatLabelNumberSample, formatPixelValue, formatPopupValue, formatRoundNum, formatS3Uri, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geocodingApiKeyAccount, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getArcgisApiKey, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getS3UrlSigner, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveLayerFilter, hasActiveQuickFilter, hasAppPrivilege, hasHeaderReferences, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, heatmapRampColors, horizontalBbox, identifyAllIncludes, identifyStateWithoutLayers, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbsoluteFilesystemPath, isAdoptedVectorAwaitingFeatures, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCesiumIonLayer, isCesiumKmlLayer, isCesiumOnlyLayer, isCredentialFieldName, isCredentialedS3Url, isCzmlLayer, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHeaderReferenceOnly, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isPopupClickEnabled, isPopupHoverEnabled, isRegionalBasemapSentinel, isS3Uri, isSafePopupUrl, isSecretEnvironmentVariable, isStoryHiddenLayer, isStyleLibraryTargetLayer, isStyleSpecColor, isVectorColorExpression, isochroneResponseToFeatures, labelFieldTextField, layerGroupDepth, layerGroupMoveability, layerGroupSortability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, localFileName, lookupProjectCredential, mapZoomStepOutputs, matchFeaturesByExpression, matchLayerStyleEntry, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeBlankBackgroundColor, normalizeCesiumBasemap, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerLibraryEntries, normalizeLayerStyleEntries, normalizeLongitude, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrimaryRenderer, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeProjectInteraction, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, overlayStoredEnvironmentVariables, overlayStoredGeocodingApiKeys, overlayStoredPreferenceCredentials, parseCesiumIonAssetId, parseContours, parseCzml, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseLayerStylesFile, parseProject, parseS3BucketList, parseS3Error, parseS3ListObjects, parseS3Url, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, portableWmsTileUrl, presignS3Url, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, registerS3UrlSigner, removeTrailingJsonCommas, rendererAppliesOpacity, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveHeaderReferences, resolveIdentifyTarget, resolveLabelNumberLocale, resolveLayerCapabilities, resolveMapGrid, resolvePopupBody, resolvePopupImageHeight, resolvePopupMaxWidth, resolvePopupRows, resolvePopupTitle, resolveProjectHeaderReferences, resolveReadableUrl, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, s3ObjectHttpsUrl, sameCesiumImagery, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, selectLayerById, selectLayerIds, selectLayerSummaries, selectLayersWhen, serializeLayerLibrary, serializeLayerStylesFile, serializeProject, serializeProjectWithLayerCache, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setProjectCredentialLookup, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, simpleStyleNumberValue, sortLayerGroupInPanel, splitProjectCredentials, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyLayerOpacityFactor, storyMapHasContent, storyVisibleLayers, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, uniqueImportedLayerName, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, unsignedSourceUrl, useAppCapability, useAppStore, useLayer, useLayerIds, useLayerSummaries, useLayersWhen, validateAttributeFormField, validateAttributeFormValues, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties };
|
|
15359
|
+
export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CESIUM_BASEMAPS, CESIUM_BING_AERIAL_ASSET_ID, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID, CESIUM_ION_QUICK_PICKS, CESIUM_ION_SOURCE_KIND, CESIUM_KML_SOURCE_KIND, CESIUM_OSM_BUILDINGS_ASSET_ID, CHINA_BASEMAPS, CZML_QUICK_PICKS, CZML_SOURCE_KIND, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRIMARY_RENDERER, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, DEPLOYMENT_CAPABILITIES, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, IDENTIFY_ALL_LAYERS_ID, LABEL_NUMBER_LOCALES, LABEL_NUMBER_SAMPLE, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_STYLES_FILE_TYPE, LAYER_STYLES_FILE_VERSION, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, NO_LAYERS, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, POPUP_IMAGE_HEIGHT_RANGE, POPUP_MAX_WIDTH_RANGE, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, S3CorsError, S3_MAX_PRESIGN_SECONDS, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STAC_PROCESSING_EXTENSION, STAC_VERSION, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeLayerFilterExpression, activeQuickFilters, allowsCredentialHeaders, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, applyStoryLayerOpacity, attributeFormFieldLabel, attributeLinkUrl, availableCesiumBasemap, basemapToCesiumImagery, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerStacItem, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, cesiumIonAssetId, cesiumIonAssetKind, cesiumKmlSource, changedPreferenceCredentials, circleRadiusValue, clampLabelDecimals, clampLatitude, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileLayerFilters, compileQuickFilter, compileQuickFilters, controlRendersLayer, copyableLayerStyleKind, createCesiumIonLayer, createCesiumKmlLayer, createCzmlLayer, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectLayerSerializationCache, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLayerEvaluator, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, czmlSource, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, diffFeatureCollections, diffProjects, documentLocale, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, encodeS3Key, ensureEditorTrackingFields, environmentVariableAccount, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, explainS3ReadError, extractCopiedLayerStyle, extractLayerStyleEntries, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, findLayerStyleEntryIndex, formatCameraAltitude, formatExpressionPreviewValue, formatLabelNumber, formatLabelNumberSample, formatPixelValue, formatPopupValue, formatRoundNum, formatS3Uri, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geocodingApiKeyAccount, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getArcgisApiKey, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getS3UrlSigner, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveLayerFilter, hasActiveQuickFilter, hasAppPrivilege, hasHeaderReferences, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, heatmapRampColors, horizontalBbox, hydrateProjectLayer, identifyAllIncludes, identifyStateWithoutLayers, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbsoluteFilesystemPath, isAdoptedVectorAwaitingFeatures, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCesiumIonLayer, isCesiumKmlLayer, isCesiumOnlyLayer, isCredentialFieldName, isCredentialUrlParam, isCredentialedS3Url, isCzmlLayer, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHeaderReferenceOnly, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isPopupClickEnabled, isPopupHoverEnabled, isRegionalBasemapSentinel, isS3Uri, isSafePopupUrl, isSecretEnvironmentVariable, isStoryHiddenLayer, isStyleLibraryTargetLayer, isStyleSpecColor, isValidMetadataDate, isValidMetadataEmail, isValidMetadataUrl, isVectorColorExpression, isochroneResponseToFeatures, jsonValuesEqual, labelFieldTextField, layerGroupDepth, layerGroupMoveability, layerGroupSortability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, localFileName, lookupProjectCredential, mapZoomStepOutputs, matchFeaturesByExpression, matchLayerStyleEntry, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metadataDateToRfc3339, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeBlankBackgroundColor, normalizeCesiumBasemap, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerDescriptiveMetadata, normalizeLayerLibraryEntries, normalizeLayerStyleEntries, normalizeLongitude, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrimaryRenderer, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeProjectInteraction, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, overlayStoredEnvironmentVariables, overlayStoredGeocodingApiKeys, overlayStoredPreferenceCredentials, parseCesiumIonAssetId, parseContours, parseCzml, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseLayerStylesFile, parseMetadataKeywords, parseProject, parseS3BucketList, parseS3Error, parseS3ListObjects, parseS3Url, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, portableWmsTileUrl, presignS3Url, previewValue, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactConfigurationCredentials, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, registerS3UrlSigner, removeTrailingJsonCommas, rendererAppliesOpacity, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveHeaderReferences, resolveIdentifyTarget, resolveLabelNumberLocale, resolveLayerCapabilities, resolveMapGrid, resolvePopupBody, resolvePopupImageHeight, resolvePopupMaxWidth, resolvePopupRows, resolvePopupTitle, resolveProjectHeaderReferences, resolveReadableUrl, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, s3ObjectHttpsUrl, sameCesiumImagery, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, selectLayerById, selectLayerIds, selectLayerSummaries, selectLayersWhen, serializeLayerLibrary, serializeLayerStylesFile, serializeProject, serializeProjectWithLayerCache, serializeProjectWithLayerCacheAsync, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setProjectCredentialLookup, setRegistryPublishableSettings, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, shouldZoomToNewLayers, simpleStyleNumberValue, sortLayerGroupInPanel, splitProjectCredentials, stacAssetMediaType, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyLayerOpacityFactor, storyLocationView, storyMapHasContent, storyVisibleLayers, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, uniqueImportedLayerName, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, unsignedSourceUrl, useAppCapability, useAppStore, useLayer, useLayerIds, useLayerSummaries, useLayersWhen, validateAttributeFormField, validateAttributeFormValues, validateLayerDescriptiveMetadata, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties };
|
|
15880
15360
|
|
|
15881
15361
|
//# sourceMappingURL=index.mjs.map
|