@geolibre/core 3.1.0 → 3.2.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
@@ -1,4 +1,4 @@
1
- import { Color, createExpression } from "@maplibre/maplibre-gl-style-spec";
1
+ import { Color, createExpression, featureFilter, latest, normalizePropertyExpression } from "@maplibre/maplibre-gl-style-spec";
2
2
  import { create } from "zustand";
3
3
  import { temporal } from "zundo";
4
4
  import { v4 } from "uuid";
@@ -4346,6 +4346,86 @@ function extrusionColorValue(style) {
4346
4346
  return parseJsonExpression(styleValue(style, "extrusionAdvancedStyleEnabled") ? styleValue(style, "extrusionColorExpression") : "") ?? flat;
4347
4347
  }
4348
4348
  //#endregion
4349
+ //#region src/style-layer-evaluator.ts
4350
+ const GEOMETRY_TYPE = {
4351
+ Point: 1,
4352
+ LineString: 2,
4353
+ Polygon: 3
4354
+ };
4355
+ /**
4356
+ * Compile a MapLibre style layer's filter and properties for evaluation
4357
+ * outside MapLibre, for a renderer that draws the layer itself (the ArcGIS
4358
+ * renderer drawing a plugin's own GeoJSON overlay, say). Uses the style spec's
4359
+ * own evaluator, so data-driven and zoom expressions, legacy functions and
4360
+ * defaults behave as they do on MapLibre. Each property compiles once.
4361
+ *
4362
+ * @param layer - The style layer (its `type`, `filter`, `paint`, `layout`).
4363
+ * @returns The layer's evaluator.
4364
+ */
4365
+ function createStyleLayerEvaluator(layer) {
4366
+ const root = `layers[${layer.id ?? ""}]`;
4367
+ let filter;
4368
+ try {
4369
+ const compiled = featureFilter(layer.filter, `${root}.filter`);
4370
+ filter = (zoom, feature) => compiled.filter({ zoom }, feature);
4371
+ } catch {
4372
+ filter = () => false;
4373
+ }
4374
+ const compiled = /* @__PURE__ */ new Map();
4375
+ const compile = (group, name) => {
4376
+ const key = `${group}/${name}`;
4377
+ if (compiled.has(key)) return compiled.get(key);
4378
+ const spec = latest[`${group}_${layer.type}`]?.[name];
4379
+ let evaluate = null;
4380
+ if (spec) {
4381
+ const raw = layer[group]?.[name];
4382
+ try {
4383
+ const expression = normalizePropertyExpression(raw ?? spec.default, `${root}.${group}.${name}`, spec);
4384
+ evaluate = (globals, feature) => expression.evaluate(globals, feature);
4385
+ } catch {
4386
+ evaluate = null;
4387
+ }
4388
+ }
4389
+ compiled.set(key, evaluate);
4390
+ return evaluate;
4391
+ };
4392
+ const input = (feature) => ({
4393
+ type: GEOMETRY_TYPE[feature.kind],
4394
+ id: feature.id,
4395
+ properties: feature.properties ?? {}
4396
+ });
4397
+ return {
4398
+ passes: (zoom, feature) => {
4399
+ try {
4400
+ return filter(zoom, input(feature));
4401
+ } catch {
4402
+ return false;
4403
+ }
4404
+ },
4405
+ value: (group, name, zoom, feature) => {
4406
+ const evaluate = compile(group, name);
4407
+ if (!evaluate) return void 0;
4408
+ let result;
4409
+ try {
4410
+ result = evaluate({ zoom }, input(feature));
4411
+ } catch {
4412
+ return;
4413
+ }
4414
+ if (result instanceof Color) {
4415
+ const [r, g, b, a] = result.rgb;
4416
+ return [
4417
+ Math.round(r * 255),
4418
+ Math.round(g * 255),
4419
+ Math.round(b * 255),
4420
+ a
4421
+ ];
4422
+ }
4423
+ if (result && typeof result === "object" && "sections" in result) return String(result);
4424
+ return result;
4425
+ }
4426
+ };
4427
+ }
4428
+ //#endregion
4349
4429
  //#region src/document-locale.ts
4350
4430
  /**
4351
4431
  * The document language, so formatted numbers and dates follow the UI locale.
@@ -4645,6 +4725,20 @@ function supportsBridgedOpacity(layerId) {
4645
4725
  function rendererAppliesOpacity(layer, renderer) {
4646
4726
  return (renderer === "arcgis" || renderer === "cesium") && layer.type === "zarr";
4647
4727
  }
4728
+ /**
4729
+ * Whether `layer` is an Add Vector Layer layer GeoLibre adopted
4730
+ * (`maplibre-gl-vector-adopted`, ADOPTED_VECTOR_SOURCE_KIND in
4731
+ * @geolibre/plugins) that has no features yet. A project saves a URL- or
4732
+ * path-backed adopted layer without them, and the vector control reads them
4733
+ * again on reopen. Until then its `source.url` may name GeoParquet or
4734
+ * GeoPackage, so a renderer must wait rather than load that URL as GeoJSON.
4735
+ *
4736
+ * @param layer - A store layer.
4737
+ * @returns True while the adopted layer awaits its features.
4738
+ */
4739
+ function isAdoptedVectorAwaitingFeatures(layer) {
4740
+ return layer.metadata.sourceKind === "maplibre-gl-vector-adopted" && !layer.geojson;
4741
+ }
4648
4742
  //#endregion
4649
4743
  //#region src/attribute-form.ts
4650
4744
  /** Look up the configuration for a field, if any. */
@@ -7340,6 +7434,134 @@ function scrubPrintLayoutForRemovedLayers(config, layerIds) {
7340
7434
  return clearBlocksForMissingLayers(config, (id) => removed.has(id));
7341
7435
  }
7342
7436
  //#endregion
7437
+ //#region src/store/session-slice.ts
7438
+ /** Reserved Identify target for querying every visible queryable layer at once. */
7439
+ const IDENTIFY_ALL_LAYERS_ID = "__geolibre_identify_all_layers__";
7440
+ /**
7441
+ * Resolve an Identify target against the layers that exist.
7442
+ *
7443
+ * Shared by the scripting `setIdentify` command and by project load, which
7444
+ * applies a project's saved `interaction.identify` (issue #2688).
7445
+ *
7446
+ * @param target - A layer id, `"all"`, a list of layer ids, or null/undefined.
7447
+ * @param layerIds - Ids of the layers currently in the project.
7448
+ * @returns The store fields to set. Ids that name no layer are dropped; a list
7449
+ * that keeps one id becomes a single-layer target, and one that keeps several
7450
+ * becomes the all-layers mode restricted to those ids.
7451
+ */
7452
+ function resolveIdentifyTarget(target, layerIds) {
7453
+ const off = {
7454
+ identifyLayerId: null,
7455
+ identifyLayerIds: null
7456
+ };
7457
+ if (target === null || target === void 0) return off;
7458
+ if (target === "all") return {
7459
+ identifyLayerId: IDENTIFY_ALL_LAYERS_ID,
7460
+ identifyLayerIds: null
7461
+ };
7462
+ const known = new Set(layerIds);
7463
+ if (typeof target === "string") return known.has(target) ? {
7464
+ identifyLayerId: target,
7465
+ identifyLayerIds: null
7466
+ } : off;
7467
+ const ids = [...new Set(target)].filter((id) => known.has(id));
7468
+ if (ids.length === 0) return off;
7469
+ if (ids.length === 1) return {
7470
+ identifyLayerId: ids[0],
7471
+ identifyLayerIds: null
7472
+ };
7473
+ return {
7474
+ identifyLayerId: IDENTIFY_ALL_LAYERS_ID,
7475
+ identifyLayerIds: ids
7476
+ };
7477
+ }
7478
+ /**
7479
+ * Whether the all-layers Identify mode may query a layer.
7480
+ *
7481
+ * @param layerId - The candidate layer's id.
7482
+ * @param identifyLayerIds - The store's `identifyLayerIds` restriction.
7483
+ * @returns True when there is no restriction or the layer is in it.
7484
+ */
7485
+ function identifyAllIncludes(layerId, identifyLayerIds) {
7486
+ return !identifyLayerIds || identifyLayerIds.includes(layerId);
7487
+ }
7488
+ /**
7489
+ * Drop removed layers from the Identify state.
7490
+ *
7491
+ * @param state - The current Identify fields.
7492
+ * @param removed - Ids of the layers being removed.
7493
+ * @returns The Identify fields with those layers gone. A restricted all-layers
7494
+ * mode left with one layer narrows to it, and with none turns Identify off.
7495
+ */
7496
+ function identifyStateWithoutLayers(state, removed) {
7497
+ if (state.identifyLayerId !== null && removed.has(state.identifyLayerId)) return {
7498
+ identifyLayerId: null,
7499
+ identifyLayerIds: null
7500
+ };
7501
+ const unchanged = {
7502
+ identifyLayerId: state.identifyLayerId,
7503
+ identifyLayerIds: state.identifyLayerIds
7504
+ };
7505
+ if (!state.identifyLayerIds) return unchanged;
7506
+ const ids = state.identifyLayerIds.filter((id) => !removed.has(id));
7507
+ if (ids.length === state.identifyLayerIds.length) return unchanged;
7508
+ if (ids.length === 0) return {
7509
+ identifyLayerId: null,
7510
+ identifyLayerIds: null
7511
+ };
7512
+ if (ids.length === 1) return {
7513
+ identifyLayerId: ids[0],
7514
+ identifyLayerIds: null
7515
+ };
7516
+ return {
7517
+ identifyLayerId: state.identifyLayerId,
7518
+ identifyLayerIds: ids
7519
+ };
7520
+ }
7521
+ const createSessionSlice = (set) => ({
7522
+ hoverTooltipsEnabled: true,
7523
+ setHoverTooltipsEnabled: (enabled) => set({ hoverTooltipsEnabled: enabled }),
7524
+ selectedLayerId: null,
7525
+ selectedFeatureId: null,
7526
+ selectedFeatureIds: [],
7527
+ identifyLayerId: null,
7528
+ identifyLayerIds: null,
7529
+ pointerCoords: null,
7530
+ pointerElevation: null,
7531
+ cameraAltitude: null,
7532
+ gpsStatus: null,
7533
+ attributeFilter: "",
7534
+ setPointerCoords: (coords) => set(coords ? { pointerCoords: coords } : {
7535
+ pointerCoords: null,
7536
+ pointerElevation: null
7537
+ }),
7538
+ setPointerElevation: (elevation) => set({ pointerElevation: elevation }),
7539
+ setCameraAltitude: (altitude) => set({ cameraAltitude: altitude }),
7540
+ setGpsStatus: (fix) => set({ gpsStatus: fix }),
7541
+ selectLayer: (id) => set({
7542
+ selectedLayerId: id,
7543
+ selectedFeatureId: null,
7544
+ selectedFeatureIds: []
7545
+ }),
7546
+ selectFeature: (id) => set({
7547
+ selectedFeatureId: id,
7548
+ selectedFeatureIds: id === null ? [] : [id]
7549
+ }),
7550
+ selectFeatures: (ids, anchorId) => set({
7551
+ selectedFeatureIds: ids,
7552
+ selectedFeatureId: anchorId != null && ids.includes(anchorId) ? anchorId : ids.at(-1) ?? null
7553
+ }),
7554
+ setIdentifyLayer: (id) => set({
7555
+ identifyLayerId: id,
7556
+ identifyLayerIds: null
7557
+ }),
7558
+ setIdentifyState: ({ identifyLayerId, identifyLayerIds }) => set({
7559
+ identifyLayerId,
7560
+ identifyLayerIds
7561
+ }),
7562
+ setAttributeFilter: (filter) => set({ attributeFilter: filter })
7563
+ });
7564
+ //#endregion
7343
7565
  //#region src/store/layer-removal.ts
7344
7566
  /**
7345
7567
  * Reference scrubbing shared by the actions that delete layers (`removeLayer`
@@ -7485,7 +7707,10 @@ const createLayerGroupsSlice = (set, get) => ({
7485
7707
  selectedLayerId: selectionRemoved ? layers[layers.length - 1]?.id ?? null : s.selectedLayerId,
7486
7708
  selectedFeatureId: selectionRemoved ? null : s.selectedFeatureId,
7487
7709
  selectedFeatureIds: selectionRemoved ? [] : s.selectedFeatureIds,
7488
- identifyLayerId: s.identifyLayerId !== null && removedIds.has(s.identifyLayerId) ? null : s.identifyLayerId,
7710
+ ...removeChildren ? identifyStateWithoutLayers(s, removedIds) : {
7711
+ identifyLayerId: s.identifyLayerId,
7712
+ identifyLayerIds: s.identifyLayerIds
7713
+ },
7489
7714
  ui: removeChildren ? {
7490
7715
  ...s.ui,
7491
7716
  selectByExpressionLayerId: s.ui.selectByExpressionLayerId !== null && removedIds.has(s.ui.selectByExpressionLayerId) ? null : s.ui.selectByExpressionLayerId,
@@ -8342,7 +8567,7 @@ const RASTER_APPEARANCE_STATE_KEYS = [
8342
8567
  "stretch",
8343
8568
  "gamma"
8344
8569
  ];
8345
- function isPlainObject$2(value) {
8570
+ function isPlainObject$3(value) {
8346
8571
  return typeof value === "object" && value !== null && !Array.isArray(value);
8347
8572
  }
8348
8573
  /**
@@ -8379,11 +8604,11 @@ function extractCopiedLayerStyle(layer) {
8379
8604
  };
8380
8605
  const rasterState = layer.metadata.rasterState;
8381
8606
  const rasterSymbology = layer.metadata.rasterSymbology;
8382
- const hasRasterSymbology = isPlainObject$2(rasterSymbology);
8607
+ const hasRasterSymbology = isPlainObject$3(rasterSymbology);
8383
8608
  return {
8384
8609
  kind,
8385
8610
  sourceName: layer.name,
8386
- rasterState: isPlainObject$2(rasterState) ? structuredClone(rasterState) : void 0,
8611
+ rasterState: isPlainObject$3(rasterState) ? structuredClone(rasterState) : void 0,
8387
8612
  hasRasterSymbology,
8388
8613
  ...hasRasterSymbology ? { rasterSymbology: structuredClone(rasterSymbology) } : {}
8389
8614
  };
@@ -8400,7 +8625,7 @@ function extractCopiedLayerStyle(layer) {
8400
8625
  function applyCopiedLayerStyle(target, copied) {
8401
8626
  if (copyableLayerStyleKind(target) !== copied.kind) return null;
8402
8627
  if (copied.kind === "vector") return { style: structuredClone(copied.style) };
8403
- const targetState = isPlainObject$2(target.metadata.rasterState) ? target.metadata.rasterState : {};
8628
+ const targetState = isPlainObject$3(target.metadata.rasterState) ? target.metadata.rasterState : {};
8404
8629
  const source = copied.rasterState ?? {};
8405
8630
  const expectedRescaleLength = rescaleEntryCount(targetState);
8406
8631
  const mergedState = { ...targetState };
@@ -8427,6 +8652,202 @@ function rescaleEntryCount(state) {
8427
8652
  return 1;
8428
8653
  }
8429
8654
  //#endregion
8655
+ //#region src/layer-style-file.ts
8656
+ /** `type` tag identifying a layer styles file. */
8657
+ const LAYER_STYLES_FILE_TYPE = "geolibre-layer-styles";
8658
+ /** Format version written by {@link serializeLayerStylesFile}. */
8659
+ const LAYER_STYLES_FILE_VERSION = 1;
8660
+ function isPlainObject$2(value) {
8661
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8662
+ }
8663
+ /**
8664
+ * Capture the style of every layer that has one. Layers without copyable
8665
+ * symbology (basemap tiles, 3D tiles, ...) are skipped. Layers that share a
8666
+ * name are all written, in stack order; the first one wins when the file is
8667
+ * applied (see {@link findLayerStyleEntryIndex}).
8668
+ *
8669
+ * @param layers - The layers to export, in store (bottom-to-top) order.
8670
+ * @returns The style entries.
8671
+ */
8672
+ function extractLayerStyleEntries(layers) {
8673
+ const entries = [];
8674
+ for (const layer of layers) {
8675
+ const copied = extractCopiedLayerStyle(layer);
8676
+ if (!copied) continue;
8677
+ if (copied.kind === "vector") entries.push({
8678
+ layerName: layer.name,
8679
+ kind: "vector",
8680
+ style: copied.style
8681
+ });
8682
+ else entries.push({
8683
+ layerName: layer.name,
8684
+ kind: "raster",
8685
+ ...copied.rasterState ? { rasterState: copied.rasterState } : {},
8686
+ ...isPlainObject$2(copied.rasterSymbology) ? { rasterSymbology: copied.rasterSymbology } : {}
8687
+ });
8688
+ }
8689
+ return entries;
8690
+ }
8691
+ /**
8692
+ * Serialize style entries into the JSON written by Export Layer Styles.
8693
+ *
8694
+ * @param entries - The entries to write.
8695
+ * @returns Pretty-printed file content.
8696
+ */
8697
+ function serializeLayerStylesFile(entries) {
8698
+ const file = {
8699
+ type: LAYER_STYLES_FILE_TYPE,
8700
+ version: 1,
8701
+ styles: [...entries]
8702
+ };
8703
+ return JSON.stringify(file, null, 2);
8704
+ }
8705
+ const RASTER_STRETCHES = [
8706
+ "linear",
8707
+ "log",
8708
+ "sqrt"
8709
+ ];
8710
+ const RASTER_NODATA_MODES = ["off", "auto"];
8711
+ function isFiniteNumber(value) {
8712
+ return typeof value === "number" && Number.isFinite(value);
8713
+ }
8714
+ /**
8715
+ * Validators for the raster appearance keys a style file may carry, by the
8716
+ * types `maplibre-gl-raster`'s `RasterLayerState` declares for them.
8717
+ */
8718
+ const RASTER_APPEARANCE_VALIDATORS = {
8719
+ colormap: (value) => typeof value === "string" && value.length > 0,
8720
+ reversed: (value) => typeof value === "boolean",
8721
+ rescale: (value) => value === null || Array.isArray(value) && value.length > 0 && value.every((range) => Array.isArray(range) && range.length === 2 && isFiniteNumber(range[0]) && isFiniteNumber(range[1])),
8722
+ nodata: (value) => isFiniteNumber(value) || RASTER_NODATA_MODES.includes(value),
8723
+ stretch: (value) => RASTER_STRETCHES.includes(value),
8724
+ gamma: (value) => isFiniteNumber(value) && value > 0
8725
+ };
8726
+ /**
8727
+ * Keep only the raster appearance keys a style applies, each with a value of
8728
+ * the type the raster renderer expects. The data selection (`mode`, `bands`,
8729
+ * ...) is dropped too: a style never applies it.
8730
+ */
8731
+ function sanitizeRasterAppearanceState(state) {
8732
+ const out = {};
8733
+ for (const key of RASTER_APPEARANCE_STATE_KEYS) if (key in state && RASTER_APPEARANCE_VALIDATORS[key](state[key])) out[key] = structuredClone(state[key]);
8734
+ return out;
8735
+ }
8736
+ /**
8737
+ * Coerce untrusted entries (a hand-edited file, or a copy kept in settings)
8738
+ * into clean ones. An entry without a name or a known kind is dropped; a
8739
+ * vector style is sanitized and completed against the default style, and a
8740
+ * raster state keeps only well-typed appearance keys, so applying an entry
8741
+ * never hands a renderer an unknown, missing or wrong-typed field.
8742
+ *
8743
+ * @param value - The raw `styles` array.
8744
+ * @returns The usable entries.
8745
+ */
8746
+ function normalizeLayerStyleEntries(value) {
8747
+ if (!Array.isArray(value)) return [];
8748
+ const entries = [];
8749
+ for (const item of value) {
8750
+ if (!isPlainObject$2(item)) continue;
8751
+ const layerName = typeof item.layerName === "string" ? item.layerName : "";
8752
+ if (!layerName.trim()) continue;
8753
+ if (item.kind === "vector") {
8754
+ if (!isPlainObject$2(item.style)) continue;
8755
+ entries.push({
8756
+ layerName,
8757
+ kind: "vector",
8758
+ style: {
8759
+ ...structuredClone(DEFAULT_LAYER_STYLE),
8760
+ ...sanitizeLayerStylePatch(item.style)
8761
+ }
8762
+ });
8763
+ } else if (item.kind === "raster") entries.push({
8764
+ layerName,
8765
+ kind: "raster",
8766
+ ...isPlainObject$2(item.rasterState) ? { rasterState: sanitizeRasterAppearanceState(item.rasterState) } : {},
8767
+ ...isPlainObject$2(item.rasterSymbology) ? { rasterSymbology: structuredClone(item.rasterSymbology) } : {}
8768
+ });
8769
+ }
8770
+ return entries;
8771
+ }
8772
+ /**
8773
+ * Parse a file written by {@link serializeLayerStylesFile}.
8774
+ *
8775
+ * @param json - The file content.
8776
+ * @returns The normalized entries.
8777
+ * @throws Error when the content is not a layer styles file, comes from a
8778
+ * newer format version, or holds no usable entries.
8779
+ */
8780
+ function parseLayerStylesFile(json) {
8781
+ let parsed;
8782
+ try {
8783
+ parsed = JSON.parse(json);
8784
+ } catch {
8785
+ throw new Error("Not a valid layer styles file (invalid JSON).");
8786
+ }
8787
+ if (!isPlainObject$2(parsed) || parsed.type !== "geolibre-layer-styles") throw new Error("Not a valid layer styles file.");
8788
+ if (parsed.version !== 1) throw new Error("Unsupported layer styles file version.");
8789
+ const entries = normalizeLayerStyleEntries(parsed.styles);
8790
+ if (entries.length === 0) throw new Error("The layer styles file holds no usable styles.");
8791
+ return entries;
8792
+ }
8793
+ function nameKey(name) {
8794
+ return name.trim().toLocaleLowerCase();
8795
+ }
8796
+ /**
8797
+ * Find the entry for a layer name: an exact match first, then one that differs
8798
+ * only in case or surrounding whitespace, so `Roads` still picks up a style
8799
+ * saved for `roads`. The first match in file order wins.
8800
+ *
8801
+ * @param entries - The style entries.
8802
+ * @param layerName - The layer name to look up.
8803
+ * @param kind - When given, only entries of this style family are considered,
8804
+ * so a raster style saved under a shared name does not shadow the vector one.
8805
+ * @returns The matching entry's index in `entries`, or -1.
8806
+ */
8807
+ function findLayerStyleEntryIndex(entries, layerName, kind) {
8808
+ const ofKind = (entry) => !kind || entry.kind === kind;
8809
+ const exact = entries.findIndex((entry) => ofKind(entry) && entry.layerName === layerName);
8810
+ if (exact >= 0) return exact;
8811
+ const key = nameKey(layerName);
8812
+ return entries.findIndex((entry) => ofKind(entry) && nameKey(entry.layerName) === key);
8813
+ }
8814
+ function toCopiedLayerStyle(entry) {
8815
+ if (entry.kind === "vector") return {
8816
+ kind: "vector",
8817
+ sourceName: entry.layerName,
8818
+ style: entry.style
8819
+ };
8820
+ return {
8821
+ kind: "raster",
8822
+ sourceName: entry.layerName,
8823
+ rasterState: entry.rasterState,
8824
+ hasRasterSymbology: entry.rasterSymbology !== void 0,
8825
+ ...entry.rasterSymbology !== void 0 ? { rasterSymbology: entry.rasterSymbology } : {}
8826
+ };
8827
+ }
8828
+ /**
8829
+ * Build the patch that restyles a layer from the entry matching its name, with
8830
+ * the same semantics as pasting a copied style: a vector style replaces the
8831
+ * whole style bag, a raster style merges its appearance keys and keeps the
8832
+ * layer's own band selection, and layer opacity is left alone.
8833
+ *
8834
+ * @param layer - The layer to restyle.
8835
+ * @param entries - The style entries.
8836
+ * @returns The matched entry and patch, or `null` when no entry of the layer's
8837
+ * style family matches its name (or the layer has no stylable symbology).
8838
+ */
8839
+ function matchLayerStyleEntry(layer, entries) {
8840
+ const kind = copyableLayerStyleKind(layer);
8841
+ if (!kind) return null;
8842
+ const entryIndex = findLayerStyleEntryIndex(entries, layer.name, kind);
8843
+ if (entryIndex < 0) return null;
8844
+ const patch = applyCopiedLayerStyle(layer, toCopiedLayerStyle(entries[entryIndex]));
8845
+ return patch ? {
8846
+ entryIndex,
8847
+ patch
8848
+ } : null;
8849
+ }
8850
+ //#endregion
8430
8851
  //#region src/store/layers-slice.ts
8431
8852
  /**
8432
8853
  * The layer types {@link AppState.addTileLayer} accepts. Each renders through
@@ -8476,7 +8897,7 @@ const createLayersSlice = (set, get) => ({
8476
8897
  selectedLayerId: s.selectedLayerId === id ? s.layers.find((l) => l.id !== id)?.id ?? null : s.selectedLayerId,
8477
8898
  selectedFeatureId: s.selectedLayerId === id ? null : s.selectedFeatureId,
8478
8899
  selectedFeatureIds: s.selectedLayerId === id ? [] : s.selectedFeatureIds,
8479
- identifyLayerId: s.identifyLayerId === id ? null : s.identifyLayerId,
8900
+ ...identifyStateWithoutLayers(s, /* @__PURE__ */ new Set([id])),
8480
8901
  ui: {
8481
8902
  ...s.ui,
8482
8903
  selectByExpressionLayerId: s.ui.selectByExpressionLayerId === id ? null : s.ui.selectByExpressionLayerId,
@@ -8551,6 +8972,29 @@ const createLayersSlice = (set, get) => ({
8551
8972
  get().updateLayer(id, patch);
8552
8973
  return true;
8553
8974
  },
8975
+ applyLayerStyleEntries: (entries, layerIds) => {
8976
+ if (entries.length === 0) return [];
8977
+ const scope = layerIds ? new Set(layerIds) : null;
8978
+ const applied = [];
8979
+ const layers = get().layers.map((layer) => {
8980
+ if (scope && !scope.has(layer.id)) return layer;
8981
+ const match = matchLayerStyleEntry(layer, entries);
8982
+ if (!match) return layer;
8983
+ applied.push({
8984
+ layerId: layer.id,
8985
+ entryIndex: match.entryIndex
8986
+ });
8987
+ return {
8988
+ ...layer,
8989
+ ...match.patch
8990
+ };
8991
+ });
8992
+ if (applied.length > 0) set({
8993
+ layers,
8994
+ isDirty: true
8995
+ });
8996
+ return applied;
8997
+ },
8554
8998
  reorderLayer: (id, direction) => set((s) => {
8555
8999
  const idx = s.layers.findIndex((l) => l.id === id);
8556
9000
  if (idx < 0) return s;
@@ -9043,6 +9487,82 @@ function createCesiumIonLayer(options) {
9043
9487
  };
9044
9488
  }
9045
9489
  //#endregion
9490
+ //#region src/header-references.ts
9491
+ /**
9492
+ * `${NAME}` references in request header values.
9493
+ *
9494
+ * A header such as `Authorization: Bearer ${TILES_TOKEN}` is saved as typed
9495
+ * and resolved from the project's Environment Variables when a request is
9496
+ * made, so the secret itself never sits on the layer. This module is pure (no
9497
+ * store import): `credentials.ts` depends on it, and the store depends on
9498
+ * `credentials.ts`.
9499
+ */
9500
+ /** `${NAME}` where NAME follows the environment-variable name rule. */
9501
+ const HEADER_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
9502
+ const HAS_HEADER_REFERENCE = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
9503
+ /**
9504
+ * Optional auth scheme word, whitespace, one reference, nothing else. Kept in
9505
+ * lockstep with `_HEADER_REFERENCE_ONLY` in `python/src/geolibre/project.py`.
9506
+ */
9507
+ const REFERENCE_ONLY = /^(?:[A-Za-z][A-Za-z0-9._-]*\s+)?\$\{[A-Za-z_][A-Za-z0-9_]*\}$/;
9508
+ const warnedMissing = /* @__PURE__ */ new Set();
9509
+ function hasHeaderReferences(headers) {
9510
+ if (!headers) return false;
9511
+ return Object.values(headers).some((value) => typeof value === "string" && HAS_HEADER_REFERENCE.test(value));
9512
+ }
9513
+ /**
9514
+ * Whether `value` carries no secret of its own: at most a scheme word such as
9515
+ * `Bearer` followed by a single `${NAME}`. Redaction keeps such values.
9516
+ */
9517
+ function isHeaderReferenceOnly(value) {
9518
+ return REFERENCE_ONLY.test(value.trim());
9519
+ }
9520
+ /**
9521
+ * Whether credential-bearing request headers may be sent to `url`: HTTPS, or
9522
+ * plain HTTP to loopback so a local dev server still works. The scheme is read
9523
+ * off a parsed URL, so an unusually cased `HTTPS://` is not misread as
9524
+ * plaintext; a relative or unparseable URL is refused.
9525
+ */
9526
+ function allowsCredentialHeaders(url) {
9527
+ try {
9528
+ const { protocol, hostname } = new URL(url);
9529
+ if (protocol === "https:") return true;
9530
+ return protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]");
9531
+ } catch {
9532
+ return false;
9533
+ }
9534
+ }
9535
+ /**
9536
+ * `headers` with every `${NAME}` replaced by `values[NAME]`. A header that
9537
+ * references an unset (or empty) variable is omitted rather than sent with a
9538
+ * hole in it. Returns the input unchanged when nothing references a variable,
9539
+ * and `undefined` when no header remains.
9540
+ */
9541
+ function resolveHeaderReferences(headers, values) {
9542
+ if (!headers) return void 0;
9543
+ if (!Object.values(headers).some((value) => typeof value === "string" && value.includes("${"))) return headers;
9544
+ const resolved = {};
9545
+ for (const [name, value] of Object.entries(headers)) {
9546
+ if (typeof value !== "string") continue;
9547
+ let missing = null;
9548
+ const next = value.replace(HEADER_REFERENCE, (_match, variable) => {
9549
+ const replacement = Object.hasOwn(values, variable) ? values[variable] : "";
9550
+ if (!replacement) missing ??= variable;
9551
+ return replacement;
9552
+ });
9553
+ if (missing !== null) {
9554
+ const warnKey = `${name}:${missing}`;
9555
+ if (!warnedMissing.has(warnKey)) {
9556
+ warnedMissing.add(warnKey);
9557
+ console.warn(`[GeoLibre] Request header "${name}" references unset environment variable ${missing}; the header is not sent.`);
9558
+ }
9559
+ continue;
9560
+ }
9561
+ resolved[name] = next;
9562
+ }
9563
+ return Object.keys(resolved).length > 0 ? resolved : void 0;
9564
+ }
9565
+ //#endregion
9046
9566
  //#region src/credentials.ts
9047
9567
  /**
9048
9568
  * Project fields whose values are credentials. Keep this registry as the
@@ -9095,7 +9615,8 @@ const PUBLISHABLE_PLUGIN_SETTINGS = {
9095
9615
  "maplibre-gl-swipe": null,
9096
9616
  "maplibre-gl-components": ["legend", "colorbar"],
9097
9617
  "maplibre-gl-time-slider": null,
9098
- "gods-eye-view": null
9618
+ "gods-eye-view": null,
9619
+ "geolibre-point-cloud-annotation": null
9099
9620
  };
9100
9621
  /**
9101
9622
  * Hash a credential value to a short, stable 64-bit token: two independent
@@ -9234,6 +9755,16 @@ function redactConfigurationValue(value, path, accumulator, depth = 0) {
9234
9755
  const result = {};
9235
9756
  for (const [key, nested] of Object.entries(value)) {
9236
9757
  const nestedPath = path ? `${path}.${key}` : key;
9758
+ const normalizedKey = normalizeCredentialName(key);
9759
+ if ((normalizedKey === "requestheaders" || normalizedKey === "headers") && isPlainObject$1(nested)) {
9760
+ const kept = {};
9761
+ const removed = {};
9762
+ for (const [name, headerValue] of Object.entries(nested)) if (typeof headerValue === "string" && isHeaderReferenceOnly(headerValue)) kept[name] = headerValue;
9763
+ else removed[name] = headerValue;
9764
+ if (Object.keys(removed).length > 0) recordRedaction(accumulator, nestedPath, removed);
9765
+ if (Object.keys(kept).length > 0) result[key] = kept;
9766
+ continue;
9767
+ }
9237
9768
  if (isCredentialFieldName(key)) {
9238
9769
  recordRedaction(accumulator, nestedPath, nested);
9239
9770
  continue;
@@ -9288,14 +9819,14 @@ function redactProjectCredentials(project) {
9288
9819
  if (redactedMapboxStyleUrl !== mapboxStyleUrl) recordRedaction(accumulator, "preferences.map.mapboxStyleUrl", mapboxStyleUrl);
9289
9820
  const preferences = project.preferences ? {
9290
9821
  ...project.preferences,
9291
- environmentVariables: [],
9822
+ environmentVariables: (project.preferences.environmentVariables ?? []).filter((variable) => variable.secret === false),
9292
9823
  geocoding,
9293
9824
  ...redactedMapboxStyleUrl !== mapboxStyleUrl ? { map: {
9294
9825
  ...project.preferences.map,
9295
9826
  mapboxStyleUrl: redactedMapboxStyleUrl
9296
9827
  } } : {}
9297
9828
  } : project.preferences;
9298
- const populatedEnvironmentVariables = project.preferences?.environmentVariables?.filter((variable) => variable.key.trim()) ?? [];
9829
+ const populatedEnvironmentVariables = project.preferences?.environmentVariables?.filter((variable) => variable.secret !== false && variable.value !== "") ?? [];
9299
9830
  if (populatedEnvironmentVariables.length > 0) recordRedaction(accumulator, "preferences.environmentVariables", populatedEnvironmentVariables, populatedEnvironmentVariables.length);
9300
9831
  const geocodingApiKeys = project.preferences?.geocoding?.apiKeys ?? {};
9301
9832
  if (Object.keys(geocodingApiKeys).length > 0) recordRedaction(accumulator, "preferences.geocoding.apiKeys", geocodingApiKeys, Object.keys(geocodingApiKeys).length);
@@ -10280,6 +10811,7 @@ function parseProject(json) {
10280
10811
  const { mapLayout, secondaryMapViews } = resolveMapGrid(normalizeMapLayout(data.mapLayout), normalizeSecondaryMapViews(data.secondaryMapViews), { mapView });
10281
10812
  const styleLibrary = normalizeStyleLibraryEntries(data.styleLibrary);
10282
10813
  const parsedComments = normalizeProjectComments(data.comments);
10814
+ const parsedInteraction = normalizeProjectInteraction(data.interaction);
10283
10815
  return {
10284
10816
  version: data.version,
10285
10817
  name: data.name,
@@ -10309,6 +10841,7 @@ function parseProject(json) {
10309
10841
  ...normalizePrimaryRenderer(data.primaryRenderer) ? { primaryRenderer: normalizePrimaryRenderer(data.primaryRenderer) } : {},
10310
10842
  ...styleLibrary.length > 0 ? { styleLibrary } : {},
10311
10843
  ...parsedComments.length > 0 ? { comments: parsedComments } : {},
10844
+ ...parsedInteraction ? { interaction: parsedInteraction } : {},
10312
10845
  metadata: data.metadata ?? {}
10313
10846
  };
10314
10847
  }
@@ -10969,6 +11502,34 @@ function normalizeDashboardColumns(value) {
10969
11502
  if (typeof value !== "number" || !Number.isFinite(value)) return 2;
10970
11503
  return Math.max(1, Math.min(6, Math.trunc(value)));
10971
11504
  }
11505
+ /**
11506
+ * Coerce an untrusted `interaction` block into a {@link ProjectInteraction}.
11507
+ *
11508
+ * Layer ids are kept even when no such layer exists: the block is replayed
11509
+ * against whatever layers the project has when it opens, and the loader drops
11510
+ * the ones it cannot find. Control names are kept as written for the same
11511
+ * reason; the app ignores names it does not know.
11512
+ *
11513
+ * @param value - Raw `interaction` value from the project JSON or the store.
11514
+ * @returns The normalized block, or null when it carries nothing to apply.
11515
+ */
11516
+ function normalizeProjectInteraction(value) {
11517
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
11518
+ const raw = value;
11519
+ const result = {};
11520
+ if (raw.identify === null) result.identify = null;
11521
+ else if (typeof raw.identify === "string" && raw.identify) result.identify = raw.identify;
11522
+ else if (Array.isArray(raw.identify)) {
11523
+ const ids = [...new Set(raw.identify.filter((id) => typeof id === "string" && !!id))];
11524
+ if (ids.length > 0) result.identify = ids;
11525
+ }
11526
+ if (raw.controls && typeof raw.controls === "object" && !Array.isArray(raw.controls)) {
11527
+ const controls = {};
11528
+ for (const [name, shown] of Object.entries(raw.controls)) if (name && typeof shown === "boolean") controls[name] = shown;
11529
+ if (Object.keys(controls).length > 0) result.controls = controls;
11530
+ }
11531
+ return "identify" in result || result.controls ? result : null;
11532
+ }
10972
11533
  function normalizeProjectPreferences(preferences) {
10973
11534
  if (!preferences || typeof preferences !== "object") return DEFAULT_PROJECT_PREFERENCES;
10974
11535
  const candidate = preferences;
@@ -11055,7 +11616,8 @@ function normalizeEnvironmentVariable(variable) {
11055
11616
  return {
11056
11617
  key,
11057
11618
  value: typeof candidate.value === "string" ? candidate.value : "",
11058
- enabled: normalizeBoolean(candidate.enabled, true)
11619
+ enabled: normalizeBoolean(candidate.enabled, true),
11620
+ ...candidate.secret === false ? { secret: false } : {}
11059
11621
  };
11060
11622
  }
11061
11623
  const PROJECT_PLUGIN_CONTROL_POSITIONS = /* @__PURE__ */ new Set([
@@ -11251,6 +11813,7 @@ function projectFromStore(state) {
11251
11813
  const processingHistory = normalizeProcessingHistory(state.processingHistory);
11252
11814
  const widgets = normalizeWidgets(state.widgets);
11253
11815
  const comments = normalizeProjectComments(state.comments);
11816
+ const interaction = normalizeProjectInteraction(state.interaction);
11254
11817
  const dashboardColumns = state.dashboardColumns === void 0 ? 2 : normalizeDashboardColumns(state.dashboardColumns);
11255
11818
  const layerGroups = state.layerGroups ?? [];
11256
11819
  const { mapLayout, secondaryMapViews } = resolveMapGrid(normalizeMapLayout(state.mapLayout), normalizeSecondaryMapViews(state.secondaryMapViews), { mapView: state.mapView });
@@ -11286,6 +11849,7 @@ function projectFromStore(state) {
11286
11849
  ...normalizePrimaryRenderer(state.primaryRenderer) ? { primaryRenderer: normalizePrimaryRenderer(state.primaryRenderer) } : {},
11287
11850
  ...styleLibrary.length > 0 ? { styleLibrary } : {},
11288
11851
  ...comments.length > 0 ? { comments } : {},
11852
+ ...interaction ? { interaction } : {},
11289
11853
  metadata: state.metadata
11290
11854
  };
11291
11855
  }
@@ -11327,6 +11891,10 @@ function prepareLayerForSave(layer) {
11327
11891
  const { geojson: _geojson, ...rest } = layer;
11328
11892
  layer = rest;
11329
11893
  }
11894
+ if (layer.geojson && layer.metadata.sourceKind === "maplibre-gl-vector-adopted" && hasRestorableSourceUrl(layer)) {
11895
+ const { geojson: _geojson, ...rest } = layer;
11896
+ layer = rest;
11897
+ }
11330
11898
  if (layer.geojson && layer.metadata.localFileReloadable === true) {
11331
11899
  const { geojson: _geojson, ...rest } = layer;
11332
11900
  layer = rest;
@@ -11447,6 +12015,7 @@ function applyProjectToStore(project) {
11447
12015
  primaryRenderer: normalizePrimaryRenderer(project.primaryRenderer) ?? "maplibre",
11448
12016
  projectStyleLibrary: normalizeStyleLibraryEntries(project.styleLibrary),
11449
12017
  comments: scrubbedComments,
12018
+ projectInteraction: normalizeProjectInteraction(project.interaction),
11450
12019
  metadata: project.metadata
11451
12020
  };
11452
12021
  }
@@ -11789,6 +12358,7 @@ const createProjectContentSlice = (set) => ({
11789
12358
  widgets: [],
11790
12359
  dashboardColumns: 2,
11791
12360
  comments: [],
12361
+ projectInteraction: null,
11792
12362
  addComment: (comment) => set((s) => {
11793
12363
  if (s.comments.some((c) => c.id === comment.id)) return s;
11794
12364
  return {
@@ -12368,6 +12938,7 @@ const createProjectSlice = (set, get) => ({
12368
12938
  const applied = applyProjectToStore(createEmptyProject(options.name, options));
12369
12939
  set((s) => ({
12370
12940
  ...applied,
12941
+ hoverTooltipsEnabled: true,
12371
12942
  projectPath: null,
12372
12943
  projectGeneration: s.projectGeneration + 1,
12373
12944
  isDirty: false,
@@ -12375,6 +12946,7 @@ const createProjectSlice = (set, get) => ({
12375
12946
  selectedFeatureId: null,
12376
12947
  selectedFeatureIds: [],
12377
12948
  identifyLayerId: null,
12949
+ identifyLayerIds: null,
12378
12950
  copiedLayerStyle: null,
12379
12951
  pointerCoords: null,
12380
12952
  pointerElevation: null,
@@ -12405,13 +12977,14 @@ const createProjectSlice = (set, get) => ({
12405
12977
  const selectedLayerId = project.selectedLayerId === null ? null : typeof project.selectedLayerId === "string" && applied.layers.some((layer) => layer.id === project.selectedLayerId) ? project.selectedLayerId : applied.layers[0]?.id ?? null;
12406
12978
  set((s) => ({
12407
12979
  ...applied,
12980
+ hoverTooltipsEnabled: true,
12408
12981
  projectPath: path,
12409
12982
  projectGeneration: s.projectGeneration + 1,
12410
12983
  isDirty: false,
12411
12984
  selectedLayerId,
12412
12985
  selectedFeatureId: null,
12413
12986
  selectedFeatureIds: [],
12414
- identifyLayerId: null,
12987
+ ...resolveIdentifyTarget(applied.projectInteraction?.identify, applied.layers.map((layer) => layer.id)),
12415
12988
  copiedLayerStyle: null,
12416
12989
  pointerCoords: null,
12417
12990
  pointerElevation: null,
@@ -12441,43 +13014,6 @@ const createProjectSlice = (set, get) => ({
12441
13014
  }
12442
13015
  });
12443
13016
  //#endregion
12444
- //#region src/store/session-slice.ts
12445
- /** Reserved Identify target for querying every visible queryable layer at once. */
12446
- const IDENTIFY_ALL_LAYERS_ID = "__geolibre_identify_all_layers__";
12447
- const createSessionSlice = (set) => ({
12448
- selectedLayerId: null,
12449
- selectedFeatureId: null,
12450
- selectedFeatureIds: [],
12451
- identifyLayerId: null,
12452
- pointerCoords: null,
12453
- pointerElevation: null,
12454
- cameraAltitude: null,
12455
- gpsStatus: null,
12456
- attributeFilter: "",
12457
- setPointerCoords: (coords) => set(coords ? { pointerCoords: coords } : {
12458
- pointerCoords: null,
12459
- pointerElevation: null
12460
- }),
12461
- setPointerElevation: (elevation) => set({ pointerElevation: elevation }),
12462
- setCameraAltitude: (altitude) => set({ cameraAltitude: altitude }),
12463
- setGpsStatus: (fix) => set({ gpsStatus: fix }),
12464
- selectLayer: (id) => set({
12465
- selectedLayerId: id,
12466
- selectedFeatureId: null,
12467
- selectedFeatureIds: []
12468
- }),
12469
- selectFeature: (id) => set({
12470
- selectedFeatureId: id,
12471
- selectedFeatureIds: id === null ? [] : [id]
12472
- }),
12473
- selectFeatures: (ids, anchorId) => set({
12474
- selectedFeatureIds: ids,
12475
- selectedFeatureId: anchorId != null && ids.includes(anchorId) ? anchorId : ids.at(-1) ?? null
12476
- }),
12477
- setIdentifyLayer: (id) => set({ identifyLayerId: id }),
12478
- setAttributeFilter: (filter) => set({ attributeFilter: filter })
12479
- });
12480
- //#endregion
12481
13017
  //#region src/store/ui-slice.ts
12482
13018
  const createUiSlice = (set) => ({
12483
13019
  ui: {
@@ -12825,20 +13361,36 @@ var ElevationFetchError = class extends Error {
12825
13361
  }
12826
13362
  };
12827
13363
  /**
13364
+ * Normalize longitude into [-180, 180] degrees (handles unwrapped coordinates
13365
+ * from antimeridian cross-overs and world-copy pannings).
13366
+ */
13367
+ function normalizeLongitude(lng) {
13368
+ if (!Number.isFinite(lng)) return 0;
13369
+ return ((lng + 180) % 360 + 360) % 360 - 180;
13370
+ }
13371
+ /**
13372
+ * Clamp latitude into [-90, 90] degrees.
13373
+ */
13374
+ function clampLatitude(lat) {
13375
+ if (!Number.isFinite(lat)) return 0;
13376
+ return Math.min(90, Math.max(-90, lat));
13377
+ }
13378
+ /**
12828
13379
  * Fetch elevations (in meters) for an ordered list of coordinates.
12829
13380
  *
12830
13381
  * @param points - Coordinates as `[lng, lat]`, at most {@link MAX_POINTS_PER_REQUEST}
12831
13382
  * @param fetchImpl - Optional `fetch` implementation; defaults to the global `fetch`
12832
13383
  * @returns The elevation in meters for each input coordinate, in the same order
12833
- * @throws {ElevationFetchError} On too many points, a network error, a non-2xx
12834
- * response, a malformed body, or a length mismatch
13384
+ * @throws {ElevationFetchError} On too many points, a non-finite coordinate, a
13385
+ * network error, a non-2xx response, a malformed body, or a length mismatch
12835
13386
  */
12836
13387
  async function fetchElevations(points, fetchImpl) {
12837
13388
  if (points.length === 0) return [];
12838
13389
  if (points.length > 100) throw new ElevationFetchError(`Too many points: ${points.length} (max 100).`);
13390
+ if (points.some(([lng, lat]) => !Number.isFinite(lng) || !Number.isFinite(lat))) throw new ElevationFetchError("Invalid coordinate: longitude and latitude must be finite.");
12839
13391
  const doFetch = fetchImpl ?? ((url, init) => fetch(url, init));
12840
- const latitudes = points.map((p) => p[1].toFixed(6)).join(",");
12841
- const longitudes = points.map((p) => p[0].toFixed(6)).join(",");
13392
+ const latitudes = points.map((p) => clampLatitude(p[1]).toFixed(6)).join(",");
13393
+ const longitudes = points.map((p) => normalizeLongitude(p[0]).toFixed(6)).join(",");
12842
13394
  const url = `${ENDPOINT}?latitude=${latitudes}&longitude=${longitudes}`;
12843
13395
  const controller = new AbortController();
12844
13396
  const timeoutId = setTimeout(() => controller.abort(), ELEVATION_REQUEST_TIMEOUT_MS);
@@ -13059,6 +13611,408 @@ function formatPixelValue(value) {
13059
13611
  return String(Number(value.toPrecision(6)));
13060
13612
  }
13061
13613
  //#endregion
13614
+ //#region src/s3.ts
13615
+ /** SigV4 allows a presigned URL to live at most seven days. */
13616
+ const S3_MAX_PRESIGN_SECONDS = 604800;
13617
+ const S3_SCHEME = /^s3a?:\/\//i;
13618
+ const VIRTUAL_HOST = /^(.+)\.s3(?:[.-](?:dualstack\.)?([a-z0-9-]+))?\.amazonaws\.com(?:\.cn)?$/i;
13619
+ const PATH_HOST = /^s3(?:[.-](?:dualstack\.)?([a-z0-9-]+))?\.amazonaws\.com(?:\.cn)?$/i;
13620
+ function regionFromHost(value) {
13621
+ if (!value || value === "external-1" || value === "dualstack") return void 0;
13622
+ return value.toLowerCase();
13623
+ }
13624
+ /** Static-website endpoints serve HTML, not the REST API a signature targets. */
13625
+ function isWebsiteRegion(value) {
13626
+ return Boolean(value && /^website/i.test(value));
13627
+ }
13628
+ function decodeKey(path) {
13629
+ try {
13630
+ return decodeURIComponent(path);
13631
+ } catch {
13632
+ return path;
13633
+ }
13634
+ }
13635
+ /**
13636
+ * Parses an `s3://bucket/key` URI or an AWS S3 HTTPS object URL
13637
+ * (virtual-hosted or path-style, any region) into its bucket and key.
13638
+ * Query strings and fragments are ignored, so a presigned URL parses to the
13639
+ * object it signs.
13640
+ *
13641
+ * @param url The URL to parse.
13642
+ * @returns The bucket and key, or null when the URL is not an S3 object URL.
13643
+ */
13644
+ function parseS3Url(url) {
13645
+ const trimmed = url.trim();
13646
+ if (S3_SCHEME.test(trimmed)) {
13647
+ const rest = trimmed.replace(S3_SCHEME, "").split(/[?#]/)[0];
13648
+ const slash = rest.indexOf("/");
13649
+ const bucket = slash === -1 ? rest : rest.slice(0, slash);
13650
+ const key = slash === -1 ? "" : rest.slice(slash + 1);
13651
+ return isSafeBucketName(bucket) ? {
13652
+ bucket,
13653
+ key
13654
+ } : null;
13655
+ }
13656
+ let parsed;
13657
+ try {
13658
+ parsed = new URL(trimmed);
13659
+ } catch {
13660
+ return null;
13661
+ }
13662
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
13663
+ const host = parsed.hostname.toLowerCase();
13664
+ const path = parsed.pathname.replace(/^\//, "");
13665
+ const virtual = host.match(VIRTUAL_HOST);
13666
+ if (virtual && !PATH_HOST.test(host)) {
13667
+ if (isWebsiteRegion(virtual[2])) return null;
13668
+ const region = regionFromHost(virtual[2]);
13669
+ return {
13670
+ bucket: virtual[1],
13671
+ key: decodeKey(path),
13672
+ ...region ? { region } : {}
13673
+ };
13674
+ }
13675
+ const pathStyle = host.match(PATH_HOST);
13676
+ if (pathStyle) {
13677
+ if (isWebsiteRegion(pathStyle[1])) return null;
13678
+ const slash = path.indexOf("/");
13679
+ const bucket = slash === -1 ? path : path.slice(0, slash);
13680
+ if (!bucket) return null;
13681
+ const region = regionFromHost(pathStyle[1]);
13682
+ return {
13683
+ bucket: decodeKey(bucket),
13684
+ key: slash === -1 ? "" : decodeKey(path.slice(slash + 1)),
13685
+ ...region ? { region } : {}
13686
+ };
13687
+ }
13688
+ return null;
13689
+ }
13690
+ /**
13691
+ * Bucket names are letters, digits, dots, dashes, and underscores (the last
13692
+ * two outside AWS's own rules, for S3-compatible stores). Anything else, a
13693
+ * backslash or `@` say, could move a virtual-hosted URL's authority to
13694
+ * another host before a signature is attached, so it is refused outright.
13695
+ */
13696
+ const SAFE_BUCKET_NAME = /^[A-Za-z0-9._-]{1,255}$/;
13697
+ function isSafeBucketName(bucket) {
13698
+ return SAFE_BUCKET_NAME.test(bucket);
13699
+ }
13700
+ /** Whether `url` is an `s3://` URI (as opposed to an HTTPS object URL). */
13701
+ function isS3Uri(url) {
13702
+ return S3_SCHEME.test(url.trim());
13703
+ }
13704
+ /** Formats a location as an `s3://bucket/key` URI. */
13705
+ function formatS3Uri(location) {
13706
+ return `s3://${location.bucket}/${location.key}`;
13707
+ }
13708
+ /**
13709
+ * URI-encodes a key the way SigV4 canonicalizes an S3 path: every byte except
13710
+ * the unreserved set and `/` is percent-encoded, once.
13711
+ */
13712
+ function encodeS3Key(key) {
13713
+ return key.split("/").map((segment) => encodeURIComponent(segment).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`)).join("/");
13714
+ }
13715
+ /** A custom endpoint's path, without a trailing slash ("" for none). */
13716
+ function endpointPathPrefix(base) {
13717
+ return base.pathname.replace(/\/+$/, "");
13718
+ }
13719
+ /**
13720
+ * Builds the HTTPS URL of an object.
13721
+ *
13722
+ * AWS buckets whose name contains a dot are addressed path-style, since the
13723
+ * `*.s3.amazonaws.com` wildcard certificate covers one label only.
13724
+ *
13725
+ * @param location The bucket and key.
13726
+ * @param config The endpoint; without one the global AWS endpoint is used.
13727
+ * @returns The unsigned object URL.
13728
+ */
13729
+ function s3ObjectHttpsUrl(location, config = {}) {
13730
+ if (location.bucket && !isSafeBucketName(location.bucket)) throw new Error(`Invalid S3 bucket name: ${location.bucket}`);
13731
+ const key = encodeS3Key(location.key);
13732
+ const endpoint = config.endpoint?.trim().replace(/\/+$/, "");
13733
+ if (!location.bucket) {
13734
+ if (endpoint) {
13735
+ const base = new URL(endpoint.includes("://") ? endpoint : `https://${endpoint}`);
13736
+ return `${base.protocol}//${base.host}${endpointPathPrefix(base)}/`;
13737
+ }
13738
+ const serviceRegion = config.region || location.region;
13739
+ return serviceRegion && serviceRegion !== "us-east-1" ? `https://s3.${serviceRegion}.amazonaws.com/` : "https://s3.amazonaws.com/";
13740
+ }
13741
+ if (endpoint) {
13742
+ const base = new URL(endpoint.includes("://") ? endpoint : `https://${endpoint}`);
13743
+ const prefix = endpointPathPrefix(base);
13744
+ return config.pathStyle ? `${base.protocol}//${base.host}${prefix}/${encodeS3Key(location.bucket)}/${key}` : `${base.protocol}//${location.bucket}.${base.host}${prefix}/${key}`;
13745
+ }
13746
+ const region = config.region || location.region;
13747
+ const host = region && region !== "us-east-1" ? `s3.${region}.amazonaws.com` : "s3.amazonaws.com";
13748
+ if (config.pathStyle || location.bucket.includes(".")) return `https://${host}/${encodeS3Key(location.bucket)}/${key}`;
13749
+ return `https://${location.bucket}.${host}/${key}`;
13750
+ }
13751
+ const encoder = new TextEncoder();
13752
+ function toHex(buffer) {
13753
+ return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, "0")).join("");
13754
+ }
13755
+ async function sha256Hex(value) {
13756
+ return toHex(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
13757
+ }
13758
+ async function hmac(key, value) {
13759
+ const cryptoKey = await crypto.subtle.importKey("raw", key, {
13760
+ name: "HMAC",
13761
+ hash: "SHA-256"
13762
+ }, false, ["sign"]);
13763
+ return crypto.subtle.sign("HMAC", cryptoKey, encoder.encode(value));
13764
+ }
13765
+ /** RFC 3986 encoding, as SigV4 requires for query names and values. */
13766
+ function encodeRfc3986(value) {
13767
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
13768
+ }
13769
+ function amzDate(date) {
13770
+ const stamp = date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}/, "");
13771
+ return {
13772
+ stamp,
13773
+ day: stamp.slice(0, 8)
13774
+ };
13775
+ }
13776
+ /**
13777
+ * Presigns an S3 request with AWS Signature Version 4 in the query string.
13778
+ * Runs on WebCrypto, so it works in the browser, the Tauri webview, and Node.
13779
+ *
13780
+ * The URL authorizes exactly one method: a GET URL cannot answer a HEAD, so
13781
+ * readers that probe with HEAD need a GET range request instead.
13782
+ *
13783
+ * @param options The object URL, region, credentials, and lifetime.
13784
+ * @returns The presigned URL.
13785
+ */
13786
+ async function presignS3Url(options) {
13787
+ const { credentials, region } = options;
13788
+ const method = (options.method ?? "GET").toUpperCase();
13789
+ const url = new URL(options.url);
13790
+ const { stamp, day } = amzDate(options.now ?? /* @__PURE__ */ new Date());
13791
+ const scope = `${day}/${region}/s3/aws4_request`;
13792
+ const expiresIn = Math.max(1, Math.min(S3_MAX_PRESIGN_SECONDS, Math.floor(options.expiresIn)));
13793
+ const params = [];
13794
+ url.searchParams.forEach((value, name) => {
13795
+ if (!name.toLowerCase().startsWith("x-amz-")) params.push([name, value]);
13796
+ });
13797
+ params.push(["X-Amz-Algorithm", "AWS4-HMAC-SHA256"], ["X-Amz-Credential", `${credentials.accessKeyId}/${scope}`], ["X-Amz-Date", stamp], ["X-Amz-Expires", String(expiresIn)], ["X-Amz-SignedHeaders", "host"]);
13798
+ if (credentials.sessionToken) params.push(["X-Amz-Security-Token", credentials.sessionToken]);
13799
+ const canonicalQuery = params.map(([name, value]) => [encodeRfc3986(name), encodeRfc3986(value)]).sort(([a, av], [b, bv]) => a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0).map(([name, value]) => `${name}=${value}`).join("&");
13800
+ const canonicalPath = url.pathname || "/";
13801
+ const stringToSign = [
13802
+ "AWS4-HMAC-SHA256",
13803
+ stamp,
13804
+ scope,
13805
+ await sha256Hex([
13806
+ method,
13807
+ canonicalPath,
13808
+ canonicalQuery,
13809
+ `host:${url.host}\n`,
13810
+ "host",
13811
+ "UNSIGNED-PAYLOAD"
13812
+ ].join("\n"))
13813
+ ].join("\n");
13814
+ const signature = toHex(await hmac(await hmac(await hmac(await hmac(await hmac(encoder.encode(`AWS4${credentials.secretAccessKey}`), day), region), "s3"), "aws4_request"), stringToSign));
13815
+ return `${url.origin}${canonicalPath}?${canonicalQuery}&X-Amz-Signature=${signature}`;
13816
+ }
13817
+ let activeSigner = null;
13818
+ /** Presigned URL → the unsigned URL it was minted for. Bounded. */
13819
+ const sourceBySignedHref = /* @__PURE__ */ new Map();
13820
+ const SIGNED_SOURCE_LIMIT = 500;
13821
+ /** Installs (or with null, removes) the app's S3 signer. */
13822
+ function registerS3UrlSigner(signer) {
13823
+ activeSigner = signer;
13824
+ }
13825
+ /**
13826
+ * Whether reading `url` needs the S3 signer: it names an S3 object in a bucket
13827
+ * a configured connection covers.
13828
+ */
13829
+ function isCredentialedS3Url(url) {
13830
+ if (!activeSigner) return false;
13831
+ const location = parseS3Url(url);
13832
+ return Boolean(location && activeSigner.covers(location.bucket));
13833
+ }
13834
+ /** Records that `signedHref` reads `sourceUrl`, so a layer can keep the latter. */
13835
+ function rememberSignedSource(signedHref, sourceUrl) {
13836
+ sourceBySignedHref.delete(signedHref);
13837
+ if (sourceBySignedHref.size >= SIGNED_SOURCE_LIMIT) {
13838
+ const oldest = sourceBySignedHref.keys().next().value;
13839
+ if (oldest !== void 0) sourceBySignedHref.delete(oldest);
13840
+ }
13841
+ sourceBySignedHref.set(signedHref, sourceUrl);
13842
+ }
13843
+ /** Removes the SigV4 query parameters (credential, token, signature) from a URL. */
13844
+ function stripPresignParameters(url) {
13845
+ try {
13846
+ const parsed = new URL(url);
13847
+ for (const name of [...parsed.searchParams.keys()]) if (name.toLowerCase().startsWith("x-amz-")) parsed.searchParams.delete(name);
13848
+ return parsed.href;
13849
+ } catch {
13850
+ return url;
13851
+ }
13852
+ }
13853
+ function unsignedSourceUrl(url) {
13854
+ if (url === void 0) return void 0;
13855
+ const known = sourceBySignedHref.get(url);
13856
+ if (known !== void 0) return known;
13857
+ if (!/[?&]x-amz-signature=/i.test(url)) return url;
13858
+ const location = parseS3Url(url);
13859
+ return location && !/[?#]/.test(location.key) ? formatS3Uri(location) : stripPresignParameters(url);
13860
+ }
13861
+ /**
13862
+ * Resolves a data URL into one a plain HTTP reader can fetch:
13863
+ *
13864
+ * - an S3 object in a bucket a configured connection covers → a presigned URL;
13865
+ * - any other `s3://` URI → the public HTTPS object URL (anonymous read);
13866
+ * - anything else → unchanged.
13867
+ *
13868
+ * @param url The URL a layer or query names.
13869
+ * @param signal Aborts a pending credential lookup.
13870
+ * @returns The URL to hand the reader.
13871
+ * @throws When the bucket is covered but its credentials cannot be resolved.
13872
+ */
13873
+ async function resolveReadableUrl(url, signal) {
13874
+ const location = parseS3Url(url);
13875
+ if (!location) return url;
13876
+ if (activeSigner?.covers(location.bucket)) {
13877
+ const signed = await activeSigner.presign({
13878
+ bucket: location.bucket,
13879
+ key: location.key,
13880
+ region: location.region
13881
+ }, signal);
13882
+ if (signed) {
13883
+ rememberSignedSource(signed.href, url);
13884
+ return signed.href;
13885
+ }
13886
+ }
13887
+ return isS3Uri(url) ? s3ObjectHttpsUrl(location) : url;
13888
+ }
13889
+ /** The installed signer, for the S3 browser. */
13890
+ function getS3UrlSigner() {
13891
+ return activeSigner;
13892
+ }
13893
+ function decodeXmlText(value) {
13894
+ return value.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCodePoint(parseInt(code, 16))).replace(/&amp;/g, "&");
13895
+ }
13896
+ function xmlTag(block, tag) {
13897
+ const match = block.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
13898
+ return match ? decodeXmlText(match[1]) : void 0;
13899
+ }
13900
+ function xmlBlocks(xml, tag) {
13901
+ return Array.from(xml.matchAll(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "g")), (m) => m[1]);
13902
+ }
13903
+ /**
13904
+ * Parses a ListObjectsV2 XML body. S3's listing XML is flat and predictable,
13905
+ * so this reads it with patterns rather than an XML DOM, which keeps it usable
13906
+ * in workers and Node tests.
13907
+ */
13908
+ function parseS3ListObjects(xml) {
13909
+ const prefixes = xmlBlocks(xml, "CommonPrefixes").map((block) => xmlTag(block, "Prefix")).filter((value) => Boolean(value));
13910
+ const objects = xmlBlocks(xml, "Contents").flatMap((block) => {
13911
+ const key = xmlTag(block, "Key");
13912
+ if (key === void 0) return [];
13913
+ const lastModified = xmlTag(block, "LastModified");
13914
+ return [{
13915
+ key,
13916
+ size: Number(xmlTag(block, "Size") ?? 0),
13917
+ ...lastModified ? { lastModified } : {}
13918
+ }];
13919
+ });
13920
+ const truncated = xmlTag(xml, "IsTruncated") === "true";
13921
+ const token = xmlTag(xml, "NextContinuationToken");
13922
+ return {
13923
+ prefixes,
13924
+ objects,
13925
+ ...truncated && token ? { nextContinuationToken: token } : {}
13926
+ };
13927
+ }
13928
+ /** The error code and message of an S3 XML error body, when it is one. */
13929
+ function parseS3Error(xml) {
13930
+ const block = xmlBlocks(xml, "Error")[0];
13931
+ if (block === void 0) return null;
13932
+ const endpoint = xmlTag(block, "Endpoint");
13933
+ const region = xmlTag(block, "Region") ?? (endpoint ? parseS3Url(`https://${endpoint}/`)?.region : void 0);
13934
+ return {
13935
+ code: xmlTag(block, "Code") ?? "Error",
13936
+ message: xmlTag(block, "Message") ?? "",
13937
+ ...region ? { region } : {}
13938
+ };
13939
+ }
13940
+ /** Bucket names from a ListBuckets (`GET /` on the service) XML body. */
13941
+ function parseS3BucketList(xml) {
13942
+ return xmlBlocks(xml, "Bucket").map((block) => xmlTag(block, "Name")).filter((name) => Boolean(name));
13943
+ }
13944
+ /**
13945
+ * How browsers word a request they blocked or could not complete, plus the
13946
+ * readers that already guess at CORS themselves (maplibre-gl-lidar's message
13947
+ * tells the user to use a file picker, which is wrong for a bucket).
13948
+ */
13949
+ const NETWORK_FAILURE = /failed to fetch|networkerror|load failed|network request failed|err_failed|cross-origin|\bcors\b/i;
13950
+ function errorText(error) {
13951
+ const parts = [];
13952
+ let current = error;
13953
+ for (let depth = 0; current && depth < 4; depth += 1) {
13954
+ parts.push(current instanceof Error ? `${current.name}: ${current.message}` : String(current));
13955
+ current = current instanceof Error ? current.cause : null;
13956
+ }
13957
+ return parts.join(" | ");
13958
+ }
13959
+ /** A read the bucket's CORS configuration blocked. */
13960
+ var S3CorsError = class extends Error {
13961
+ bucket;
13962
+ origin;
13963
+ constructor(message, bucket, origin, cause) {
13964
+ super(message, { cause });
13965
+ this.name = "S3CorsError";
13966
+ this.bucket = bucket;
13967
+ this.origin = origin;
13968
+ }
13969
+ };
13970
+ /**
13971
+ * Explains why reading an S3 object failed when the browser only said
13972
+ * "Failed to fetch". A browser reports a request the bucket's CORS rules
13973
+ * blocked exactly like a network outage, so this repeats the request twice:
13974
+ * normally, where success means the failure was transient; then in `no-cors`
13975
+ * mode, which CORS cannot block. Only when the first fails and the second gets
13976
+ * an answer is the bucket's CORS configuration the cause.
13977
+ *
13978
+ * @param url The `s3://` URI or object URL that was being read.
13979
+ * @param error What the reader threw.
13980
+ * @param translate Turns the message into the UI language.
13981
+ * @returns An {@link S3CorsError} naming the bucket and the origin to allow,
13982
+ * or `error` unchanged when it is not a CORS refusal of an S3 read.
13983
+ */
13984
+ async function explainS3ReadError(url, error, translate) {
13985
+ const location = parseS3Url(url);
13986
+ if (!location || error instanceof S3CorsError) return error;
13987
+ if (!NETWORK_FAILURE.test(errorText(error))) return error;
13988
+ if (typeof fetch !== "function" || typeof window === "undefined") return error;
13989
+ const controller = new AbortController();
13990
+ const timer = setTimeout(() => controller.abort(), 1e4);
13991
+ try {
13992
+ const href = await resolveReadableUrl(url);
13993
+ const probe = {
13994
+ signal: controller.signal,
13995
+ cache: "no-store"
13996
+ };
13997
+ if (await fetch(href, probe).then(() => true, () => false)) return error;
13998
+ await fetch(href, {
13999
+ ...probe,
14000
+ mode: "no-cors"
14001
+ });
14002
+ } catch {
14003
+ return error;
14004
+ } finally {
14005
+ controller.abort();
14006
+ clearTimeout(timer);
14007
+ }
14008
+ const origin = window.location.origin;
14009
+ const fallback = `The bucket "${location.bucket}" does not allow reads from ${origin}: its CORS configuration is missing or does not list this origin. Add a CORS rule that allows GET and HEAD from this origin and exposes Content-Length, Content-Range, and ETag (see Settings > Cloud Storage).`;
14010
+ return new S3CorsError(translate ? translate("s3Browser.corsError", fallback, {
14011
+ bucket: location.bucket,
14012
+ origin
14013
+ }) : fallback, location.bucket, origin, error);
14014
+ }
14015
+ //#endregion
13062
14016
  //#region src/storymap-sample.ts
13063
14017
  const SAMPLE_ASSET_BASE = "https://opengeos.org/maplibre-gl-storymaps/assets";
13064
14018
  /**
@@ -14465,6 +15419,184 @@ function persistedThreeDTilesRequestHeaders(url, headers) {
14465
15419
  return nonEmptyRecord(stripGoogleMapsApiKeyHeader(headers));
14466
15420
  }
14467
15421
  //#endregion
15422
+ //#region src/project-credential-refs.ts
15423
+ /**
15424
+ * Credentials a project refers to by name rather than carrying inline.
15425
+ *
15426
+ * On the desktop app the geocoding API keys and secret environment variables
15427
+ * of a project live in the OS keychain, keyed device-wide by provider id /
15428
+ * variable name. The project file keeps the names with empty values. Request
15429
+ * headers never go there: they reference variables as `${NAME}` instead (see
15430
+ * `resolveProjectHeaderReferences`). This module is the pure half of that
15431
+ * scheme: the account names, the overlay that fills empty values from the
15432
+ * stored ones, and the split that moves values out of a project before it is
15433
+ * written. The app installs the lookup; without one (web, Jupyter) every
15434
+ * function here is the identity, so values stay in the project file.
15435
+ *
15436
+ * A non-empty value in the project always wins over the stored one: a file
15437
+ * that still carries plaintext (older files, the web build) is used as-is for
15438
+ * the session.
15439
+ */
15440
+ function geocodingApiKeyAccount(providerId) {
15441
+ return `project.geocoding.apiKey.${providerId}`;
15442
+ }
15443
+ function environmentVariableAccount(key) {
15444
+ return `project.env.${key}`;
15445
+ }
15446
+ /** Rows are secret unless explicitly marked `secret: false`. */
15447
+ function isSecretEnvironmentVariable(variable) {
15448
+ return variable.secret !== false;
15449
+ }
15450
+ let projectCredentialLookup = null;
15451
+ /** Install (or clear, with `null`) the source of stored project credentials. */
15452
+ function setProjectCredentialLookup(lookup) {
15453
+ projectCredentialLookup = lookup;
15454
+ }
15455
+ /** The stored value for `account`, or `undefined` when none (or no lookup). */
15456
+ function lookupProjectCredential(account) {
15457
+ const value = projectCredentialLookup?.(account);
15458
+ return value ? value : void 0;
15459
+ }
15460
+ /** Fill empty geocoding API keys from the stored ones. */
15461
+ function overlayStoredGeocodingApiKeys(geocoding) {
15462
+ let apiKeys = null;
15463
+ for (const { id } of GEOCODING_PROVIDERS) {
15464
+ if (geocoding.apiKeys[id]?.trim()) continue;
15465
+ const stored = lookupProjectCredential(geocodingApiKeyAccount(id));
15466
+ if (!stored) continue;
15467
+ apiKeys ??= { ...geocoding.apiKeys };
15468
+ apiKeys[id] = stored;
15469
+ }
15470
+ return apiKeys ? {
15471
+ ...geocoding,
15472
+ apiKeys
15473
+ } : geocoding;
15474
+ }
15475
+ /** Fill empty secret environment variable values from the stored ones. */
15476
+ function overlayStoredEnvironmentVariables(variables) {
15477
+ let changed = false;
15478
+ const overlaid = variables.map((variable) => {
15479
+ if (!isSecretEnvironmentVariable(variable) || variable.value !== "") return variable;
15480
+ const key = variable.key.trim();
15481
+ const stored = key ? lookupProjectCredential(environmentVariableAccount(key)) : void 0;
15482
+ if (!stored) return variable;
15483
+ changed = true;
15484
+ return {
15485
+ ...variable,
15486
+ value: stored
15487
+ };
15488
+ });
15489
+ return changed ? overlaid : variables;
15490
+ }
15491
+ /** Preferences with every empty stored credential filled in. */
15492
+ function overlayStoredPreferenceCredentials(preferences) {
15493
+ const geocoding = overlayStoredGeocodingApiKeys(preferences.geocoding);
15494
+ const environmentVariables = overlayStoredEnvironmentVariables(preferences.environmentVariables);
15495
+ if (geocoding === preferences.geocoding && environmentVariables === preferences.environmentVariables) return preferences;
15496
+ return {
15497
+ ...preferences,
15498
+ geocoding,
15499
+ environmentVariables
15500
+ };
15501
+ }
15502
+ /**
15503
+ * The credential changes an edited Settings draft makes, keyed by account
15504
+ * (`""` deletes).
15505
+ *
15506
+ * - `seeded`: the overlaid preferences the dialog opened with.
15507
+ * - `next`: the normalized draft being saved.
15508
+ *
15509
+ * Only values the user changed are returned, so an untouched session override
15510
+ * (plaintext from the opened file) stays out of the keychain and opening a
15511
+ * file never writes it. Removing, renaming, or un-secreting a variable never
15512
+ * deletes its stored value: names are shared by every project on the device.
15513
+ * Clearing a field deletes the stored value only when the field showed that
15514
+ * stored value; clearing a file override that differed from it just drops the
15515
+ * override, so the shared value applies again and other projects keep it.
15516
+ */
15517
+ function changedPreferenceCredentials(seeded, next) {
15518
+ const changes = {};
15519
+ const record = (account, seededValue, nextValue) => {
15520
+ if (nextValue === seededValue) return;
15521
+ if (nextValue === "" && seededValue !== lookupProjectCredential(account)) return;
15522
+ changes[account] = nextValue;
15523
+ };
15524
+ const providerIds = /* @__PURE__ */ new Set([...Object.keys(seeded.geocoding.apiKeys), ...Object.keys(next.geocoding.apiKeys)]);
15525
+ for (const providerId of providerIds) record(geocodingApiKeyAccount(providerId), seeded.geocoding.apiKeys[providerId]?.trim() ?? "", next.geocoding.apiKeys[providerId]?.trim() ?? "");
15526
+ const seededSecrets = /* @__PURE__ */ new Map();
15527
+ for (const variable of seeded.environmentVariables) {
15528
+ if (!isSecretEnvironmentVariable(variable) || seededSecrets.has(variable.key)) continue;
15529
+ seededSecrets.set(variable.key, variable.value);
15530
+ }
15531
+ for (const variable of next.environmentVariables) {
15532
+ if (!isSecretEnvironmentVariable(variable)) continue;
15533
+ const seededValue = seededSecrets.get(variable.key);
15534
+ if (seededValue === void 0 && variable.value === "") continue;
15535
+ record(environmentVariableAccount(variable.key), seededValue ?? "", variable.value);
15536
+ }
15537
+ return changes;
15538
+ }
15539
+ /**
15540
+ * Move unambiguous geocoding API keys and secret environment variable values
15541
+ * out of `project` (a copy; the input is untouched) into `secrets`, keyed by
15542
+ * account. Secret environment rows that have no unique name remain in the
15543
+ * project for an explicit keep/strip choice. Layers pass through: their
15544
+ * request headers reference variables as `${NAME}` rather than holding
15545
+ * secrets that need moving.
15546
+ */
15547
+ function splitProjectCredentials(project) {
15548
+ const secrets = {};
15549
+ for (const [providerId, apiKey] of Object.entries(project.preferences.geocoding.apiKeys)) {
15550
+ const trimmed = apiKey.trim();
15551
+ if (trimmed) secrets[geocodingApiKeyAccount(providerId)] = trimmed;
15552
+ }
15553
+ const nameCounts = /* @__PURE__ */ new Map();
15554
+ for (const variable of project.preferences.environmentVariables) {
15555
+ const key = variable.key.trim();
15556
+ if (key) nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);
15557
+ }
15558
+ const environmentVariables = project.preferences.environmentVariables.map((variable) => {
15559
+ if (!isSecretEnvironmentVariable(variable) || variable.value === "") return variable;
15560
+ const key = variable.key.trim();
15561
+ if (!key || (nameCounts.get(key) ?? 0) > 1) return variable;
15562
+ secrets[environmentVariableAccount(key)] = variable.value;
15563
+ return {
15564
+ ...variable,
15565
+ value: ""
15566
+ };
15567
+ });
15568
+ return {
15569
+ project: {
15570
+ ...project,
15571
+ preferences: {
15572
+ ...project.preferences,
15573
+ geocoding: {
15574
+ ...project.preferences.geocoding,
15575
+ apiKeys: {}
15576
+ },
15577
+ environmentVariables
15578
+ }
15579
+ },
15580
+ secrets
15581
+ };
15582
+ }
15583
+ /**
15584
+ * `headers` with every `${NAME}` resolved from the project's enabled
15585
+ * Environment Variables (stored secret values filled in). Only those rows
15586
+ * count, not the OS environment or build-time values that share the runtime
15587
+ * env. A header referencing an unset variable is dropped.
15588
+ */
15589
+ function resolveProjectHeaderReferences(headers) {
15590
+ if (!headers) return headers;
15591
+ const values = {};
15592
+ const variables = overlayStoredEnvironmentVariables(useAppStore.getState().preferences.environmentVariables);
15593
+ for (const variable of variables) {
15594
+ const key = variable.key.trim();
15595
+ if (variable.enabled && key) values[key] = variable.value;
15596
+ }
15597
+ return resolveHeaderReferences(headers, values);
15598
+ }
15599
+ //#endregion
14468
15600
  //#region src/visibility.ts
14469
15601
  /**
14470
15602
  * Returns a new FeatureCollection with properties marked as "excluded" removed.
@@ -14744,6 +15876,6 @@ function currentEditorIdentity() {
14744
15876
  return pickEditorIdentity(isActive ? selfName : void 0, readStoredAuthorName());
14745
15877
  }
14746
15878
  //#endregion
14747
- export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CESIUM_BASEMAPS, CESIUM_BING_AERIAL_ASSET_ID, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID, CESIUM_ION_QUICK_PICKS, CESIUM_ION_SOURCE_KIND, CESIUM_KML_SOURCE_KIND, CESIUM_OSM_BUILDINGS_ASSET_ID, CHINA_BASEMAPS, CZML_QUICK_PICKS, CZML_SOURCE_KIND, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRIMARY_RENDERER, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, DEPLOYMENT_CAPABILITIES, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, IDENTIFY_ALL_LAYERS_ID, LABEL_NUMBER_LOCALES, LABEL_NUMBER_SAMPLE, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, NO_LAYERS, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, POPUP_IMAGE_HEIGHT_RANGE, POPUP_MAX_WIDTH_RANGE, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeLayerFilterExpression, activeQuickFilters, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, applyStoryLayerOpacity, attributeFormFieldLabel, attributeLinkUrl, availableCesiumBasemap, basemapToCesiumImagery, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, cesiumIonAssetId, cesiumIonAssetKind, cesiumKmlSource, circleRadiusValue, clampLabelDecimals, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileLayerFilters, compileQuickFilter, compileQuickFilters, controlRendersLayer, copyableLayerStyleKind, createCesiumIonLayer, createCesiumKmlLayer, createCzmlLayer, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectLayerSerializationCache, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, czmlSource, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, documentLocale, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, ensureEditorTrackingFields, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, extractCopiedLayerStyle, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, formatCameraAltitude, formatExpressionPreviewValue, formatLabelNumber, formatLabelNumberSample, formatPixelValue, formatPopupValue, formatRoundNum, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getArcgisApiKey, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveLayerFilter, hasActiveQuickFilter, hasAppPrivilege, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, heatmapRampColors, horizontalBbox, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbsoluteFilesystemPath, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCesiumIonLayer, isCesiumKmlLayer, isCesiumOnlyLayer, isCredentialFieldName, isCzmlLayer, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isPopupClickEnabled, isPopupHoverEnabled, isRegionalBasemapSentinel, isSafePopupUrl, isStoryHiddenLayer, isStyleLibraryTargetLayer, isStyleSpecColor, isVectorColorExpression, isochroneResponseToFeatures, labelFieldTextField, layerGroupDepth, layerGroupMoveability, layerGroupSortability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, localFileName, mapZoomStepOutputs, matchFeaturesByExpression, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeBlankBackgroundColor, normalizeCesiumBasemap, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerLibraryEntries, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrimaryRenderer, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, parseCesiumIonAssetId, parseContours, parseCzml, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseProject, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, portableWmsTileUrl, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, removeTrailingJsonCommas, rendererAppliesOpacity, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveLabelNumberLocale, resolveLayerCapabilities, resolveMapGrid, resolvePopupBody, resolvePopupImageHeight, resolvePopupMaxWidth, resolvePopupRows, resolvePopupTitle, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, sameCesiumImagery, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, selectLayerById, selectLayerIds, selectLayerSummaries, selectLayersWhen, serializeLayerLibrary, serializeProject, serializeProjectWithLayerCache, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, simpleStyleNumberValue, sortLayerGroupInPanel, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyLayerOpacityFactor, storyMapHasContent, storyVisibleLayers, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, uniqueImportedLayerName, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, useAppCapability, useAppStore, useLayer, useLayerIds, useLayerSummaries, useLayersWhen, validateAttributeFormField, validateAttributeFormValues, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties };
15879
+ export { ALL_APP_PRIVILEGES, ALL_DEPLOYMENT_CAPABILITIES, BLANK_BASEMAP, BLEND_MODES, BUILT_IN_STYLE_PRESETS, CARTOCIUDAD_MIN_INTERVAL_MS, CARTOCIUDAD_PUBLIC_HOST, CESIUM_BASEMAPS, CESIUM_BING_AERIAL_ASSET_ID, CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID, CESIUM_ION_QUICK_PICKS, CESIUM_ION_SOURCE_KIND, CESIUM_KML_SOURCE_KIND, CESIUM_OSM_BUILDINGS_ASSET_ID, CHINA_BASEMAPS, CZML_QUICK_PICKS, CZML_SOURCE_KIND, DEFAULT_BASEMAP, DEFAULT_BLEND_MODE, DEFAULT_COLLABORATION_STATE, DEFAULT_DASHBOARD_COLUMNS, DEFAULT_EDITOR_IDENTITY, DEFAULT_EDITOR_TRACKING_CONFIG, DEFAULT_ELLIPSOID_ID, DEFAULT_FORWARD_GEOCODE_ENDPOINT, DEFAULT_GEOCODING_PROVIDER_ID, DEFAULT_LAYER_GROUP_OPACITY, DEFAULT_LAYER_STYLE, DEFAULT_LEGEND_CONFIG, DEFAULT_MAP_GRID_LAYOUT, DEFAULT_PRIMARY_RENDERER, DEFAULT_PRINT_LAYOUT, DEFAULT_PROJECT_NAME, DEFAULT_PROJECT_PREFERENCES, DEFAULT_REVERSE_GEOCODE_ENDPOINT, DEFAULT_ROUTING_ENDPOINT, DEFAULT_STORY_MAP, DEPLOYMENT_CAPABILITIES, DUCKDB_VECTOR_FEATURE_WARN_COUNT, DUCKDB_VECTOR_ROUTE_BYTES, EARTH_MEAN_RADIUS_METERS, EDITOR_IDENTITY_STORAGE_KEY, ELEVATION_REQUEST_TIMEOUT_MS, ELLIPSOIDS, EXPRESSION_FUNCTION_CATEGORIES, ElevationFetchError, FEET_PER_METER, FEET_PER_MILE, GEOCODE_DISPLAY_NAME_KEY, GEOCODE_LAT_KEY, GEOCODE_LON_KEY, GEOCODE_PROVIDER_KEY, GEOCODE_SCORE_KEY, GEOCODE_STATUS_KEY, GEOCODING_PROVIDERS, GOOGLE_MAPS_API_KEY_HEADER, IDENTIFY_ALL_LAYERS_ID, LABEL_NUMBER_LOCALES, LABEL_NUMBER_SAMPLE, LARGE_VECTOR_FEATURE_THRESHOLD, LAYER_LIBRARY_BUNDLE_TYPE, LAYER_LIBRARY_BUNDLE_VERSION, LAYER_PALETTE, LAYER_STYLES_FILE_TYPE, LAYER_STYLES_FILE_VERSION, LAYER_TYPES, MAX_DASHBOARD_COLUMNS, MAX_DIAGRAM_FEATURES, MAX_DIAGRAM_SCAN_FEATURES, MAX_LAYER_LIBRARY_ENTRIES, MAX_LAYER_LIBRARY_ENTRY_BYTES, MAX_MAP_GRID_DIM, MAX_POINTS_PER_REQUEST, MAX_PROCESSING_HISTORY, MAX_REDACT_DEPTH, MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0, METERS_PER_FOOT, METERS_PER_MILE, METERS_PER_NAUTICAL_MILE, MIN_DASHBOARD_COLUMNS, MIN_DIAGRAM_SIZE, NETCDF_IMAGE_SOURCE_KIND, NOMINATIM_MIN_INTERVAL_MS, NOMINATIM_PUBLIC_HOST, NO_DEPLOYMENT_CAPABILITIES, NO_LAYERS, OPENFREEMAP_BASEMAPS, PHOTO_FULL_PROPERTY, PHOTO_PROPERTY, PLANETARY_BASEMAPS, PLANETARY_BASEMAP_GROUPS, PLANETARY_BASEMAP_SENTINEL_PREFIX, PLANET_SWITCHER_OPTIONS, POINTER_ELEVATION_CACHE_LIMIT, POINTER_ELEVATION_DEBOUNCE_MS, POPUP_IMAGE_HEIGHT_RANGE, POPUP_MAX_WIDTH_RANGE, PROJECT_CREDENTIAL_FIELDS, PROJECT_VERSION, PROTOMAPS_BASEMAPS, PUBLIC_GEOCODE_ROW_CAP, PUBLISHABLE_PLUGIN_SETTINGS, RAMP_STYLE_KEYS, RASTER_APPEARANCE_STATE_KEYS, REGIONAL_BASEMAPS, REGIONAL_BASEMAP_GROUPS, REGIONAL_BASEMAP_SENTINEL_PREFIX, ROLE_PRIVILEGES, RoutingRequestError, S3CorsError, S3_MAX_PRESIGN_SECONDS, SELECTION_MODES, SQL_QUERY_SOURCE_KIND, STYLE_LIBRARY_BUNDLE_TYPE, STYLE_LIBRARY_BUNDLE_VERSION, SYMBOL_STYLE_KEYS, VECTOR_COLOR_RAMPS, activeLayerFilterExpression, activeQuickFilters, allowsCredentialHeaders, appPrivilegeReason, applyCopiedLayerStyle, applyGroupEffects, applyJoinsToLayer, applyLayerJoins, applyLayerVirtualFields, applyMatchedSelection, applyProjectToStore, applySelectionMode, applyStoryLayerOpacity, attributeFormFieldLabel, attributeLinkUrl, availableCesiumBasemap, basemapToCesiumImagery, batchDecodePolylines, bodyLengthToEarth, buildForwardGeocodeUrl, buildIsochroneRequest, buildLayerPanelUnits, buildLayerTree, buildMatrixRequest, buildReverseGeocodeUrl, buildRouteRequest, canRedoProjectRestore, canSaveLayerToLibrary, canUndoProjectRestore, captureLayerLibraryEntry, cascadeLayerJoinRefresh, cesiumIonAssetId, cesiumIonAssetKind, cesiumKmlSource, changedPreferenceCredentials, circleRadiusValue, clampLabelDecimals, clampLatitude, clearExternalNativePaintBridge, clearHistory, clearQuickFilterValues, coerceAttributeFormValue, collectDiagramData, collectTransitiveJoinSourceIds, compareSequenceValues, compileFeatureExpression, compileLayerFilters, compileQuickFilter, compileQuickFilters, controlRendersLayer, copyableLayerStyleKind, createCesiumIonLayer, createCesiumKmlLayer, createCzmlLayer, createDefaultAppCapabilities, createDefaultMapView, createDefaultPrintLayout, createEmptyProject, createEqualIntervalBreaks, createGraduatedClassBreaks, createLayerLibraryEntryId, createPointerElevationResolver, createProjectLayerSerializationCache, createProjectTemplate, createQuantileBreaks, createSampleStoryMap, createStyleLayerEvaluator, createStyleLibraryEntryId, csvRowsToGeocodeRequests, currentEditorIdentity, czmlSource, darkenHex, decodePolyline, decodePolylineDetailed, detachProjectCopy, detectNonGeographicCoordinates, diagramAnchor, diagramPixelSize, diagramsSuppressedByPointRenderer, documentLocale, dominantGeometry, drawMarkerPath, earthAreaToBody, earthLengthToBody, editorTrackingFieldNames, effectiveLayerRenderState, effectiveVectorRules, encodePolyline, encodeS3Key, ensureEditorTrackingFields, environmentVariableAccount, evaluateMapExpression, excludeHiddenFieldsFromGeojson, excludeHiddenFieldsFromProject, explainS3ReadError, extractCopiedLayerStyle, extractLayerStyleEntries, extractStyleLibraryStyle, extrusionColorValue, extrusionHeightValue, featureSelectionId, fetchElevations, findLayerStyleEntryIndex, formatCameraAltitude, formatExpressionPreviewValue, formatLabelNumber, formatLabelNumberSample, formatPixelValue, formatPopupValue, formatRoundNum, formatS3Uri, generatorCircleRadiusValue, generatorSizeRange, geoJSONToPolylineStr, geocodeForward, geocodeMatchToFeature, geocodeReverse, geocoderMinIntervalMs, geocoderNeedsApiKey, geocodingApiKeyAccount, geojsonHasZCoordinates, getActiveBodyRadiusRatio, getActiveEllipsoid, getActiveMeanRadiusMeters, getActiveSemiMajorAxisMeters, getArcgisApiKey, getAttributeFormField, getBuildEnvironment, getCesiumIonToken, getEllipsoid, getExternalNativePaintBridge, getGeocoderConfig, getGeocodingProvider, getGoogleMapsApiKey, getHistoryCoalesceMs, getMapboxAccessToken, getMaxHistoryFeatureCount, getPlanetaryBasemapById, getPlanetaryBasemapByStyleUrl, getProtomapsApiKey, getProtomapsStyleUrl, getRegionalBasemapById, getRegionalBasemapByStyleUrl, getRoundNum, getRoutingConfig, getRuntimeEnvironment, getS3UrlSigner, getSpatialExtensionPath, getVectorColorRamp, googleMapsApiKeyHeaderValue, groupMemberIndices, hasActiveLayerFilter, hasActiveQuickFilter, hasAppPrivilege, hasHeaderReferences, hasPathTraversal, hasRestorableLayerSource, hasSimpleStyleProperties, heatmapRampColors, horizontalBbox, identifyAllIncludes, identifyStateWithoutLayers, inferFieldTypes, inferLayerCapabilities, initialLayerStyle, interpolateColors, interpolateHexColor, interpolateRampColors, intersectPrivileges, invertSelection, isAbsoluteFilesystemPath, isAdoptedVectorAwaitingFeatures, isAllowedPluginManifestUrl, isAttributeFormFieldVisible, isCesiumIonLayer, isCesiumKmlLayer, isCesiumOnlyLayer, isCredentialFieldName, isCredentialedS3Url, isCzmlLayer, isDefaultPrintLayout, isDeploymentCapability, isDiagramStyleEnabled, isDuckDBQueryLayer, isExternalNativeLayerRecord, isGooglePhotorealisticTilesetUrl, isHeaderReferenceOnly, isHexColor, isInitialLayerStyle, isInlineImageValue, isInternalPopupField, isIpadDesktopUserAgent, isMaintainedEditorTrackingField, isPopupClickEnabled, isPopupHoverEnabled, isRegionalBasemapSentinel, isS3Uri, isSafePopupUrl, isSecretEnvironmentVariable, isStoryHiddenLayer, isStyleLibraryTargetLayer, isStyleSpecColor, isVectorColorExpression, isochroneResponseToFeatures, labelFieldTextField, layerGroupDepth, layerGroupMoveability, layerGroupSortability, layerJoinKey, layerLibraryConfigPatch, layerLibraryEntryNeedsLocalFile, layerPanelGroupHeaders, lineWidthValue, localFileName, lookupProjectCredential, mapZoomStepOutputs, matchFeaturesByExpression, matchLayerStyleEntry, matrixResponseToFeatures, meanRadiusMeters, mercatorMetersPerPixelAtZoom0, metersWidthExpression, nextDelayMs, nextLayerPaletteColor, nominatimResultToFeature, nominatimReverseResultToDisplay, nonEmptyRecord, normalizeAppPrivileges, normalizeBlankBackgroundColor, normalizeCesiumBasemap, normalizeDashboardColumns, normalizeGeocodingProviderId, normalizeGroupContiguity, normalizeHexColor, normalizeLayerCapabilities, normalizeLayerLibraryEntries, normalizeLayerStyleEntries, normalizeLongitude, normalizeMapLayout, normalizeMapViewState, normalizeModelGraph, normalizeModels, normalizePrimaryRenderer, normalizePrintLayoutConfig, normalizeProcessingHistory, normalizeProjectComments, normalizeProjectInteraction, normalizeSecondaryMapViews, normalizeStoryMap, normalizeStyleLibraryEntries, normalizeWidgets, overlayStoredEnvironmentVariables, overlayStoredGeocodingApiKeys, overlayStoredPreferenceCredentials, parseCesiumIonAssetId, parseContours, parseCzml, parseDeploymentCapabilities, parseHexColor, parseHexColorList, parseJsonExpression, parseLayerLibrary, parseLayerStylesFile, parseProject, parseS3BucketList, parseS3Error, parseS3ListObjects, parseS3Url, parseStoryMapCsv, parseStoryMapJson, parseStyleLibrary, parseValueMapText, persistedThreeDTilesRequestHeaders, pickEditorIdentity, planLayerLibraryAdd, pluginOwnsPaint, polylineStrToGeoJSON, popupFieldLabel, portableWmsTileUrl, presignS3Url, printLayoutConfigsEqual, projectFromStore, projectPathLabel, proportionalRadiusExpression, proportionalSizeRange, readStoredAuthorName, reapplyLayerJoins, redactCredentials, redactProjectCredentials, redactUrlCredentials, redo, registerProjectRestoreHistory, registerS3UrlSigner, removeTrailingJsonCommas, rendererAppliesOpacity, reorderLayerGroupInPanel, requestIsochrone, requestMatrix, requestRoute, resolveConfiguredPopupTitle, resolveEditorTrackingConfig, resolveGeocoderConfig, resolveHeaderReferences, resolveIdentifyTarget, resolveLabelNumberLocale, resolveLayerCapabilities, resolveMapGrid, resolvePopupBody, resolvePopupImageHeight, resolvePopupMaxWidth, resolvePopupRows, resolvePopupTitle, resolveProjectHeaderReferences, resolveReadableUrl, resolveRolePrivileges, resolveSvgSource, resolveThreeDTilesRequestHeaders, rgbToHex, routeResponseToFeatures, rowCap, ruleBasedColorExpression, ruleBasedVisibilityFilter, s3ObjectHttpsUrl, sameCesiumImagery, sampleMapTerrain, sampleMapTerrainPoint, sampleRemoteElevations, sanitizeLayerStylePatch, scaleAltitudeToActiveBody, scaleDenomination, scaleSpan, scrubCommentsForRemovedLayers, scrubLegendForRemovedLayers, scrubPrintLayoutForLayers, scrubPrintLayoutForRemovedLayers, scrubWidgetsForRemovedLayers, selectLayerById, selectLayerIds, selectLayerSummaries, selectLayersWhen, serializeLayerLibrary, serializeLayerStylesFile, serializeProject, serializeProjectWithLayerCache, serializeStoryMapCsv, serializeStoryMapJson, serializeStyleLibrary, setActiveEllipsoidId, setExternalNativePaintBridge, setGeocodingFetch, setHistoryCoalesceMs, setMaxHistoryFeatureCount, setProjectCredentialLookup, setStoredAuthorName, shouldThrottle, shouldUseTiledRendering, simpleStyleNumberValue, sortLayerGroupInPanel, splitProjectCredentials, stampFeatureCollectionEditorTracking, stampFeatureEditorTracking, stampFeaturePropertiesEditorTracking, storyLayerOpacityFactor, storyMapHasContent, storyVisibleLayers, stringifyPopupValue, stripGoogleMapsApiKeyHeader, stripJoinFields, stripVirtualFieldColumns, styleValue, subscribeProjectRestoreHistory, substituteExpressionVariables, supportsBridgedOpacity, transformGeojsonElevation, trimHistoryBySize, undo, unescapePolyline, uniqueImportedLayerName, unmatchedGeocodeFeature, unresolvedLayerLibraryJoins, unsignedSourceUrl, useAppCapability, useAppStore, useLayer, useLayerIds, useLayerSummaries, useLayersWhen, validateAttributeFormField, validateAttributeFormValues, validateMapExpression, valueMapLabelFor, valueMapToText, vectorCircleColorValue, vectorColorExpression, vectorFillColorValue, vectorFillOpacityValue, vectorLineColorValue, vectorOutlineColorValue, vectorStrokeWidthValue, visiblePopupFields, visiblePopupProperties };
14748
15880
 
14749
15881
  //# sourceMappingURL=index.mjs.map