@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.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
- * Resolves runtime environment variables shared by the external-service clients
2275
- * (geocoding, routing). Build-time Vite vars (`import.meta.env`) are overlaid
2276
- * with project-supplied runtime vars (`window.__GEOLIBRE_RUNTIME_ENV__`, set
2277
- * from project preferences) so a self-hosted endpoint can be configured without
2278
- * a rebuild. Carries no React/MapLibre dependency so callers stay unit-testable.
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
- const buildEnv = import.meta.env;
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
- * The 3D-globe view's world imagery and terrain need a Cesium Ion token. It is
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). When unset, the globe cannot
2382
- * render and the 3D view is not offered.
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) if (ch === "\"") if (input[i + 1] === "\"") {
10857
- field += "\"";
10858
- i++;
10859
- } else inQuotes = false;
10860
- else field += ch;
10861
- else if (ch === "\"") inQuotes = true;
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