@geolibre/core 2.7.0 → 2.9.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 +659 -6
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1056 -23
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -331,6 +331,7 @@ const DEFAULT_PROJECT_PREFERENCES = {
|
|
|
331
331
|
ellipsoidId: "earth",
|
|
332
332
|
scaleUnit: "metric",
|
|
333
333
|
showPointerElevation: false,
|
|
334
|
+
terrainEnabled: false,
|
|
334
335
|
coordinateFormat: "dd"
|
|
335
336
|
},
|
|
336
337
|
environmentVariables: [],
|
|
@@ -1400,6 +1401,175 @@ function isRegionalBasemapSentinel(styleUrl) {
|
|
|
1400
1401
|
return Boolean(styleUrl?.startsWith(REGIONAL_BASEMAP_SENTINEL_PREFIX));
|
|
1401
1402
|
}
|
|
1402
1403
|
//#endregion
|
|
1404
|
+
//#region src/cesium-imagery.ts
|
|
1405
|
+
/**
|
|
1406
|
+
* Maps the project's basemap onto imagery the Cesium globe can render.
|
|
1407
|
+
*
|
|
1408
|
+
* The 2D panes render `basemapStyleUrl` through MapLibre, which understands
|
|
1409
|
+
* vector GL styles and GeoLibre's `geolibre://` sentinels. Cesium understands
|
|
1410
|
+
* neither: it draws imagery from tile providers only. So a globe pane that
|
|
1411
|
+
* simply mounted the store basemap would show nothing, which is why the pane
|
|
1412
|
+
* used to hardcode Ion World Imagery (with a token) or OpenStreetMap (without)
|
|
1413
|
+
* and ignore the user's choice entirely — leaving a bright globe beside dark 2D
|
|
1414
|
+
* panes and breaking the "same map, different projection" promise the multi-map
|
|
1415
|
+
* grid makes.
|
|
1416
|
+
*
|
|
1417
|
+
* This module is the translation. It is deliberately **engine-free** — it
|
|
1418
|
+
* returns a plain descriptor and never imports Cesium — so it lives in
|
|
1419
|
+
* `@geolibre/core` beside the basemap catalogs it reads, the 2D and 3D
|
|
1420
|
+
* renderers share one source of truth, and the Cesium chunk boundary in
|
|
1421
|
+
* `vite.config.ts` stays intact.
|
|
1422
|
+
*
|
|
1423
|
+
* Three outcomes are possible, and the caller has to handle all three (see
|
|
1424
|
+
* {@link CesiumBasemapImagery}): tiles to draw, nothing to draw, or "no raster
|
|
1425
|
+
* equivalent exists — pick your own default".
|
|
1426
|
+
*/
|
|
1427
|
+
const OSM_CREDIT = "© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors";
|
|
1428
|
+
/** EOX Maps serves these layers keyless under CC BY-SA 4.0. */
|
|
1429
|
+
const EOX_CREDIT = "<a href=\"https://maps.eox.at\">EOX Maps</a> (CC BY-SA 4.0)";
|
|
1430
|
+
const eoxLayer = (layer, extension) => `https://tiles.maps.eox.at/wmts/1.0.0/${layer}/default/g/{z}/{y}/{x}.${extension}`;
|
|
1431
|
+
/** Colourful streets — the tone of Liberty and Bright. */
|
|
1432
|
+
const STREETS = {
|
|
1433
|
+
template: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
1434
|
+
attribution: OSM_CREDIT,
|
|
1435
|
+
maximumLevel: 19
|
|
1436
|
+
};
|
|
1437
|
+
/** Light and muted — the tone of Positron and the pale Protomaps flavors. */
|
|
1438
|
+
const LIGHT = {
|
|
1439
|
+
template: eoxLayer("terrain-light_3857", "jpg"),
|
|
1440
|
+
attribution: `Terrain Light · ${OSM_CREDIT} · ${EOX_CREDIT}`,
|
|
1441
|
+
maximumLevel: 16
|
|
1442
|
+
};
|
|
1443
|
+
/**
|
|
1444
|
+
* Dark — the tone of Dark, Fiord, and the dark Protomaps flavors. NASA's Black
|
|
1445
|
+
* Marble night lights is the one keyless dark raster basemap left, and it suits
|
|
1446
|
+
* a globe: the point of matching here is that a dark project does not end up
|
|
1447
|
+
* with a glaring bright globe in the corner.
|
|
1448
|
+
*/
|
|
1449
|
+
const DARK = {
|
|
1450
|
+
template: eoxLayer("blackmarble_3857", "jpg"),
|
|
1451
|
+
attribution: `Black Marble © NASA Earth Observatory · ${EOX_CREDIT}`,
|
|
1452
|
+
maximumLevel: 8
|
|
1453
|
+
};
|
|
1454
|
+
/** Raster analogue per OpenFreeMap style name (the last path segment). */
|
|
1455
|
+
const OPENFREEMAP_ANALOGUES = {
|
|
1456
|
+
liberty: STREETS,
|
|
1457
|
+
bright: STREETS,
|
|
1458
|
+
positron: LIGHT,
|
|
1459
|
+
dark: DARK,
|
|
1460
|
+
fiord: DARK
|
|
1461
|
+
};
|
|
1462
|
+
/** Raster analogue per Protomaps v5 flavor. */
|
|
1463
|
+
const PROTOMAPS_ANALOGUES = {
|
|
1464
|
+
light: LIGHT,
|
|
1465
|
+
white: LIGHT,
|
|
1466
|
+
grayscale: LIGHT,
|
|
1467
|
+
dark: DARK,
|
|
1468
|
+
black: DARK
|
|
1469
|
+
};
|
|
1470
|
+
function toImagery(analogue) {
|
|
1471
|
+
return {
|
|
1472
|
+
kind: "xyz",
|
|
1473
|
+
template: analogue.template,
|
|
1474
|
+
attribution: analogue.attribution,
|
|
1475
|
+
maximumLevel: analogue.maximumLevel
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Whether two descriptors would draw the same thing.
|
|
1480
|
+
*
|
|
1481
|
+
* Descriptors are built fresh on every call, so two basemaps resolving to the
|
|
1482
|
+
* same analogue — OpenFreeMap `liberty` and `bright` are both the streets
|
|
1483
|
+
* tone — yield equal but distinct objects. A renderer that remembers the
|
|
1484
|
+
* descriptor it drew (to skip redundant work) must compare by value: a
|
|
1485
|
+
* reference check would miss that case and tear down and re-add an identical
|
|
1486
|
+
* tile provider, costing a flash and a round of re-requested tiles for no
|
|
1487
|
+
* visible change.
|
|
1488
|
+
*/
|
|
1489
|
+
function sameCesiumImagery(a, b) {
|
|
1490
|
+
if (a.kind !== b.kind) return false;
|
|
1491
|
+
if (a.kind !== "xyz" || b.kind !== "xyz") return true;
|
|
1492
|
+
return a.template === b.template && a.attribution === b.attribution && a.maximumLevel === b.maximumLevel && a.scheme === b.scheme && a.overlayTemplate === b.overlayTemplate;
|
|
1493
|
+
}
|
|
1494
|
+
/**
|
|
1495
|
+
* `decodeURIComponent` that reports failure instead of throwing. A URL keeps a
|
|
1496
|
+
* malformed escape (`%E0%A4`) in its pathname quite happily, but decoding one
|
|
1497
|
+
* throws `URIError` — and this runs inside a React render, so an unhandled throw
|
|
1498
|
+
* would take the globe pane down instead of falling through to the default.
|
|
1499
|
+
*/
|
|
1500
|
+
function safeDecode(value) {
|
|
1501
|
+
try {
|
|
1502
|
+
return decodeURIComponent(value);
|
|
1503
|
+
} catch {
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* The raster analogue for a vector GL style URL, or undefined when the style is
|
|
1509
|
+
* not one GeoLibre ships (a provider style, a user's own URL, a Mapbox style).
|
|
1510
|
+
* Matching on host + path rather than searching the URL for "dark"/"light"
|
|
1511
|
+
* keeps an unrelated style whose *key* happens to contain one of those words
|
|
1512
|
+
* from being mapped to the wrong tone.
|
|
1513
|
+
*/
|
|
1514
|
+
function vectorStyleAnalogue(styleUrl) {
|
|
1515
|
+
let parsed;
|
|
1516
|
+
try {
|
|
1517
|
+
parsed = new URL(styleUrl);
|
|
1518
|
+
} catch {
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (parsed.hostname === "tiles.openfreemap.org") {
|
|
1522
|
+
const name = parsed.pathname.split("/").filter(Boolean).at(-1);
|
|
1523
|
+
return name ? OPENFREEMAP_ANALOGUES[name] : void 0;
|
|
1524
|
+
}
|
|
1525
|
+
if (parsed.hostname === "api.protomaps.com") {
|
|
1526
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
1527
|
+
const flavor = segments[0] === "styles" ? segments[2] : void 0;
|
|
1528
|
+
const decoded = flavor ? safeDecode(flavor) : void 0;
|
|
1529
|
+
return decoded ? PROTOMAPS_ANALOGUES[decoded] : void 0;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Translate a basemap style URL into imagery for the Cesium globe.
|
|
1534
|
+
*
|
|
1535
|
+
* Planetary and regional basemaps are already raster tile sets, so they carry
|
|
1536
|
+
* straight over — the globe shows the same Mars/Moon/Amap tiles the 2D panes
|
|
1537
|
+
* do. GeoLibre's own vector styles map to a keyless raster basemap of matching
|
|
1538
|
+
* tone, so switching the project to a dark basemap darkens the globe too.
|
|
1539
|
+
* Everything else reports `default`, leaving the choice of fallback imagery to
|
|
1540
|
+
* the renderer (which knows whether an Ion token is configured).
|
|
1541
|
+
*
|
|
1542
|
+
* A `geolibre://` sentinel that no longer resolves is treated as the default
|
|
1543
|
+
* basemap, mirroring what `resolveMapStyle` does for the 2D map rather than
|
|
1544
|
+
* handing the sentinel onward as if it were fetchable.
|
|
1545
|
+
*
|
|
1546
|
+
* @param styleUrl - The project's `basemapStyleUrl`.
|
|
1547
|
+
* @returns What the globe should draw beneath the data layers.
|
|
1548
|
+
*/
|
|
1549
|
+
function basemapToCesiumImagery(styleUrl) {
|
|
1550
|
+
if (styleUrl === "") return { kind: "none" };
|
|
1551
|
+
const url = styleUrl ?? "https://tiles.openfreemap.org/styles/liberty";
|
|
1552
|
+
const planetary = getPlanetaryBasemapByStyleUrl(url);
|
|
1553
|
+
if (planetary) return {
|
|
1554
|
+
kind: "xyz",
|
|
1555
|
+
template: planetary.tileUrl,
|
|
1556
|
+
attribution: planetary.attribution,
|
|
1557
|
+
maximumLevel: planetary.maxZoom,
|
|
1558
|
+
...planetary.scheme ? { scheme: planetary.scheme } : {}
|
|
1559
|
+
};
|
|
1560
|
+
const regional = getRegionalBasemapByStyleUrl(url);
|
|
1561
|
+
if (regional) return {
|
|
1562
|
+
kind: "xyz",
|
|
1563
|
+
template: regional.tileUrl,
|
|
1564
|
+
attribution: regional.attribution,
|
|
1565
|
+
maximumLevel: regional.maxZoom,
|
|
1566
|
+
...regional.scheme ? { scheme: regional.scheme } : {},
|
|
1567
|
+
...regional.overlayTileUrl ? { overlayTemplate: regional.overlayTileUrl } : {}
|
|
1568
|
+
};
|
|
1569
|
+
const analogue = vectorStyleAnalogue(url.startsWith("geolibre://") ? DEFAULT_BASEMAP : url);
|
|
1570
|
+
return analogue ? toImagery(analogue) : { kind: "default" };
|
|
1571
|
+
}
|
|
1572
|
+
//#endregion
|
|
1403
1573
|
//#region src/geojson-z.ts
|
|
1404
1574
|
/**
|
|
1405
1575
|
* Helpers for vector data whose coordinates carry a Z value (elevation), e.g.
|
|
@@ -2270,14 +2440,24 @@ function batchDecodePolylines(input, options = {}) {
|
|
|
2270
2440
|
}
|
|
2271
2441
|
//#endregion
|
|
2272
2442
|
//#region src/runtime-env.ts
|
|
2443
|
+
const buildEnv = typeof __GEOLIBRE_BUILD_ENV__ === "undefined" ? {} : __GEOLIBRE_BUILD_ENV__;
|
|
2273
2444
|
/**
|
|
2274
|
-
*
|
|
2275
|
-
*
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
*
|
|
2445
|
+
* The allowlisted build-time environment, with no runtime overlay.
|
|
2446
|
+
*
|
|
2447
|
+
* For settings that must be fixed by the build and NOT overridable by a
|
|
2448
|
+
* `.geolibre.json` a user opened — the auth gate, the deployment profile, the
|
|
2449
|
+
* embed origin allowlist. Routing those through {@link getRuntimeEnvironment}
|
|
2450
|
+
* would let project-supplied `__GEOLIBRE_RUNTIME_ENV__` values relax them.
|
|
2451
|
+
*
|
|
2452
|
+
* Callers should prefer this over reading `import.meta.env` directly: a
|
|
2453
|
+
* whole-object read defeats Vite's per-key replacement and inlines every
|
|
2454
|
+
* `VITE_` var on the build machine into the chunk.
|
|
2455
|
+
*
|
|
2456
|
+
* @returns The build-time environment record.
|
|
2279
2457
|
*/
|
|
2280
|
-
|
|
2458
|
+
function getBuildEnvironment() {
|
|
2459
|
+
return buildEnv;
|
|
2460
|
+
}
|
|
2281
2461
|
/**
|
|
2282
2462
|
* Merges build-time env with project runtime env (the latter wins). Falls back
|
|
2283
2463
|
* to build-time env alone outside a browser (e.g. in tests).
|
|
@@ -2285,9 +2465,9 @@ const buildEnv = import.meta.env;
|
|
|
2285
2465
|
* @returns The resolved environment variables.
|
|
2286
2466
|
*/
|
|
2287
2467
|
function getRuntimeEnvironment() {
|
|
2288
|
-
if (typeof window === "undefined") return buildEnv
|
|
2468
|
+
if (typeof window === "undefined") return buildEnv;
|
|
2289
2469
|
return {
|
|
2290
|
-
...buildEnv
|
|
2470
|
+
...buildEnv,
|
|
2291
2471
|
...window.__GEOLIBRE_RUNTIME_ENV__ ?? {}
|
|
2292
2472
|
};
|
|
2293
2473
|
}
|
|
@@ -2373,13 +2553,18 @@ function getMapboxAccessToken(env) {
|
|
|
2373
2553
|
/**
|
|
2374
2554
|
* Resolves the Cesium Ion access token from the runtime environment.
|
|
2375
2555
|
*
|
|
2376
|
-
*
|
|
2556
|
+
* Cesium World Terrain and Ion World Imagery need a Cesium Ion token. It is
|
|
2377
2557
|
* supplied via `VITE_CESIUM_TOKEN` (baked in at build time from the bare
|
|
2378
2558
|
* `CESIUM_TOKEN` env var, which `vite.config.ts` copies into the prefixed name)
|
|
2379
2559
|
* or set at runtime through Settings → Environment variables
|
|
2380
2560
|
* (`window.__GEOLIBRE_RUNTIME_ENV__`, which bypasses Vite's envPrefix allowlist,
|
|
2381
|
-
* so a bare `CESIUM_TOKEN` entry works there too).
|
|
2382
|
-
*
|
|
2561
|
+
* so a bare `CESIUM_TOKEN` entry works there too).
|
|
2562
|
+
*
|
|
2563
|
+
* The 3D-globe view itself does **not** need one: it draws the project basemap
|
|
2564
|
+
* as its base imagery whenever that basemap has a raster form (see
|
|
2565
|
+
* {@link basemapToCesiumImagery}), and falls back to Ion World Imagery — or, with
|
|
2566
|
+
* no token, to OpenStreetMap — for the ones that do not. So an absent token
|
|
2567
|
+
* costs terrain and the quality of that fallback, not the view.
|
|
2383
2568
|
*
|
|
2384
2569
|
* @param env - Environment record (defaults to the runtime environment);
|
|
2385
2570
|
* injectable for testing.
|
|
@@ -4135,6 +4320,302 @@ function featureForEvaluation(properties, feature) {
|
|
|
4135
4320
|
};
|
|
4136
4321
|
}
|
|
4137
4322
|
//#endregion
|
|
4323
|
+
//#region src/popup.ts
|
|
4324
|
+
/** Match an inline base64 raster image (excludes SVG, which can carry scripts). */
|
|
4325
|
+
const INLINE_IMAGE_DATA_URL = /^data:image\/(?!svg)[\w.+-]+;base64,/i;
|
|
4326
|
+
/**
|
|
4327
|
+
* A trimmed non-empty string, or `undefined` for anything else.
|
|
4328
|
+
*
|
|
4329
|
+
* Popup configs arrive from untrusted JSON — a hand-edited `.geolibre.json`, an
|
|
4330
|
+
* imported layer-library bundle, an MCP-authored project — so a field typed as
|
|
4331
|
+
* `string` may hold a number, an object or null. `value.trim()` on one of those
|
|
4332
|
+
* throws and takes the whole popup render with it, so every string read here
|
|
4333
|
+
* goes through this.
|
|
4334
|
+
*/
|
|
4335
|
+
function trimmedString(value) {
|
|
4336
|
+
if (typeof value !== "string") return void 0;
|
|
4337
|
+
const trimmed = value.trim();
|
|
4338
|
+
return trimmed === "" ? void 0 : trimmed;
|
|
4339
|
+
}
|
|
4340
|
+
/** Whether the Identify popup should open for a layer. Default: yes. */
|
|
4341
|
+
function isPopupClickEnabled(popup) {
|
|
4342
|
+
return popup?.click !== false;
|
|
4343
|
+
}
|
|
4344
|
+
/** Whether a hover tooltip should follow the pointer. Default: no. */
|
|
4345
|
+
function isPopupHoverEnabled(popup) {
|
|
4346
|
+
return popup?.hover === true;
|
|
4347
|
+
}
|
|
4348
|
+
/** The label a popup shows for a configured field. */
|
|
4349
|
+
function popupFieldLabel(config) {
|
|
4350
|
+
return trimmedString(config.label) ?? config.field;
|
|
4351
|
+
}
|
|
4352
|
+
/**
|
|
4353
|
+
* Property keys that never belong in a popup: GeoLibre's own internal columns
|
|
4354
|
+
* and the full-resolution companion of a photo thumbnail (an internal twin of
|
|
4355
|
+
* a value already shown, and often multiple megabytes of data URL).
|
|
4356
|
+
*/
|
|
4357
|
+
function isInternalPopupField(key) {
|
|
4358
|
+
return key === "photo_full" || key.startsWith("__geolibre_");
|
|
4359
|
+
}
|
|
4360
|
+
/**
|
|
4361
|
+
* The property keys a popup may show, before any popup config narrows them:
|
|
4362
|
+
* the feature's own keys minus the internal ones and anything the author
|
|
4363
|
+
* marked `"hidden"` or `"excluded"`.
|
|
4364
|
+
*/
|
|
4365
|
+
function visiblePopupFields(properties, fieldVisibility) {
|
|
4366
|
+
return Object.keys(properties).filter((key) => !isInternalPopupField(key) && !fieldVisibility?.[key]);
|
|
4367
|
+
}
|
|
4368
|
+
function formatNumber(value, format, locale) {
|
|
4369
|
+
const options = { useGrouping: format.thousands === true };
|
|
4370
|
+
if (format.decimals != null && Number.isFinite(format.decimals)) {
|
|
4371
|
+
const digits = Math.max(0, Math.min(20, Math.trunc(format.decimals)));
|
|
4372
|
+
options.minimumFractionDigits = digits;
|
|
4373
|
+
options.maximumFractionDigits = digits;
|
|
4374
|
+
}
|
|
4375
|
+
return new Intl.NumberFormat(locale, options).format(value);
|
|
4376
|
+
}
|
|
4377
|
+
/**
|
|
4378
|
+
* Coerce a property value to a Date. A number is epoch milliseconds (what a
|
|
4379
|
+
* GeoJSON timestamp column normally holds); a string goes through `Date`'s own
|
|
4380
|
+
* parser. Anything that does not parse comes back as `null` so the caller can
|
|
4381
|
+
* fall back to the raw text rather than printing "Invalid Date".
|
|
4382
|
+
*/
|
|
4383
|
+
function toDate(value) {
|
|
4384
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
|
|
4385
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
4386
|
+
const fromEpoch = new Date(value);
|
|
4387
|
+
return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch;
|
|
4388
|
+
}
|
|
4389
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
4390
|
+
const parsed = new Date(value);
|
|
4391
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
4392
|
+
}
|
|
4393
|
+
return null;
|
|
4394
|
+
}
|
|
4395
|
+
function formatDate(date, dateFormat, locale) {
|
|
4396
|
+
switch (dateFormat) {
|
|
4397
|
+
case "datetime": return date.toLocaleString(locale);
|
|
4398
|
+
case "time": return date.toLocaleTimeString(locale);
|
|
4399
|
+
case "iso": return date.toISOString();
|
|
4400
|
+
case "year": return String(date.getFullYear());
|
|
4401
|
+
default: return date.toLocaleDateString(locale);
|
|
4402
|
+
}
|
|
4403
|
+
}
|
|
4404
|
+
/** Stringify a value the way the untyped popup always has. */
|
|
4405
|
+
function stringifyPopupValue(value) {
|
|
4406
|
+
if (value == null) return "";
|
|
4407
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
4408
|
+
return String(value);
|
|
4409
|
+
}
|
|
4410
|
+
/**
|
|
4411
|
+
* Render one value as text under a field's kind and format. `"auto"` and
|
|
4412
|
+
* `"image"` values are returned verbatim (the renderer draws them itself);
|
|
4413
|
+
* every other kind is stringified, formatted, then wrapped in the configured
|
|
4414
|
+
* prefix/suffix.
|
|
4415
|
+
*/
|
|
4416
|
+
function formatPopupValue(value, config = {}, options = {}) {
|
|
4417
|
+
const kind = config.kind ?? "auto";
|
|
4418
|
+
const format = config.format ?? {};
|
|
4419
|
+
if (kind === "auto" || kind === "image") return stringifyPopupValue(value);
|
|
4420
|
+
if (value == null || typeof value === "string" && value.trim() === "") return "";
|
|
4421
|
+
let body;
|
|
4422
|
+
if (kind === "number") {
|
|
4423
|
+
const numeric = typeof value === "number" ? value : Number(stringifyPopupValue(value));
|
|
4424
|
+
body = Number.isFinite(numeric) ? formatNumber(numeric, format, options.locale) : stringifyPopupValue(value);
|
|
4425
|
+
} else if (kind === "date") {
|
|
4426
|
+
const date = toDate(value);
|
|
4427
|
+
body = date ? formatDate(date, format.dateFormat ?? "date", options.locale) : stringifyPopupValue(value);
|
|
4428
|
+
} else body = stringifyPopupValue(value);
|
|
4429
|
+
if (body === "") return "";
|
|
4430
|
+
return `${format.prefix ?? ""}${body}${format.suffix ?? ""}`;
|
|
4431
|
+
}
|
|
4432
|
+
/** Whether a row would draw as a picture rather than as text. */
|
|
4433
|
+
function rendersAsImage(kind, value) {
|
|
4434
|
+
return kind === "image" || kind === "auto" && isInlineImageValue(value);
|
|
4435
|
+
}
|
|
4436
|
+
/**
|
|
4437
|
+
* The properties a popup may read, with the author's hidden and excluded
|
|
4438
|
+
* fields and GeoLibre's internal columns removed.
|
|
4439
|
+
*
|
|
4440
|
+
* Expressions get this rather than the raw record. `["get", "ssn"]` in a title
|
|
4441
|
+
* or body would otherwise reach a column the author hid, printing it into the
|
|
4442
|
+
* popup — and onto the hover tooltip, which needs no click at all. The rest of
|
|
4443
|
+
* this module already honors `fieldVisibility` field by field; an expression
|
|
4444
|
+
* has no field list to filter, so the filtering has to happen to its input.
|
|
4445
|
+
*/
|
|
4446
|
+
function visiblePopupProperties(properties, fieldVisibility) {
|
|
4447
|
+
const visible = {};
|
|
4448
|
+
for (const key of visiblePopupFields(properties, fieldVisibility)) visible[key] = properties[key];
|
|
4449
|
+
return visible;
|
|
4450
|
+
}
|
|
4451
|
+
/**
|
|
4452
|
+
* The rows a popup (or hover tooltip) should render for one feature's
|
|
4453
|
+
* properties.
|
|
4454
|
+
*
|
|
4455
|
+
* With no `popup.fields`, every visible property is returned in the feature's
|
|
4456
|
+
* own key order and left untyped, which is exactly what the popup did before
|
|
4457
|
+
* the designer existed. With a list, only the listed fields appear, in the
|
|
4458
|
+
* listed order, under their labels and formats — and a listed field the
|
|
4459
|
+
* feature does not carry is skipped rather than printed empty, so one config
|
|
4460
|
+
* can cover a layer whose features have ragged properties.
|
|
4461
|
+
*
|
|
4462
|
+
* `hover: true` narrows the list to fields flagged `hover`. A hover request
|
|
4463
|
+
* with no flagged field returns nothing: an author who turned the tooltip on
|
|
4464
|
+
* without choosing a field gets no tip rather than the whole table following
|
|
4465
|
+
* the pointer.
|
|
4466
|
+
*/
|
|
4467
|
+
function resolvePopupRows(properties, options = {}) {
|
|
4468
|
+
const { popup, fieldVisibility, hover = false, locale } = options;
|
|
4469
|
+
const configured = (Array.isArray(popup?.fields) ? popup.fields : []).filter((config) => Boolean(config) && typeof config.field === "string");
|
|
4470
|
+
if (configured.length === 0) {
|
|
4471
|
+
if (hover) return [];
|
|
4472
|
+
return visiblePopupFields(properties, fieldVisibility).map((field) => ({
|
|
4473
|
+
field,
|
|
4474
|
+
label: field,
|
|
4475
|
+
value: properties[field],
|
|
4476
|
+
text: stringifyPopupValue(properties[field]),
|
|
4477
|
+
kind: "auto"
|
|
4478
|
+
}));
|
|
4479
|
+
}
|
|
4480
|
+
const rows = [];
|
|
4481
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4482
|
+
for (const config of configured) {
|
|
4483
|
+
if (hover && config.hover !== true) continue;
|
|
4484
|
+
if (fieldVisibility?.[config.field]) continue;
|
|
4485
|
+
if (isInternalPopupField(config.field)) continue;
|
|
4486
|
+
if (!(config.field in properties)) continue;
|
|
4487
|
+
if (seen.has(config.field)) continue;
|
|
4488
|
+
const value = properties[config.field];
|
|
4489
|
+
if (hover && rendersAsImage(config.kind ?? "auto", value)) continue;
|
|
4490
|
+
seen.add(config.field);
|
|
4491
|
+
rows.push({
|
|
4492
|
+
field: config.field,
|
|
4493
|
+
label: popupFieldLabel(config),
|
|
4494
|
+
value,
|
|
4495
|
+
text: formatPopupValue(value, config, { locale }),
|
|
4496
|
+
kind: config.kind ?? "auto",
|
|
4497
|
+
...trimmedString(config.format?.linkLabel) ? { linkLabel: trimmedString(config.format?.linkLabel) } : {}
|
|
4498
|
+
});
|
|
4499
|
+
}
|
|
4500
|
+
return rows;
|
|
4501
|
+
}
|
|
4502
|
+
/**
|
|
4503
|
+
* Compiled title/body expressions, keyed by zoom and source.
|
|
4504
|
+
*
|
|
4505
|
+
* `createHoverTooltipElement` calls {@link resolvePopupTitle} once per
|
|
4506
|
+
* animation frame for as long as the pointer sits on a hovered feature, and
|
|
4507
|
+
* compiling means a `JSON.parse` plus a full style-spec validation. Without
|
|
4508
|
+
* this the same unchanged source string would be re-parsed sixty times a
|
|
4509
|
+
* second. Zoom is part of the key because a compiled expression bakes it in
|
|
4510
|
+
* for `["zoom"]`; it is constant through a hover, so the cache still hits.
|
|
4511
|
+
*/
|
|
4512
|
+
const EXPRESSION_CACHE = /* @__PURE__ */ new Map();
|
|
4513
|
+
/** Bound on the cache, so a project full of layers cannot grow it without end. */
|
|
4514
|
+
const EXPRESSION_CACHE_LIMIT = 64;
|
|
4515
|
+
function compiledPopupExpression(source, zoom) {
|
|
4516
|
+
const key = `${zoom}\u0000${source}`;
|
|
4517
|
+
const cached = EXPRESSION_CACHE.get(key);
|
|
4518
|
+
if (cached) return cached;
|
|
4519
|
+
const compiled = compileFeatureExpression(source, { zoom });
|
|
4520
|
+
if (EXPRESSION_CACHE.size >= EXPRESSION_CACHE_LIMIT) {
|
|
4521
|
+
const oldest = EXPRESSION_CACHE.keys().next().value;
|
|
4522
|
+
if (oldest !== void 0) EXPRESSION_CACHE.delete(oldest);
|
|
4523
|
+
}
|
|
4524
|
+
EXPRESSION_CACHE.set(key, compiled);
|
|
4525
|
+
return compiled;
|
|
4526
|
+
}
|
|
4527
|
+
/**
|
|
4528
|
+
* Evaluate a popup expression against a feature's properties, or return
|
|
4529
|
+
* `undefined` when it does not compile or throws. Both failures mean the same
|
|
4530
|
+
* thing to every caller here: fall back to what the popup would show without
|
|
4531
|
+
* the expression.
|
|
4532
|
+
*/
|
|
4533
|
+
function evaluatePopupExpression(source, properties, options) {
|
|
4534
|
+
const compiled = compiledPopupExpression(source, options.zoom ?? 0);
|
|
4535
|
+
if (!compiled.ok || !compiled.evaluate) return void 0;
|
|
4536
|
+
try {
|
|
4537
|
+
return compiled.evaluate(featureFor(visiblePopupProperties(properties, options.fieldVisibility), options.feature));
|
|
4538
|
+
} catch {
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
/** Build the synthetic feature an expression is evaluated against. */
|
|
4543
|
+
function featureFor(properties, feature) {
|
|
4544
|
+
if (feature) return {
|
|
4545
|
+
...feature,
|
|
4546
|
+
properties
|
|
4547
|
+
};
|
|
4548
|
+
return {
|
|
4549
|
+
type: "Feature",
|
|
4550
|
+
geometry: null,
|
|
4551
|
+
properties
|
|
4552
|
+
};
|
|
4553
|
+
}
|
|
4554
|
+
/**
|
|
4555
|
+
* The heading a popup should show: the title expression's value, else the
|
|
4556
|
+
* title field's value, else the layer name. An expression that fails to
|
|
4557
|
+
* compile or evaluate, and a title field that is empty or hidden, both fall
|
|
4558
|
+
* back to the layer name rather than leaving the popup headless.
|
|
4559
|
+
*/
|
|
4560
|
+
function resolvePopupTitle(layerName, properties, popup, options = {}) {
|
|
4561
|
+
return resolveConfiguredPopupTitle(properties, popup, options) ?? layerName;
|
|
4562
|
+
}
|
|
4563
|
+
/**
|
|
4564
|
+
* The title the author's own configuration produced, or `null` when there is
|
|
4565
|
+
* none — no title field or expression, or one that evaluated to nothing, or a
|
|
4566
|
+
* field the author hid.
|
|
4567
|
+
*
|
|
4568
|
+
* {@link resolvePopupTitle} is this plus the layer-name fallback. The hover
|
|
4569
|
+
* tooltip needs the two apart: a tip with no fields and no configured title
|
|
4570
|
+
* would be a box repeating the layer name, so it is suppressed — but that
|
|
4571
|
+
* decision has to rest on whether a title was *configured*, not on whether the
|
|
4572
|
+
* resolved text happens to equal the layer name. A city layer named "Olympia"
|
|
4573
|
+
* whose feature is also called "Olympia" is a real title, not a fallback.
|
|
4574
|
+
*/
|
|
4575
|
+
function resolveConfiguredPopupTitle(properties, popup, options = {}) {
|
|
4576
|
+
const source = trimmedString(popup?.titleExpression);
|
|
4577
|
+
if (source) {
|
|
4578
|
+
const text = stringifyPopupValue(evaluatePopupExpression(source, properties, options)).trim();
|
|
4579
|
+
if (text) return text;
|
|
4580
|
+
}
|
|
4581
|
+
const field = trimmedString(popup?.titleField);
|
|
4582
|
+
if (field && !options.fieldVisibility?.[field] && !isInternalPopupField(field)) {
|
|
4583
|
+
const text = stringifyPopupValue(properties[field]).trim();
|
|
4584
|
+
if (text) return text;
|
|
4585
|
+
}
|
|
4586
|
+
return null;
|
|
4587
|
+
}
|
|
4588
|
+
/**
|
|
4589
|
+
* The popup body text when the author supplied a body expression, or `null`
|
|
4590
|
+
* when there is none (or it failed) and the field rows should render instead.
|
|
4591
|
+
*/
|
|
4592
|
+
function resolvePopupBody(properties, popup, options = {}) {
|
|
4593
|
+
const source = trimmedString(popup?.bodyExpression);
|
|
4594
|
+
if (!source) return null;
|
|
4595
|
+
const preview = evaluatePopupExpression(source, properties, options);
|
|
4596
|
+
if (preview === void 0) return null;
|
|
4597
|
+
const text = stringifyPopupValue(preview);
|
|
4598
|
+
return text.trim() ? text : null;
|
|
4599
|
+
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Whether a value should render as an inline image thumbnail under `"auto"`.
|
|
4602
|
+
* Kept here so the renderer and the designer's preview agree on what counts.
|
|
4603
|
+
*/
|
|
4604
|
+
function isInlineImageValue(value) {
|
|
4605
|
+
return typeof value === "string" && INLINE_IMAGE_DATA_URL.test(value);
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Whether a URL is safe to put in a popup `href`/`src`. Only http(s) and
|
|
4609
|
+
* inline raster data URLs: a `javascript:` or `data:text/html` value coming
|
|
4610
|
+
* from an untrusted GeoJSON must never become a live link.
|
|
4611
|
+
*/
|
|
4612
|
+
function isSafePopupUrl(value, allowDataImage = false) {
|
|
4613
|
+
if (typeof value !== "string") return false;
|
|
4614
|
+
const trimmed = value.trim();
|
|
4615
|
+
if (/^https?:\/\//i.test(trimmed)) return true;
|
|
4616
|
+
return allowDataImage && INLINE_IMAGE_DATA_URL.test(trimmed);
|
|
4617
|
+
}
|
|
4618
|
+
//#endregion
|
|
4138
4619
|
//#region src/virtual-fields.ts
|
|
4139
4620
|
/**
|
|
4140
4621
|
* Virtual fields (QGIS Field Calculator → "Create virtual field", issue
|
|
@@ -4604,6 +5085,246 @@ function reapplyLayerJoins(layers) {
|
|
|
4604
5085
|
return current;
|
|
4605
5086
|
}
|
|
4606
5087
|
//#endregion
|
|
5088
|
+
//#region src/quick-filters.ts
|
|
5089
|
+
/**
|
|
5090
|
+
* Quick filters (issue #2114): per-layer, data-driven filter controls that
|
|
5091
|
+
* compile down to the MapLibre filter a layer already supports.
|
|
5092
|
+
*
|
|
5093
|
+
* The persisted record is the *control state* (field, kind, chosen values),
|
|
5094
|
+
* never the compiled expression. Storing the compiled output would make a
|
|
5095
|
+
* filter unreadable and un-editable the moment it is saved, which is exactly
|
|
5096
|
+
* the wall this feature exists to remove: a rule filter, Select by Expression,
|
|
5097
|
+
* and the attribute table's filter box all already accept a hand-written
|
|
5098
|
+
* MapLibre expression.
|
|
5099
|
+
*
|
|
5100
|
+
* Compilation happens at sync time in `@geolibre/map`'s layer sync, which
|
|
5101
|
+
* combines the result with the transient `timeFilter`, the embed API's
|
|
5102
|
+
* `embedFilter`, and the rule-based visibility filter under one `["all", …]`,
|
|
5103
|
+
* so a host page's filter and a user's filter narrow the layer together
|
|
5104
|
+
* instead of clobbering each other.
|
|
5105
|
+
*
|
|
5106
|
+
* Every clause reads the property in place, so the same expression is correct
|
|
5107
|
+
* for a GeoJSON source and for a tile-backed source whose tiles decode later.
|
|
5108
|
+
*/
|
|
5109
|
+
/** Day length in milliseconds, used to make a date filter's end day inclusive. */
|
|
5110
|
+
const DAY_MS = 864e5;
|
|
5111
|
+
/** The `YYYY-MM-DD` prefix length every ISO date comparison slices to. */
|
|
5112
|
+
const ISO_DAY_LENGTH = 10;
|
|
5113
|
+
/** Whether a filter is switched on (the field defaults to enabled). */
|
|
5114
|
+
function isEnabled(filter) {
|
|
5115
|
+
return filter.enabled !== false;
|
|
5116
|
+
}
|
|
5117
|
+
/**
|
|
5118
|
+
* Validate a `YYYY-MM-DD` bound, returning its canonical text and UTC midnight,
|
|
5119
|
+
* or `null` when the text is blank, malformed, or not a real calendar day.
|
|
5120
|
+
*
|
|
5121
|
+
* The round-trip check is what rejects a day that does not exist: `Date.parse`
|
|
5122
|
+
* happily reads `2026-02-30` as March 2, so shape-checking alone would compile
|
|
5123
|
+
* a bound the user never chose. Rejecting here means a half-typed or impossible
|
|
5124
|
+
* bound compiles to no clause at all, leaving that side unconstrained rather
|
|
5125
|
+
* than filtering on a date nobody asked for.
|
|
5126
|
+
*/
|
|
5127
|
+
function parseIsoDayBound(day) {
|
|
5128
|
+
if (typeof day !== "string") return null;
|
|
5129
|
+
const trimmed = day.trim();
|
|
5130
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) return null;
|
|
5131
|
+
const ms = Date.parse(`${trimmed}T00:00:00Z`);
|
|
5132
|
+
if (!Number.isFinite(ms)) return null;
|
|
5133
|
+
return new Date(ms).toISOString().slice(0, ISO_DAY_LENGTH) === trimmed ? {
|
|
5134
|
+
day: trimmed,
|
|
5135
|
+
ms
|
|
5136
|
+
} : null;
|
|
5137
|
+
}
|
|
5138
|
+
/** A finite number, or `null` for every other input (including `NaN`). */
|
|
5139
|
+
function finite(value) {
|
|
5140
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
5141
|
+
}
|
|
5142
|
+
/**
|
|
5143
|
+
* `["get", field]` coerced for comparison, guarded by `["has", field]` where a
|
|
5144
|
+
* missing property would otherwise coerce into range (`to-number` turns a
|
|
5145
|
+
* missing property into `0`, and `to-string` into `""`, either of which can sit
|
|
5146
|
+
* inside a one-sided bound).
|
|
5147
|
+
*/
|
|
5148
|
+
function presenceGuard(field) {
|
|
5149
|
+
return ["has", field];
|
|
5150
|
+
}
|
|
5151
|
+
function compileCategorical(filter) {
|
|
5152
|
+
const values = filter.values ?? [];
|
|
5153
|
+
if (values.length === 0) return null;
|
|
5154
|
+
return [
|
|
5155
|
+
"in",
|
|
5156
|
+
["get", filter.field],
|
|
5157
|
+
["literal", values]
|
|
5158
|
+
];
|
|
5159
|
+
}
|
|
5160
|
+
function compileRange(filter) {
|
|
5161
|
+
const first = finite(filter.min);
|
|
5162
|
+
const second = finite(filter.max);
|
|
5163
|
+
if (first === null && second === null) return null;
|
|
5164
|
+
const min = first !== null && second !== null ? Math.min(first, second) : first;
|
|
5165
|
+
const max = first !== null && second !== null ? Math.max(first, second) : second;
|
|
5166
|
+
const value = ["to-number", ["get", filter.field]];
|
|
5167
|
+
const clauses = [presenceGuard(filter.field)];
|
|
5168
|
+
if (min !== null) clauses.push([
|
|
5169
|
+
">=",
|
|
5170
|
+
value,
|
|
5171
|
+
min
|
|
5172
|
+
]);
|
|
5173
|
+
if (max !== null) clauses.push([
|
|
5174
|
+
"<=",
|
|
5175
|
+
value,
|
|
5176
|
+
max
|
|
5177
|
+
]);
|
|
5178
|
+
return ["all", ...clauses];
|
|
5179
|
+
}
|
|
5180
|
+
function compileDate(filter) {
|
|
5181
|
+
const first = parseIsoDayBound(filter.start);
|
|
5182
|
+
const second = parseIsoDayBound(filter.end);
|
|
5183
|
+
if (first === null && second === null) return null;
|
|
5184
|
+
const inverted = first !== null && second !== null && first.ms > second.ms;
|
|
5185
|
+
const start = inverted ? second : first;
|
|
5186
|
+
const end = inverted ? first : second;
|
|
5187
|
+
const clauses = [presenceGuard(filter.field)];
|
|
5188
|
+
if ((filter.dateKind ?? "iso") === "iso") {
|
|
5189
|
+
const value = [
|
|
5190
|
+
"slice",
|
|
5191
|
+
["to-string", ["get", filter.field]],
|
|
5192
|
+
0,
|
|
5193
|
+
ISO_DAY_LENGTH
|
|
5194
|
+
];
|
|
5195
|
+
if (start !== null) clauses.push([
|
|
5196
|
+
">=",
|
|
5197
|
+
value,
|
|
5198
|
+
start.day
|
|
5199
|
+
]);
|
|
5200
|
+
if (end !== null) clauses.push([
|
|
5201
|
+
"<=",
|
|
5202
|
+
value,
|
|
5203
|
+
end.day
|
|
5204
|
+
]);
|
|
5205
|
+
return ["all", ...clauses];
|
|
5206
|
+
}
|
|
5207
|
+
const scale = filter.dateKind === "epochS" ? .001 : 1;
|
|
5208
|
+
const value = ["to-number", ["get", filter.field]];
|
|
5209
|
+
if (start !== null) clauses.push([
|
|
5210
|
+
">=",
|
|
5211
|
+
value,
|
|
5212
|
+
start.ms * scale
|
|
5213
|
+
]);
|
|
5214
|
+
if (end !== null) clauses.push([
|
|
5215
|
+
"<",
|
|
5216
|
+
value,
|
|
5217
|
+
(end.ms + DAY_MS) * scale
|
|
5218
|
+
]);
|
|
5219
|
+
return ["all", ...clauses];
|
|
5220
|
+
}
|
|
5221
|
+
function compileText(filter) {
|
|
5222
|
+
const needle = filter.text?.trim().toLowerCase() ?? "";
|
|
5223
|
+
if (needle === "") return null;
|
|
5224
|
+
const haystack = ["downcase", ["to-string", ["get", filter.field]]];
|
|
5225
|
+
switch (filter.operator ?? "contains") {
|
|
5226
|
+
case "equals": return [
|
|
5227
|
+
"==",
|
|
5228
|
+
haystack,
|
|
5229
|
+
needle
|
|
5230
|
+
];
|
|
5231
|
+
case "startsWith": return [
|
|
5232
|
+
"==",
|
|
5233
|
+
[
|
|
5234
|
+
"index-of",
|
|
5235
|
+
needle,
|
|
5236
|
+
haystack
|
|
5237
|
+
],
|
|
5238
|
+
0
|
|
5239
|
+
];
|
|
5240
|
+
default: return [
|
|
5241
|
+
"!=",
|
|
5242
|
+
[
|
|
5243
|
+
"index-of",
|
|
5244
|
+
needle,
|
|
5245
|
+
haystack
|
|
5246
|
+
],
|
|
5247
|
+
-1
|
|
5248
|
+
];
|
|
5249
|
+
}
|
|
5250
|
+
}
|
|
5251
|
+
/**
|
|
5252
|
+
* Compile one quick filter to a MapLibre filter expression, or `null` when it
|
|
5253
|
+
* places no constraint (switched off, no field, or nothing chosen yet). A
|
|
5254
|
+
* control that has been added but not yet answered must never hide features.
|
|
5255
|
+
*
|
|
5256
|
+
* @param filter - The persisted control state.
|
|
5257
|
+
* @returns A MapLibre filter expression, or `null`.
|
|
5258
|
+
*/
|
|
5259
|
+
function compileQuickFilter(filter) {
|
|
5260
|
+
if (!isEnabled(filter) || typeof filter.field !== "string" || filter.field === "") return null;
|
|
5261
|
+
switch (filter.kind) {
|
|
5262
|
+
case "categorical": return compileCategorical(filter);
|
|
5263
|
+
case "range": return compileRange(filter);
|
|
5264
|
+
case "date": return compileDate(filter);
|
|
5265
|
+
case "text": return compileText(filter);
|
|
5266
|
+
default: return null;
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
/**
|
|
5270
|
+
* Compile a layer's quick filters into a single MapLibre filter expression.
|
|
5271
|
+
* Multiple controls narrow the data together (`["all", …]`), matching how a
|
|
5272
|
+
* viewer reads a stack of filters. Returns `null` when no control constrains
|
|
5273
|
+
* anything, so the common path adds no filter at all.
|
|
5274
|
+
*
|
|
5275
|
+
* @param filters - The layer's persisted quick filters.
|
|
5276
|
+
* @returns A MapLibre filter expression, or `null`.
|
|
5277
|
+
*/
|
|
5278
|
+
function compileQuickFilters(filters) {
|
|
5279
|
+
if (!filters || filters.length === 0) return null;
|
|
5280
|
+
const clauses = [];
|
|
5281
|
+
for (const filter of filters) {
|
|
5282
|
+
const compiled = compileQuickFilter(filter);
|
|
5283
|
+
if (compiled) clauses.push(compiled);
|
|
5284
|
+
}
|
|
5285
|
+
if (clauses.length === 0) return null;
|
|
5286
|
+
return clauses.length === 1 ? clauses[0] : ["all", ...clauses];
|
|
5287
|
+
}
|
|
5288
|
+
/**
|
|
5289
|
+
* The quick filters that actually constrain the layer right now. Drives the
|
|
5290
|
+
* "filtered" badge on the layer row and its count, so an active filter is
|
|
5291
|
+
* never mistaken for missing data.
|
|
5292
|
+
*
|
|
5293
|
+
* @param layer - The layer to inspect.
|
|
5294
|
+
* @returns The constraining filters, in their configured order.
|
|
5295
|
+
*/
|
|
5296
|
+
function activeQuickFilters(layer) {
|
|
5297
|
+
return (layer.quickFilters ?? []).filter((filter) => compileQuickFilter(filter) !== null);
|
|
5298
|
+
}
|
|
5299
|
+
/**
|
|
5300
|
+
* Whether the layer is currently narrowed by at least one quick filter.
|
|
5301
|
+
*
|
|
5302
|
+
* @param layer - The layer to inspect.
|
|
5303
|
+
* @returns `true` when a quick filter constrains the rendered features.
|
|
5304
|
+
*/
|
|
5305
|
+
function hasActiveQuickFilter(layer) {
|
|
5306
|
+
return (layer.quickFilters ?? []).some((filter) => compileQuickFilter(filter) !== null);
|
|
5307
|
+
}
|
|
5308
|
+
/**
|
|
5309
|
+
* Clear every chosen value while keeping the controls themselves, so "Clear
|
|
5310
|
+
* filters" is one action that leaves the layer's configured controls in place
|
|
5311
|
+
* for the next question.
|
|
5312
|
+
*
|
|
5313
|
+
* @param filters - The layer's persisted quick filters.
|
|
5314
|
+
* @returns The same controls with their selections emptied.
|
|
5315
|
+
*/
|
|
5316
|
+
function clearQuickFilterValues(filters) {
|
|
5317
|
+
return (filters ?? []).map((filter) => ({
|
|
5318
|
+
...filter,
|
|
5319
|
+
values: filter.kind === "categorical" ? [] : filter.values,
|
|
5320
|
+
min: filter.kind === "range" ? null : filter.min,
|
|
5321
|
+
max: filter.kind === "range" ? null : filter.max,
|
|
5322
|
+
start: filter.kind === "date" ? null : filter.start,
|
|
5323
|
+
end: filter.kind === "date" ? null : filter.end,
|
|
5324
|
+
text: filter.kind === "text" ? "" : filter.text
|
|
5325
|
+
}));
|
|
5326
|
+
}
|
|
5327
|
+
//#endregion
|
|
4607
5328
|
//#region src/capabilities.ts
|
|
4608
5329
|
/**
|
|
4609
5330
|
* Default inferred capabilities for a layer based on its type and metadata.
|
|
@@ -4682,6 +5403,171 @@ function normalizeLayerCapabilities(raw) {
|
|
|
4682
5403
|
}
|
|
4683
5404
|
return hasAny ? caps : void 0;
|
|
4684
5405
|
}
|
|
5406
|
+
/**
|
|
5407
|
+
* All supported application privilege identifiers in GeoLibre.
|
|
5408
|
+
*/
|
|
5409
|
+
const ALL_APP_PRIVILEGES = [
|
|
5410
|
+
"layers:edit",
|
|
5411
|
+
"layers:add-remote",
|
|
5412
|
+
"layers:add-local",
|
|
5413
|
+
"processing:run",
|
|
5414
|
+
"processing:sidecar",
|
|
5415
|
+
"project:save",
|
|
5416
|
+
"project:share",
|
|
5417
|
+
"project:share-public",
|
|
5418
|
+
"plugins:install",
|
|
5419
|
+
"assistant:use",
|
|
5420
|
+
"connections:manage",
|
|
5421
|
+
"export:data",
|
|
5422
|
+
"export:image",
|
|
5423
|
+
"settings:manage"
|
|
5424
|
+
];
|
|
5425
|
+
/**
|
|
5426
|
+
* Standard privilege bundles for named application roles.
|
|
5427
|
+
*/
|
|
5428
|
+
const ROLE_PRIVILEGES = {
|
|
5429
|
+
viewer: ["export:image", "export:data"],
|
|
5430
|
+
editor: [
|
|
5431
|
+
"export:image",
|
|
5432
|
+
"export:data",
|
|
5433
|
+
"layers:edit",
|
|
5434
|
+
"layers:add-local",
|
|
5435
|
+
"layers:add-remote",
|
|
5436
|
+
"processing:run",
|
|
5437
|
+
"project:save"
|
|
5438
|
+
],
|
|
5439
|
+
publisher: [
|
|
5440
|
+
"export:image",
|
|
5441
|
+
"export:data",
|
|
5442
|
+
"layers:edit",
|
|
5443
|
+
"layers:add-local",
|
|
5444
|
+
"layers:add-remote",
|
|
5445
|
+
"processing:run",
|
|
5446
|
+
"project:save",
|
|
5447
|
+
"project:share",
|
|
5448
|
+
"project:share-public",
|
|
5449
|
+
"processing:sidecar",
|
|
5450
|
+
"assistant:use"
|
|
5451
|
+
],
|
|
5452
|
+
administrator: ALL_APP_PRIVILEGES
|
|
5453
|
+
};
|
|
5454
|
+
/**
|
|
5455
|
+
* Resolves the effective privilege list for a given role, applying custom overrides if specified.
|
|
5456
|
+
*/
|
|
5457
|
+
function resolveRolePrivileges(role, customPrivileges) {
|
|
5458
|
+
if (role === "custom") {
|
|
5459
|
+
if (!customPrivileges || customPrivileges.length === 0) return [];
|
|
5460
|
+
const validSet = new Set(ALL_APP_PRIVILEGES);
|
|
5461
|
+
return [...new Set(customPrivileges.filter((p) => validSet.has(p)))];
|
|
5462
|
+
}
|
|
5463
|
+
return [...ROLE_PRIVILEGES[role]];
|
|
5464
|
+
}
|
|
5465
|
+
/**
|
|
5466
|
+
* Intersects multiple sets of privileges to derive the effective permissions when multiple
|
|
5467
|
+
* policies (e.g. deployment, organization, and share link) apply simultaneously.
|
|
5468
|
+
*/
|
|
5469
|
+
function intersectPrivileges(...privilegeSets) {
|
|
5470
|
+
if (privilegeSets.length === 0) return [];
|
|
5471
|
+
if (privilegeSets.length === 1) return [...new Set(privilegeSets[0])];
|
|
5472
|
+
let current = new Set(privilegeSets[0]);
|
|
5473
|
+
for (let i = 1; i < privilegeSets.length; i++) {
|
|
5474
|
+
const nextSet = new Set(privilegeSets[i]);
|
|
5475
|
+
current = new Set([...current].filter((p) => nextSet.has(p)));
|
|
5476
|
+
}
|
|
5477
|
+
return [...current];
|
|
5478
|
+
}
|
|
5479
|
+
/**
|
|
5480
|
+
* Evaluates whether an application capability set grants a specific privilege.
|
|
5481
|
+
*/
|
|
5482
|
+
function hasAppPrivilege(capabilities, privilege) {
|
|
5483
|
+
if (!capabilities) return true;
|
|
5484
|
+
return capabilities.privileges.includes(privilege);
|
|
5485
|
+
}
|
|
5486
|
+
/**
|
|
5487
|
+
* Resolves why a privilege is withheld: its own reason if it has one, otherwise
|
|
5488
|
+
* the reason recorded for the capability set as a whole.
|
|
5489
|
+
*
|
|
5490
|
+
* @param capabilities - The active capability set, if any.
|
|
5491
|
+
* @param privilege - The privilege being explained.
|
|
5492
|
+
* @returns The reason, or undefined when none was recorded.
|
|
5493
|
+
*/
|
|
5494
|
+
function appPrivilegeReason(capabilities, privilege) {
|
|
5495
|
+
if (!capabilities) return void 0;
|
|
5496
|
+
return capabilities.privilegeReasons?.[privilege] ?? capabilities.reason;
|
|
5497
|
+
}
|
|
5498
|
+
/**
|
|
5499
|
+
* Creates the default unconstrained application capabilities (Administrator role).
|
|
5500
|
+
*/
|
|
5501
|
+
function createDefaultAppCapabilities() {
|
|
5502
|
+
return {
|
|
5503
|
+
role: "administrator",
|
|
5504
|
+
privileges: [...ALL_APP_PRIVILEGES]
|
|
5505
|
+
};
|
|
5506
|
+
}
|
|
5507
|
+
/**
|
|
5508
|
+
* Normalizes an untrusted array of privilege strings.
|
|
5509
|
+
*/
|
|
5510
|
+
function normalizeAppPrivileges(raw) {
|
|
5511
|
+
if (!Array.isArray(raw)) return void 0;
|
|
5512
|
+
const validSet = new Set(ALL_APP_PRIVILEGES);
|
|
5513
|
+
const result = [];
|
|
5514
|
+
for (const item of raw) if (typeof item === "string" && validSet.has(item)) result.push(item);
|
|
5515
|
+
return result.length > 0 ? [...new Set(result)] : [];
|
|
5516
|
+
}
|
|
5517
|
+
//#endregion
|
|
5518
|
+
//#region src/deployment-capabilities.ts
|
|
5519
|
+
/** Every capability, in a stable order for docs and tests. */
|
|
5520
|
+
const DEPLOYMENT_CAPABILITIES = [
|
|
5521
|
+
"project:edit",
|
|
5522
|
+
"data:add",
|
|
5523
|
+
"processing:run",
|
|
5524
|
+
"export:data",
|
|
5525
|
+
"plugins:install",
|
|
5526
|
+
"settings:manage"
|
|
5527
|
+
];
|
|
5528
|
+
/**
|
|
5529
|
+
* The default grant: everything. `full` must stay the default so an existing
|
|
5530
|
+
* deployment that configures nothing behaves exactly as it did before.
|
|
5531
|
+
*/
|
|
5532
|
+
const ALL_DEPLOYMENT_CAPABILITIES = new Set(DEPLOYMENT_CAPABILITIES);
|
|
5533
|
+
/** Whether an arbitrary string names a capability we know how to enforce. */
|
|
5534
|
+
function isDeploymentCapability(value) {
|
|
5535
|
+
return DEPLOYMENT_CAPABILITIES.includes(value);
|
|
5536
|
+
}
|
|
5537
|
+
/**
|
|
5538
|
+
* The reserved token that grants nothing, for a deployment that wants the map
|
|
5539
|
+
* and nothing else.
|
|
5540
|
+
*
|
|
5541
|
+
* Needed because a blank value cannot mean it: the deployment env reader treats
|
|
5542
|
+
* an empty string as unset (a `-e VAR=` produces one), and unset has to keep
|
|
5543
|
+
* meaning "full" so existing deployments are unchanged. Without a spelling for
|
|
5544
|
+
* "none", the most locked-down configuration would be the one you cannot write.
|
|
5545
|
+
*/
|
|
5546
|
+
const NO_DEPLOYMENT_CAPABILITIES = "none";
|
|
5547
|
+
/**
|
|
5548
|
+
* Parse a comma-separated capability list from deployment configuration.
|
|
5549
|
+
*
|
|
5550
|
+
* Unknown tokens are dropped rather than throwing: a newer container image may
|
|
5551
|
+
* be configured with a capability this build does not know about, and the safe
|
|
5552
|
+
* reading of an unrecognized grant is "not granted".
|
|
5553
|
+
*
|
|
5554
|
+
* Fails closed on purpose. An operator who set the variable at all meant to
|
|
5555
|
+
* restrict something, so a list that parses to nothing grants nothing — only an
|
|
5556
|
+
* absent (or blank) value falls back to the full set, which is the caller's job
|
|
5557
|
+
* to detect.
|
|
5558
|
+
*
|
|
5559
|
+
* @param raw - The configured value, e.g. `"data:add, export:data"`.
|
|
5560
|
+
* @returns The granted capabilities, with duplicates and unknown names removed.
|
|
5561
|
+
*/
|
|
5562
|
+
function parseDeploymentCapabilities(raw) {
|
|
5563
|
+
const granted = /* @__PURE__ */ new Set();
|
|
5564
|
+
if (raw.trim().toLowerCase() === "none") return granted;
|
|
5565
|
+
for (const token of raw.split(",")) {
|
|
5566
|
+
const name = token.trim();
|
|
5567
|
+
if (isDeploymentCapability(name)) granted.add(name);
|
|
5568
|
+
}
|
|
5569
|
+
return granted;
|
|
5570
|
+
}
|
|
4685
5571
|
//#endregion
|
|
4686
5572
|
//#region src/history.ts
|
|
4687
5573
|
/**
|
|
@@ -6287,6 +7173,9 @@ function scrubPrintLayoutForRemovedLayers(config, layerIds) {
|
|
|
6287
7173
|
//#region src/project.ts
|
|
6288
7174
|
/** Placeholder name a project carries before the user names it. */
|
|
6289
7175
|
const DEFAULT_PROJECT_NAME = "Untitled Project";
|
|
7176
|
+
function normalizeBlankBackgroundColor(value) {
|
|
7177
|
+
return typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value) ? value : null;
|
|
7178
|
+
}
|
|
6290
7179
|
function createDefaultMapView() {
|
|
6291
7180
|
return {
|
|
6292
7181
|
center: [-100, 40],
|
|
@@ -6303,6 +7192,7 @@ function createEmptyProject(name = DEFAULT_PROJECT_NAME, options = {}) {
|
|
|
6303
7192
|
basemapStyleUrl: options.basemapStyleUrl ?? "https://tiles.openfreemap.org/styles/liberty",
|
|
6304
7193
|
basemapVisible: true,
|
|
6305
7194
|
basemapOpacity: 1,
|
|
7195
|
+
blankBackgroundColor: null,
|
|
6306
7196
|
layers: [],
|
|
6307
7197
|
layerGroups: [],
|
|
6308
7198
|
styles: {},
|
|
@@ -6424,6 +7314,7 @@ function parseProject(json) {
|
|
|
6424
7314
|
const basemapStyleUrl = data.basemapStyleUrl ?? "https://tiles.openfreemap.org/styles/liberty";
|
|
6425
7315
|
const basemapVisible = data.basemapVisible ?? true;
|
|
6426
7316
|
const basemapOpacity = data.basemapOpacity ?? 1;
|
|
7317
|
+
const blankBackgroundColor = normalizeBlankBackgroundColor(data.blankBackgroundColor);
|
|
6427
7318
|
const mapView = normalizeMapViewState(data.mapView);
|
|
6428
7319
|
const { mapLayout, secondaryMapViews } = resolveMapGrid(normalizeMapLayout(data.mapLayout), normalizeSecondaryMapViews(data.secondaryMapViews), { mapView });
|
|
6429
7320
|
const styleLibrary = normalizeStyleLibraryEntries(data.styleLibrary);
|
|
@@ -6435,6 +7326,7 @@ function parseProject(json) {
|
|
|
6435
7326
|
basemapStyleUrl,
|
|
6436
7327
|
basemapVisible,
|
|
6437
7328
|
basemapOpacity,
|
|
7329
|
+
blankBackgroundColor,
|
|
6438
7330
|
layers,
|
|
6439
7331
|
...selectedLayerId !== void 0 ? { selectedLayerId } : {},
|
|
6440
7332
|
...layerGroups.length > 0 ? { layerGroups } : {},
|
|
@@ -7119,6 +8011,7 @@ function normalizeProjectPreferences(preferences) {
|
|
|
7119
8011
|
ellipsoidId: getEllipsoid(map.ellipsoidId).id,
|
|
7120
8012
|
scaleUnit: normalizeScaleUnit(map.scaleUnit),
|
|
7121
8013
|
showPointerElevation: normalizeBoolean(map.showPointerElevation, DEFAULT_PROJECT_PREFERENCES.map.showPointerElevation),
|
|
8014
|
+
terrainEnabled: normalizeBoolean(map.terrainEnabled, DEFAULT_PROJECT_PREFERENCES.map.terrainEnabled),
|
|
7122
8015
|
coordinateFormat: typeof map.coordinateFormat === "string" ? map.coordinateFormat : DEFAULT_PROJECT_PREFERENCES.map.coordinateFormat
|
|
7123
8016
|
},
|
|
7124
8017
|
environmentVariables: Array.isArray(candidate.environmentVariables) ? candidate.environmentVariables.map(normalizeEnvironmentVariable).filter((variable) => Boolean(variable)) : [],
|
|
@@ -7351,6 +8244,7 @@ function projectFromStore(state) {
|
|
|
7351
8244
|
basemapStyleUrl: state.basemapStyleUrl,
|
|
7352
8245
|
basemapVisible: state.basemapVisible,
|
|
7353
8246
|
basemapOpacity: state.basemapOpacity,
|
|
8247
|
+
...state.blankBackgroundColor ? { blankBackgroundColor: state.blankBackgroundColor } : {},
|
|
7354
8248
|
layers: state.layers.map(prepareLayerForSave),
|
|
7355
8249
|
...selectedLayerId !== void 0 ? { selectedLayerId } : {},
|
|
7356
8250
|
...layerGroups.length > 0 ? { layerGroups } : {},
|
|
@@ -7397,6 +8291,18 @@ function prepareLayerForSave(layer) {
|
|
|
7397
8291
|
const { geojson: _geojson, ...rest } = layer;
|
|
7398
8292
|
layer = rest;
|
|
7399
8293
|
}
|
|
8294
|
+
if (layer.type === "wms") {
|
|
8295
|
+
const tiles = layer.source.tiles;
|
|
8296
|
+
if (!Array.isArray(tiles)) return layer;
|
|
8297
|
+
const portableTiles = tiles.map((tile) => portableWmsTileUrl(tile));
|
|
8298
|
+
return portableTiles.some((tile, index) => tile !== tiles[index]) ? {
|
|
8299
|
+
...layer,
|
|
8300
|
+
source: {
|
|
8301
|
+
...layer.source,
|
|
8302
|
+
tiles: portableTiles
|
|
8303
|
+
}
|
|
8304
|
+
} : layer;
|
|
8305
|
+
}
|
|
7400
8306
|
if (layer.type !== "xyz") return layer;
|
|
7401
8307
|
const originalUrl = typeof layer.metadata.originalUrl === "string" && layer.metadata.originalUrl.trim() ? layer.metadata.originalUrl : typeof layer.source.url === "string" && layer.source.url.trim() ? layer.source.url : null;
|
|
7402
8308
|
if (!originalUrl) return layer;
|
|
@@ -7412,11 +8318,23 @@ function prepareLayerForSave(layer) {
|
|
|
7412
8318
|
metadata
|
|
7413
8319
|
};
|
|
7414
8320
|
}
|
|
8321
|
+
function portableWmsTileUrl(tile) {
|
|
8322
|
+
if (typeof tile !== "string" || !tile.startsWith("geolibre-wms://")) return tile;
|
|
8323
|
+
try {
|
|
8324
|
+
const url = new URL(tile).searchParams.get("url");
|
|
8325
|
+
if (!url) return tile;
|
|
8326
|
+
const parsed = new URL(url);
|
|
8327
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? url : tile;
|
|
8328
|
+
} catch {
|
|
8329
|
+
return tile;
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
7415
8332
|
function applyProjectToStore(project) {
|
|
7416
8333
|
const layers = project.layers.map((layer) => ({
|
|
7417
8334
|
...layer,
|
|
7418
8335
|
style: project.styles[layer.id] ? {
|
|
7419
8336
|
...DEFAULT_LAYER_STYLE,
|
|
8337
|
+
...layer.style,
|
|
7420
8338
|
...project.styles[layer.id]
|
|
7421
8339
|
} : {
|
|
7422
8340
|
...DEFAULT_LAYER_STYLE,
|
|
@@ -7432,6 +8350,7 @@ function applyProjectToStore(project) {
|
|
|
7432
8350
|
const basemapStyleUrl = project.basemapStyleUrl;
|
|
7433
8351
|
const basemapVisible = project.basemapVisible ?? true;
|
|
7434
8352
|
const basemapOpacity = project.basemapOpacity ?? 1;
|
|
8353
|
+
const blankBackgroundColor = normalizeBlankBackgroundColor(project.blankBackgroundColor);
|
|
7435
8354
|
const mapView = normalizeMapViewState(project.mapView);
|
|
7436
8355
|
const { mapLayout, secondaryMapViews } = resolveMapGrid(normalizeMapLayout(project.mapLayout), normalizeSecondaryMapViews(project.secondaryMapViews), { mapView });
|
|
7437
8356
|
const existingLayerIds = new Set(normalizedLayers.map((l) => l.id));
|
|
@@ -7460,6 +8379,7 @@ function applyProjectToStore(project) {
|
|
|
7460
8379
|
basemapStyleUrl,
|
|
7461
8380
|
basemapVisible,
|
|
7462
8381
|
basemapOpacity,
|
|
8382
|
+
blankBackgroundColor,
|
|
7463
8383
|
layers: normalizedLayers,
|
|
7464
8384
|
layerGroups,
|
|
7465
8385
|
preferences: normalizeProjectPreferences(project.preferences),
|
|
@@ -8373,7 +9293,8 @@ function captureLayerLibraryEntry(layer, options) {
|
|
|
8373
9293
|
...localPath ? { sourcePath: localPath } : {},
|
|
8374
9294
|
...layer.joins?.length ? { joins: structuredClone(layer.joins) } : {},
|
|
8375
9295
|
...layer.virtualFields?.length ? { virtualFields: structuredClone(layer.virtualFields) } : {},
|
|
8376
|
-
...layer.attributeForm ? { attributeForm: structuredClone(layer.attributeForm) } : {}
|
|
9296
|
+
...layer.attributeForm ? { attributeForm: structuredClone(layer.attributeForm) } : {},
|
|
9297
|
+
...layer.popup ? { popup: structuredClone(layer.popup) } : {}
|
|
8377
9298
|
};
|
|
8378
9299
|
const withFeatures = embed && !controlPainted ? {
|
|
8379
9300
|
...base,
|
|
@@ -8429,7 +9350,8 @@ function planLayerLibraryAdd(entry, options) {
|
|
|
8429
9350
|
...entry.geojson ? { geojson: structuredClone(entry.geojson) } : {},
|
|
8430
9351
|
...entry.joins ? { joins: structuredClone(entry.joins) } : {},
|
|
8431
9352
|
...entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {},
|
|
8432
|
-
...entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {}
|
|
9353
|
+
...entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {},
|
|
9354
|
+
...entry.popup ? { popup: structuredClone(entry.popup) } : {}
|
|
8433
9355
|
}
|
|
8434
9356
|
};
|
|
8435
9357
|
}
|
|
@@ -8447,7 +9369,8 @@ function layerLibraryConfigPatch(entry) {
|
|
|
8447
9369
|
opacity: entry.opacity,
|
|
8448
9370
|
...entry.joins ? { joins: structuredClone(entry.joins) } : {},
|
|
8449
9371
|
...entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {},
|
|
8450
|
-
...entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {}
|
|
9372
|
+
...entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {},
|
|
9373
|
+
...entry.popup ? { popup: structuredClone(entry.popup) } : {}
|
|
8451
9374
|
};
|
|
8452
9375
|
}
|
|
8453
9376
|
/**
|
|
@@ -8533,6 +9456,24 @@ function validAttributeForm(value) {
|
|
|
8533
9456
|
fields
|
|
8534
9457
|
});
|
|
8535
9458
|
}
|
|
9459
|
+
/**
|
|
9460
|
+
* A structurally plausible popup design, or undefined. `fields` is optional (a
|
|
9461
|
+
* config may carry only a title or the hover flag), so unlike the attribute
|
|
9462
|
+
* form an empty field list does not disqualify the block — but a config with
|
|
9463
|
+
* nothing set at all is dropped rather than stored as an empty object.
|
|
9464
|
+
*/
|
|
9465
|
+
function validPopup(value) {
|
|
9466
|
+
const config = plainObject(value);
|
|
9467
|
+
if (!config) return void 0;
|
|
9468
|
+
const fields = Array.isArray(config.fields) ? config.fields.filter((field) => plainObject(field) !== void 0 && nonEmptyString(field.field)) : void 0;
|
|
9469
|
+
const rest = { ...config };
|
|
9470
|
+
delete rest.fields;
|
|
9471
|
+
if (!fields?.length && Object.keys(rest).length === 0) return void 0;
|
|
9472
|
+
return structuredClone({
|
|
9473
|
+
...rest,
|
|
9474
|
+
...fields?.length ? { fields } : {}
|
|
9475
|
+
});
|
|
9476
|
+
}
|
|
8536
9477
|
/** An array of plain JSON objects, or undefined when nothing usable is present. */
|
|
8537
9478
|
function objectArray(value) {
|
|
8538
9479
|
if (!Array.isArray(value)) return void 0;
|
|
@@ -8598,10 +9539,12 @@ function normalizeLayerLibraryEntries(value) {
|
|
|
8598
9539
|
const joins = validJoins(candidate.joins);
|
|
8599
9540
|
const virtualFields = validVirtualFields(candidate.virtualFields);
|
|
8600
9541
|
const attributeForm = validAttributeForm(candidate.attributeForm);
|
|
9542
|
+
const popup = validPopup(candidate.popup);
|
|
8601
9543
|
return {
|
|
8602
9544
|
...joins ? { joins } : {},
|
|
8603
9545
|
...virtualFields ? { virtualFields } : {},
|
|
8604
|
-
...attributeForm ? { attributeForm } : {}
|
|
9546
|
+
...attributeForm ? { attributeForm } : {},
|
|
9547
|
+
...popup ? { popup } : {}
|
|
8605
9548
|
};
|
|
8606
9549
|
})(),
|
|
8607
9550
|
...geojson ? { geojson: structuredClone(geojson) } : {},
|
|
@@ -8764,6 +9707,8 @@ function createLayerLibraryEntryId() {
|
|
|
8764
9707
|
}
|
|
8765
9708
|
//#endregion
|
|
8766
9709
|
//#region src/store.ts
|
|
9710
|
+
/** Reserved Identify target for querying every visible queryable layer at once. */
|
|
9711
|
+
const IDENTIFY_ALL_LAYERS_ID = "__geolibre_identify_all_layers__";
|
|
8767
9712
|
const MAX_RECENT_PROJECTS = 10;
|
|
8768
9713
|
/**
|
|
8769
9714
|
* A fresh, inactive collaboration slice (no live session). Frozen (like
|
|
@@ -9002,6 +9947,7 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
9002
9947
|
basemapStyleUrl: DEFAULT_BASEMAP,
|
|
9003
9948
|
basemapVisible: true,
|
|
9004
9949
|
basemapOpacity: 1,
|
|
9950
|
+
blankBackgroundColor: null,
|
|
9005
9951
|
layers: [],
|
|
9006
9952
|
layerGroups: [],
|
|
9007
9953
|
preferences: DEFAULT_PROJECT_PREFERENCES,
|
|
@@ -9033,7 +9979,9 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
9033
9979
|
metadata: {},
|
|
9034
9980
|
recentProjects: [],
|
|
9035
9981
|
attributeFilter: "",
|
|
9982
|
+
deploymentCapabilities: ALL_DEPLOYMENT_CAPABILITIES,
|
|
9036
9983
|
collaboration: DEFAULT_COLLABORATION_STATE,
|
|
9984
|
+
capabilities: createDefaultAppCapabilities(),
|
|
9037
9985
|
ui: {
|
|
9038
9986
|
processingOpen: false,
|
|
9039
9987
|
processingInitialTool: null,
|
|
@@ -9311,6 +10259,10 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
9311
10259
|
basemapOpacity: opacity,
|
|
9312
10260
|
isDirty: true
|
|
9313
10261
|
}),
|
|
10262
|
+
setBlankBackgroundColor: (color) => set({
|
|
10263
|
+
blankBackgroundColor: normalizeBlankBackgroundColor(color),
|
|
10264
|
+
isDirty: true
|
|
10265
|
+
}),
|
|
9314
10266
|
setPreferences: (preferences) => set({
|
|
9315
10267
|
preferences,
|
|
9316
10268
|
isDirty: true
|
|
@@ -9660,6 +10612,7 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
9660
10612
|
projectName: name,
|
|
9661
10613
|
isDirty: true
|
|
9662
10614
|
}),
|
|
10615
|
+
setDeploymentCapabilities: (capabilities) => set({ deploymentCapabilities: new Set(capabilities) }),
|
|
9663
10616
|
setRecentProjects: (projects) => set({ recentProjects: normalizeRecentProjects(projects) }),
|
|
9664
10617
|
rememberRecentProject: (entry) => set((s) => ({ recentProjects: normalizeRecentProjects([entry, ...s.recentProjects]) })),
|
|
9665
10618
|
forgetRecentProject: (path) => {
|
|
@@ -9729,6 +10682,7 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
9729
10682
|
};
|
|
9730
10683
|
}),
|
|
9731
10684
|
setLayerAttributeForm: (id, attributeForm) => get().updateLayer(id, { attributeForm }),
|
|
10685
|
+
setLayerPopup: (id, popup) => get().updateLayer(id, { popup }),
|
|
9732
10686
|
setLayerEditorTracking: (id, editorTracking) => get().updateLayer(id, { editorTracking }),
|
|
9733
10687
|
setLayerVirtualFields: (id, fields) => set((s) => {
|
|
9734
10688
|
let layers = s.layers.map((l) => l.id === id ? applyJoinsToLayer(l, s.layers, void 0, fields) : l);
|
|
@@ -9738,6 +10692,7 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
9738
10692
|
isDirty: true
|
|
9739
10693
|
};
|
|
9740
10694
|
}),
|
|
10695
|
+
setLayerQuickFilters: (id, filters) => get().updateLayer(id, { quickFilters: filters.length > 0 ? filters : void 0 }),
|
|
9741
10696
|
setLayerVisibility: (id, visible) => get().updateLayer(id, { visible }),
|
|
9742
10697
|
setLayerOpacity: (id, opacity) => get().updateLayer(id, { opacity }),
|
|
9743
10698
|
setLayerStyle: (id, style) => set((s) => ({
|
|
@@ -10122,6 +11077,51 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
10122
11077
|
name: project.name,
|
|
10123
11078
|
openedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10124
11079
|
});
|
|
11080
|
+
},
|
|
11081
|
+
setAppRole: (role, options) => {
|
|
11082
|
+
set({ capabilities: {
|
|
11083
|
+
role,
|
|
11084
|
+
privileges: resolveRolePrivileges(role, options?.customPrivileges),
|
|
11085
|
+
reason: options?.reason
|
|
11086
|
+
} });
|
|
11087
|
+
},
|
|
11088
|
+
setAppPrivileges: (privileges, reason) => {
|
|
11089
|
+
set({ capabilities: {
|
|
11090
|
+
role: "custom",
|
|
11091
|
+
privileges: normalizeAppPrivileges(privileges) ?? [],
|
|
11092
|
+
reason
|
|
11093
|
+
} });
|
|
11094
|
+
},
|
|
11095
|
+
grantAppPrivilege: (privilege) => {
|
|
11096
|
+
const current = get().capabilities;
|
|
11097
|
+
if (current.privileges.includes(privilege)) return;
|
|
11098
|
+
const { [privilege]: _granted, ...privilegeReasons } = current.privilegeReasons ?? {};
|
|
11099
|
+
set({ capabilities: {
|
|
11100
|
+
...current,
|
|
11101
|
+
role: "custom",
|
|
11102
|
+
privileges: [...current.privileges, privilege],
|
|
11103
|
+
privilegeReasons
|
|
11104
|
+
} });
|
|
11105
|
+
},
|
|
11106
|
+
revokeAppPrivilege: (privilege, reason) => {
|
|
11107
|
+
const current = get().capabilities;
|
|
11108
|
+
const held = current.privileges.includes(privilege);
|
|
11109
|
+
if (!held && !reason) return;
|
|
11110
|
+
set({ capabilities: {
|
|
11111
|
+
...current,
|
|
11112
|
+
role: held ? "custom" : current.role,
|
|
11113
|
+
privileges: held ? current.privileges.filter((p) => p !== privilege) : current.privileges,
|
|
11114
|
+
privilegeReasons: reason ? {
|
|
11115
|
+
...current.privilegeReasons,
|
|
11116
|
+
[privilege]: reason
|
|
11117
|
+
} : current.privilegeReasons
|
|
11118
|
+
} });
|
|
11119
|
+
},
|
|
11120
|
+
resetAppCapabilities: () => {
|
|
11121
|
+
set({ capabilities: createDefaultAppCapabilities() });
|
|
11122
|
+
},
|
|
11123
|
+
hasAppPrivilege: (privilege) => {
|
|
11124
|
+
return hasAppPrivilege(get().capabilities, privilege);
|
|
10125
11125
|
}
|
|
10126
11126
|
}), {
|
|
10127
11127
|
partialize: (s) => ({
|
|
@@ -10130,10 +11130,11 @@ const useAppStore = create()(temporal((set, get) => ({
|
|
|
10130
11130
|
basemapStyleUrl: s.basemapStyleUrl,
|
|
10131
11131
|
basemapVisible: s.basemapVisible,
|
|
10132
11132
|
basemapOpacity: s.basemapOpacity,
|
|
11133
|
+
blankBackgroundColor: s.blankBackgroundColor,
|
|
10133
11134
|
storymap: s.storymap,
|
|
10134
11135
|
comments: s.comments
|
|
10135
11136
|
}),
|
|
10136
|
-
equality: (a, b) => a.basemapStyleUrl === b.basemapStyleUrl && a.basemapVisible === b.basemapVisible && a.basemapOpacity === b.basemapOpacity && a.storymap === b.storymap && shallow(a.layers, b.layers) && shallow(a.comments, b.comments) && layerGroupsEqualForHistory(a.layerGroups, b.layerGroups),
|
|
11137
|
+
equality: (a, b) => a.basemapStyleUrl === b.basemapStyleUrl && a.basemapVisible === b.basemapVisible && a.basemapOpacity === b.basemapOpacity && a.blankBackgroundColor === b.blankBackgroundColor && a.storymap === b.storymap && shallow(a.layers, b.layers) && shallow(a.comments, b.comments) && layerGroupsEqualForHistory(a.layerGroups, b.layerGroups),
|
|
10137
11138
|
limit: 100,
|
|
10138
11139
|
handleSet: (baseHandleSet) => {
|
|
10139
11140
|
const debounced = leadingDebounce(baseHandleSet, getHistoryCoalesceMs);
|
|
@@ -10258,6 +11259,19 @@ function clearHistory() {
|
|
|
10258
11259
|
notifyProjectRestoreHistory();
|
|
10259
11260
|
}
|
|
10260
11261
|
}
|
|
11262
|
+
/**
|
|
11263
|
+
* React hook for consuming application capability state for a specific privilege.
|
|
11264
|
+
*
|
|
11265
|
+
* @param privilege - The privilege to check.
|
|
11266
|
+
* @returns `{ granted: boolean, reason?: string }`
|
|
11267
|
+
*/
|
|
11268
|
+
function useAppCapability(privilege) {
|
|
11269
|
+
const capabilities = useAppStore((state) => state.capabilities);
|
|
11270
|
+
return {
|
|
11271
|
+
granted: capabilities.privileges.includes(privilege),
|
|
11272
|
+
reason: appPrivilegeReason(capabilities, privilege)
|
|
11273
|
+
};
|
|
11274
|
+
}
|
|
10261
11275
|
//#endregion
|
|
10262
11276
|
//#region src/selection-actions.ts
|
|
10263
11277
|
/**
|
|
@@ -10583,6 +11597,23 @@ function formatCameraAltitude(meters, unit) {
|
|
|
10583
11597
|
})} ${label}`;
|
|
10584
11598
|
}
|
|
10585
11599
|
//#endregion
|
|
11600
|
+
//#region src/pixel-format.ts
|
|
11601
|
+
/**
|
|
11602
|
+
* Trim a float sample to something readable. A 32-bit raster value decoded to a
|
|
11603
|
+
* JS double prints all 17 digits of its binary representation
|
|
11604
|
+
* (`48.11851119995117`), which is noise past the sensor's precision — six
|
|
11605
|
+
* significant digits is more than any COG carries. Integers are left alone so a
|
|
11606
|
+
* classification code is never shown in exponential form.
|
|
11607
|
+
*
|
|
11608
|
+
* @param value A decoded raster sample.
|
|
11609
|
+
* @returns The sample rendered for display.
|
|
11610
|
+
*/
|
|
11611
|
+
function formatPixelValue(value) {
|
|
11612
|
+
if (!Number.isFinite(value)) return String(value);
|
|
11613
|
+
if (Number.isInteger(value)) return String(value);
|
|
11614
|
+
return String(Number(value.toPrecision(6)));
|
|
11615
|
+
}
|
|
11616
|
+
//#endregion
|
|
10586
11617
|
//#region src/storymap-sample.ts
|
|
10587
11618
|
const SAMPLE_ASSET_BASE = "https://opengeos.org/maplibre-gl-storymaps/assets";
|
|
10588
11619
|
/**
|
|
@@ -10853,12 +11884,14 @@ function parseCsvRows(text) {
|
|
|
10853
11884
|
const input = text.replace(/\r\n?/g, "\n");
|
|
10854
11885
|
for (let i = 0; i < input.length; i++) {
|
|
10855
11886
|
const ch = input[i];
|
|
10856
|
-
if (inQuotes)
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
11887
|
+
if (inQuotes) {
|
|
11888
|
+
if (ch === "\"") {
|
|
11889
|
+
if (input[i + 1] === "\"") {
|
|
11890
|
+
field += "\"";
|
|
11891
|
+
i++;
|
|
11892
|
+
} else inQuotes = false;
|
|
11893
|
+
} else field += ch;
|
|
11894
|
+
} else if (ch === "\"") inQuotes = true;
|
|
10862
11895
|
else if (ch === ",") {
|
|
10863
11896
|
row.push(field);
|
|
10864
11897
|
field = "";
|
|
@@ -12058,6 +13091,6 @@ function currentEditorIdentity() {
|
|
|
12058
13091
|
return pickEditorIdentity(isActive ? selfName : void 0, readStoredAuthorName());
|
|
12059
13092
|
}
|
|
12060
13093
|
//#endregion
|
|
12061
|
-
export { BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CHINA_BASEMAPS, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, RoutingRequestError, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, attributeFormFieldLabel, attributeLinkUrl, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, circleRadiusValue, clearExternalNativePaintBridge, clearHistory, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, controlRendersLayer, copyableLayerStyleKind, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, ensureEditorTrackingFields, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, extractCopiedLayerStyle, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, formatCameraAltitude, formatExpressionPreviewValue, formatRoundNum, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getAttributeFormField, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, invertSelection, isAbsoluteFilesystemPath, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCredentialFieldName, isDefaultPrintLayout, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHexColor, isInitialLayerStyle, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isRegionalBasemapSentinel, isStyleLibraryTargetLayer, isStyleSpecColor, isVectorColorExpression, isochroneResponseToFeatures, layerGroupDepth, layerGroupMoveability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, mapZoomStepOutputs, matchFeaturesByExpression, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerLibraryEntries, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, parseContours, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseProject, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, removeTrailingJsonCommas, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveLayerCapabilities, resolveMapGrid, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, serializeLayerLibrary, serializeProject, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, simpleStyleNumberValue, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyMapHasContent, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, useAppStore, validateAttributeFormField, validateAttributeFormValues, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue };
|
|
13094
|
+
export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CHINA_BASEMAPS, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, 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, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeQuickFilters, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, attributeFormFieldLabel, attributeLinkUrl, basemapToCesiumImagery, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, circleRadiusValue, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileQuickFilter, compileQuickFilters, controlRendersLayer, copyableLayerStyleKind, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, ensureEditorTrackingFields, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, extractCopiedLayerStyle, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, formatCameraAltitude, formatExpressionPreviewValue, formatPixelValue, formatPopupValue, formatRoundNum, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveQuickFilter, hasAppPrivilege, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbsoluteFilesystemPath, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCredentialFieldName, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isPopupClickEnabled, isPopupHoverEnabled, isRegionalBasemapSentinel, isSafePopupUrl, isStyleLibraryTargetLayer, isStyleSpecColor, isVectorColorExpression, isochroneResponseToFeatures, layerGroupDepth, layerGroupMoveability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, mapZoomStepOutputs, matchFeaturesByExpression, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeBlankBackgroundColor, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerLibraryEntries, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, parseContours, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseProject, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, removeTrailingJsonCommas, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveLayerCapabilities, resolveMapGrid, resolvePopupBody, resolvePopupRows, resolvePopupTitle, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, sameCesiumImagery, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, serializeLayerLibrary, serializeProject, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, simpleStyleNumberValue, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyMapHasContent, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, useAppCapability, useAppStore, validateAttributeFormField, validateAttributeFormValues, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties };
|
|
12062
13095
|
|
|
12063
13096
|
//# sourceMappingURL=index.mjs.map
|