@geolibre/core 3.3.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["uuidv4","isPlainObject","isPlainObject","uuidv4","newLayerId","isPlainObject","isGeoJsonPayload","MAX_REDACT_DEPTH","isPlainObject","uuidv4","uuidv4"],"sources":["../src/types.ts","../src/diagram.ts","../src/marker-shape.ts","../src/hyperlink.ts","../src/photo.ts","../src/regional-basemaps.ts","../src/cesium-imagery.ts","../src/color-ramp.ts","../src/paths.ts","../src/runtime-env.ts","../src/routing.ts","../src/vector-color.ts","../src/style-layer-evaluator.ts","../src/document-locale.ts","../src/label-number-format.ts","../src/external-native-paint.ts","../src/attribute-form.ts","../src/layer-descriptive-metadata.ts","../src/popup.ts","../src/quick-filters.ts","../src/layer-filters.ts","../src/capabilities.ts","../src/deployment-capabilities.ts","../src/store/capabilities-slice.ts","../src/store/collaboration-slice.ts","../src/layer-groups.ts","../src/layer-ref-scrub.ts","../src/print-layout-config.ts","../src/store/session-slice.ts","../src/store/layer-removal.ts","../src/store/layer-groups-slice.ts","../src/file-name.ts","../src/layer-defaults.ts","../src/style-library.ts","../src/layer-style-clipboard.ts","../src/layer-style-file.ts","../src/store/layers-slice.ts","../src/czml.ts","../src/cesium-kml.ts","../src/cesium-ion.ts","../src/header-references.ts","../src/credentials.ts","../src/layer-library.ts","../src/store/libraries-slice.ts","../src/project.ts","../src/store/map-view-slice.ts","../src/store/processing-slice.ts","../src/store/project-content-slice.ts","../src/history.ts","../src/store/undo-history.ts","../src/store/project-slice.ts","../src/store/ui-slice.ts","../src/store.ts","../src/selection-actions.ts","../src/scale-units.ts","../src/elevation.ts","../src/camera-altitude.ts","../src/project-diff.ts","../src/pixel-format.ts","../src/s3.ts","../src/storymap-sample.ts","../src/storymap-playback.ts","../src/storymap-io.ts","../src/layer-selectors.ts","../src/geocoding.ts","../src/platform.ts","../src/three-d-tiles.ts","../src/project-credential-refs.ts","../src/visibility.ts","../src/editor-tracking.ts","../src/editor-identity.ts"],"sourcesContent":["import type { FeatureCollection } from \"geojson\";\nimport type { PrintLayoutConfig } from \"./print-layout-config\";\n\nexport const OPENFREEMAP_BASEMAPS = [\n {\n id: \"liberty\",\n name: \"Liberty\",\n styleUrl: \"https://tiles.openfreemap.org/styles/liberty\",\n },\n {\n id: \"liberty-3d\",\n name: \"Liberty 3D\",\n styleUrl: \"https://tiles.openfreemap.org/styles/liberty\",\n },\n {\n id: \"positron\",\n name: \"Positron\",\n styleUrl: \"https://tiles.openfreemap.org/styles/positron\",\n },\n {\n id: \"bright\",\n name: \"Bright\",\n styleUrl: \"https://tiles.openfreemap.org/styles/bright\",\n },\n {\n id: \"dark\",\n name: \"Dark\",\n styleUrl: \"https://tiles.openfreemap.org/styles/dark\",\n },\n {\n id: \"fiord\",\n name: \"Fiord\",\n styleUrl: \"https://tiles.openfreemap.org/styles/fiord\",\n },\n] as const;\n\n/**\n * Protomaps v5 basemap flavors. These are resolved to full style URLs at use\n * time by `getProtomapsStyleUrl`, which injects the `VITE_PROTOMAPS_API_KEY`\n * runtime env var. The key is only present in builds configured with it (e.g.\n * the GitHub Pages web demo), so consumers should hide these options when no\n * key is available.\n */\nexport const PROTOMAPS_BASEMAPS = [\n { id: \"protomaps-light\", name: \"Light\", flavor: \"light\" },\n { id: \"protomaps-dark\", name: \"Dark\", flavor: \"dark\" },\n { id: \"protomaps-white\", name: \"White\", flavor: \"white\" },\n { id: \"protomaps-grayscale\", name: \"Grayscale\", flavor: \"grayscale\" },\n { id: \"protomaps-black\", name: \"Black\", flavor: \"black\" },\n] as const;\n\nexport const DEFAULT_BASEMAP = \"https://tiles.openfreemap.org/styles/liberty\";\n\nexport const BLANK_BASEMAP = \"\";\n\nexport const PROJECT_VERSION = \"0.2.0\";\n\n/**\n * Every layer type, as a runtime list so untrusted input (an imported Layer\n * Library bundle, a hand-edited project) can be validated against it.\n * {@link LayerType} is derived from this array, so the two cannot drift.\n */\nexport const LAYER_TYPES = [\n \"geojson\",\n \"raster\",\n \"wms\",\n \"wmts\",\n \"xyz\",\n \"vector-tiles\",\n \"arcgis\",\n \"pmtiles\",\n \"mbtiles\",\n \"zarr\",\n \"lidar\",\n \"gaussian-splat\",\n \"3d-tiles\",\n \"cog\",\n \"flatgeobuf\",\n \"geoparquet\",\n \"duckdb-query\",\n \"deckgl-viz\",\n \"video\",\n \"image\",\n] as const;\n\nexport type LayerType = (typeof LAYER_TYPES)[number];\n\nexport type VectorStyleMode = \"single\" | \"graduated\" | \"categorized\" | \"rule-based\" | \"expression\";\n\n/**\n * One entry in a {@link LayerStyle.vectorRules} rule-based renderer. Each rule\n * pairs a MapLibre filter expression (a boolean expression array, serialized as\n * JSON) with a symbol color. Rules are evaluated top to bottom; the first whose\n * filter matches wins. The catch-all rule (`isElse`) has no filter and supplies\n * the color for features no other rule matched. Mirrors the QGIS/ArcGIS\n * rule-based renderer.\n */\nexport interface VectorRule {\n id: string;\n /** Human-readable label shown in the editor and the legend. */\n label: string;\n /**\n * A MapLibre boolean filter expression serialized as JSON, e.g.\n * `[\"==\", [\"get\", \"TYPE\"], \"park\"]`. Ignored when {@link isElse} is set or\n * the JSON is invalid.\n */\n filter: string;\n /** The symbol fill/circle color for features this rule matches (6-digit hex). */\n color: string;\n /**\n * When true this is the catch-all \"else\" rule: {@link filter} is unused and\n * the rule is an unconditional fallback for features no other rule matched,\n * so the {@link minZoom}/{@link maxZoom} and {@link parentId} fields are\n * ignored on it. Its symbol overrides and {@link enabled} toggle do apply.\n */\n isElse: boolean;\n /**\n * Whether the rule participates in rendering. `false` temporarily disables\n * the rule (and, for a group, its whole subtree) without deleting it,\n * mirroring the QGIS rule checkbox. Absent means enabled.\n */\n enabled?: boolean;\n /**\n * Lowest zoom (inclusive) the rule applies at, mirroring MapLibre's layer\n * `minzoom` convention. Absent means no lower bound.\n */\n minZoom?: number;\n /**\n * Zoom the rule stops applying at (exclusive, like MapLibre's layer\n * `maxzoom`). Absent means no upper bound. Together with {@link minZoom}\n * this is the QGIS per-rule \"scale range\".\n */\n maxZoom?: number;\n /**\n * The id of the parent rule when this rule is nested inside a group (QGIS\n * rule tree). A child rule matches only features that also match every\n * ancestor's filter, and inherits the intersection of the ancestors' zoom\n * ranges and enabled state. A rule that has children acts as a group: its\n * own symbol is not rendered; only leaf rules draw. Absent or dangling\n * means a top-level rule.\n */\n parentId?: string;\n /**\n * Per-rule stroke/outline color override (6-digit hex): the polygon outline\n * and circle stroke color for matching features. Absent inherits the layer\n * {@link LayerStyle.strokeColor}.\n */\n strokeColor?: string;\n /**\n * Per-rule stroke width override in pixels: the line width (lines and\n * polygon outlines) and circle stroke width for matching features. Absent\n * inherits the layer {@link LayerStyle.strokeWidth}.\n */\n strokeWidth?: number;\n /**\n * Per-rule fill/circle opacity override (0..1) for matching features.\n * Absent inherits the layer {@link LayerStyle.fillOpacity}.\n */\n fillOpacity?: number;\n /**\n * Per-rule circle radius override in pixels for matching point features.\n * Absent inherits the layer {@link LayerStyle.circleRadius}.\n */\n circleRadius?: number;\n}\n\n/**\n * The fill pattern applied to polygon layers. `\"none\"` keeps a flat fill; the\n * named patterns are generated as recolorable sprite tiles; `\"svg\"` rasterizes\n * the user-supplied markup in {@link LayerStyle.fillPatternSvg}.\n */\nexport type FillPattern =\n | \"none\"\n | \"hatch\"\n | \"cross-hatch\"\n | \"horizontal\"\n | \"vertical\"\n | \"dots\"\n | \"svg\";\n\n/**\n * The built-in marker shape for a point layer, or `\"custom\"` to rasterize the\n * user-supplied SVG in {@link LayerStyle.markerSvg}. Built-in shapes are drawn\n * on a sprite tile and recolored via {@link LayerStyle.markerColor}.\n */\nexport type MarkerShape =\n | \"circle\"\n | \"square\"\n | \"triangle\"\n | \"diamond\"\n | \"star\"\n | \"cross\"\n | \"pin\"\n | \"custom\";\n\n/**\n * How a point layer is rendered: as individual markers, a density heatmap, or\n * clustered bubbles. Only applies to point geometry.\n */\nexport type PointRenderer = \"single\" | \"heatmap\" | \"cluster\";\n\n/**\n * The repeated decoration symbol drawn along line features (and polygon\n * outlines), or `\"none\"` when decorations are off. Mirrors the QGIS\n * marker-line / arrow symbol layers: `\"arrow\"` renders directional arrowheads\n * that follow the line, the other shapes render as repeated markers.\n */\nexport type LineDecoration = \"none\" | \"arrow\" | \"triangle\" | \"circle\" | \"square\";\n\n/**\n * The per-feature derived geometry rendered by the geometry generator, or\n * `\"none\"` when the generator is off. Mirrors QGIS geometry-generator symbol\n * layers: each feature's derived geometry (its centroid, bounding box, convex\n * hull, or a buffer) is drawn as an extra symbol over the layer's normal\n * symbology. `\"centroid\"` derives points; the rest derive polygons.\n */\nexport type GeometryGeneratorType = \"none\" | \"centroid\" | \"bounding-box\" | \"convex-hull\" | \"buffer\";\n\n/**\n * Unit a stroke/line width is measured in. `\"pixels\"` is constant screen space;\n * `\"meters\"` is ground distance, so the rendered width scales with the map\n * scale (zoom). See {@link LayerStyle.strokeWidthUnit}.\n */\nexport type StrokeWidthUnit = \"pixels\" | \"meters\";\n\n/**\n * The chart drawn on top of each feature by the diagram renderer (QGIS-style\n * diagram symbology), or `\"none\"` when diagrams are off. Diagrams visualize\n * several numeric attributes per feature at once — e.g. election results by\n * party per county — and render through the shared deck.gl overlay on the\n * feature's point location or polygon centroid.\n */\nexport type DiagramType = \"none\" | \"pie\" | \"donut\" | \"bar\" | \"stacked-bar\";\n\n/**\n * How the overall diagram size is determined.\n *\n * - `\"fixed\"`: every diagram renders at {@link LayerStyle.diagramSize} pixels.\n * - `\"sum\"`: scaled by the sum of the mapped attribute values, so the largest\n * total renders at {@link LayerStyle.diagramSize} pixels (area-true square\n * root scaling).\n * - `\"attribute\"`: scaled the same way by the single numeric attribute in\n * {@link LayerStyle.diagramSizeProperty}.\n */\nexport type DiagramSizeMode = \"fixed\" | \"sum\" | \"attribute\";\n\n/** One attribute rendered as a slice/bar of a feature diagram. */\nexport interface DiagramField {\n /** Numeric feature property visualized by this slice/bar. */\n property: string;\n /** Slice/bar color (6-digit hex). */\n color: string;\n}\n\nexport interface VectorStyleStop {\n value: string | number;\n color: string;\n label?: string;\n}\n\n/** Attribute-driven labeling for a vector layer (rendered as a MapLibre symbol layer). */\nexport interface LabelStyle {\n /** Whether labels are shown for the layer. */\n enabled: boolean;\n /** Attribute field whose value becomes the label text. */\n field: string;\n /**\n * Optional MapLibre expression (JSON string) for the label text, which\n * overrides {@link field} when non-empty (e.g. concatenating several fields).\n */\n expression: string;\n /** `\"point\"` labels at the feature/centroid; `\"line\"` places them along lines. */\n placement: \"point\" | \"line\";\n /** Label text size in pixels. */\n size: number;\n /** CSS color string for the label text. */\n color: string;\n /** CSS color string for the text halo drawn behind the label. */\n haloColor: string;\n /** Width of the text halo in pixels. */\n haloWidth: number;\n /** Scale range for labels; `0` / `24` inherit the layer's own zoom range. */\n minZoom: number;\n maxZoom: number;\n /** Let labels overlap instead of hiding colliding ones. */\n allowOverlap: boolean;\n /**\n * Where the label sits relative to its anchor point (MapLibre `text-anchor`),\n * e.g. `\"top\"` places the text above the point. Ignored for line placement.\n */\n anchor: LabelAnchor;\n /** Horizontal label offset in ems (MapLibre `text-offset` x). */\n offsetX: number;\n /** Vertical label offset in ems (MapLibre `text-offset` y; positive is down). */\n offsetY: number;\n /** Label rotation in degrees clockwise (MapLibre `text-rotate`). */\n rotation: number;\n /** Maximum line width in ems before the label wraps (MapLibre `text-max-width`). */\n maxWidth: number;\n /** Letter-case transform applied to the label text (MapLibre `text-transform`). */\n transform: LabelTransform;\n /**\n * Render a numeric {@link field} with the locale's thousands and decimal\n * separators (issue #2336), so `1234567.5` labels as `1,234,567.5` instead\n * of running together. Non-numeric values are unaffected, and it is not\n * applied to {@link expression}, which formats its own output (MapLibre's\n * `number-format`, offered in the Expression Builder).\n */\n numberFormatEnabled: boolean;\n /** Decimal places kept while {@link numberFormatEnabled} is on (0-10). */\n numberDecimals: number;\n /**\n * BCP 47 tag picking the separators for {@link numberFormatEnabled}, from\n * {@link LABEL_NUMBER_LOCALES}. Empty (the default) follows the app's own\n * language, the way popup number fields do.\n */\n numberLocale: string;\n /**\n * How to handle features that share a label.\n *\n * - `\"off\"`: every feature is labeled (the historical behavior).\n * - `\"unique\"`: features stacked at the same point are collapsed to a single\n * label, so co-located points (e.g. several antennas at one cell site) do not\n * stack overlapping text.\n * - `\"concatenate\"`: co-located points are merged into one label that joins\n * their distinct {@link field} values, one per line.\n *\n * Applies to point layers rendered through the inline GeoJSON path and uses\n * {@link field} (not {@link expression}) as the label value.\n */\n dedupe: LabelDedupe;\n /**\n * Data-defined override for {@link size}: a MapLibre expression (JSON\n * string) producing a number, e.g. sizing labels by population. Empty means\n * \"use the literal {@link size}\". Like the other data-defined overrides\n * below, it reads source feature attributes, so it is skipped while\n * {@link dedupe} collapsing is active (the aggregated features carry only\n * the label value).\n */\n sizeExpression: string;\n /**\n * Data-defined override for {@link color}: a MapLibre expression (JSON\n * string) producing a color, e.g. coloring labels by category.\n */\n colorExpression: string;\n /**\n * Data-defined label opacity: a MapLibre expression (JSON string) producing\n * a number in 0..1. When set it replaces the layer-wide opacity for labels\n * (wrapping it would invalidate top-level `[\"zoom\"]` interpolations).\n */\n opacityExpression: string;\n /**\n * Per-feature label visibility: a MapLibre expression (JSON string)\n * producing a boolean. Features evaluating false get no label (e.g. hide\n * labels below an attribute threshold). Combined with the layer's other\n * feature filters.\n */\n visibilityExpression: string;\n /**\n * Per-feature placement priority: a MapLibre expression (JSON string)\n * producing a number, applied as `symbol-sort-key`. Labels with lower\n * values are placed first, so they win when space is tight.\n */\n priorityExpression: string;\n}\n\n/** MapLibre `text-anchor` positions offered for {@link LabelStyle.anchor}. */\nexport type LabelAnchor =\n | \"center\"\n | \"left\"\n | \"right\"\n | \"top\"\n | \"bottom\"\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\";\n\n/** Letter-case transform offered for {@link LabelStyle.transform}. */\nexport type LabelTransform = \"none\" | \"uppercase\" | \"lowercase\";\n\n/** Duplicate-label handling offered for {@link LabelStyle.dedupe}. */\nexport type LabelDedupe = \"off\" | \"unique\" | \"concatenate\";\n\n/**\n * Cartographic blend modes offered for {@link LayerStyle.blendMode}.\n *\n * The list is bounded by what WebGL *fixed-function* blending can express --\n * one `blendFunc` factor pair plus one `blendEquation` -- because that is the\n * only blend state MapLibre's renderer exposes, and it applies to colour and\n * alpha together. Two consequences shaped this list:\n *\n * - Modes needing the destination colour inside a fragment shader (`overlay`,\n * `color-dodge`, `soft-light`, `difference`, and the non-separable HSL modes)\n * are absent. MapLibre draws straight into the map framebuffer, which a\n * shader cannot sample, so computing them would need an extra copy pass.\n * - `darken` and `subtract` are absent even though `MIN` and\n * `FUNC_REVERSE_SUBTRACT` look like they would serve. Both also apply to the\n * alpha channel, where they drive the map canvas transparent: `MIN` takes the\n * whole canvas to `min(0, dst) = 0` everywhere the layer does not cover, and\n * `FUNC_REVERSE_SUBTRACT` leaves `dstA - srcA` inside it. Correcting either\n * needs `blendEquationSeparate` / `blendFuncSeparate`, which MapLibre's state\n * tracker does not offer.\n *\n * Every mode here leaves the canvas opaque and leaves the map untouched\n * wherever the layer contributes nothing. See `docs/user-guide/layers.md`.\n */\nexport const BLEND_MODES = [\"normal\", \"multiply\", \"screen\", \"lighten\", \"add\"] as const;\n\n/**\n * How a layer's pixels combine with the map beneath it. `\"normal\"` is ordinary\n * alpha compositing (the default, and what every layer did before blend modes\n * existed). See {@link BLEND_MODES}.\n */\nexport type BlendMode = (typeof BLEND_MODES)[number];\n\n/**\n * The mode a layer renders with unless it says otherwise. Declared separately\n * from {@link DEFAULT_LAYER_STYLE} so consumers get a non-optional\n * {@link BlendMode} to fall back to: `blendMode` is optional on\n * {@link LayerStyle}, so `DEFAULT_LAYER_STYLE.blendMode` is typed\n * `BlendMode | undefined` however it is initialized.\n */\nexport const DEFAULT_BLEND_MODE: BlendMode = \"normal\";\n\nexport interface LayerStyle {\n minZoom: number;\n maxZoom: number;\n fillColor: string;\n strokeColor: string;\n strokeWidth: number;\n /**\n * Unit the {@link strokeWidth} value is expressed in.\n *\n * - `\"pixels\"` (default): a constant screen-space width that never changes\n * with zoom — the historical behavior.\n * - `\"meters\"`: a ground-distance width, so the rendered line stays\n * proportional to the map scale (thicker zoomed in, thinner zoomed out),\n * matching QGIS \"map units\". Only affects line/polygon-outline rendering;\n * point/circle outlines remain pixel-based.\n */\n strokeWidthUnit: StrokeWidthUnit;\n fillOpacity: number;\n circleRadius: number;\n textColor: string;\n textHaloColor: string;\n textHaloWidth: number;\n textSize: number;\n labels: LabelStyle;\n extrusionEnabled: boolean;\n extrusionColor: string;\n extrusionOpacity: number;\n extrusionHeightProperty: string;\n extrusionHeightScale: number;\n extrusionBase: number;\n extrusionAdvancedStyleEnabled: boolean;\n extrusionColorExpression: string;\n extrusionHeightExpression: string;\n /**\n * When true, a vector layer whose coordinates carry Z values (e.g. a GPX\n * track with elevations) renders in true 3D through the shared deck.gl\n * overlay instead of MapLibre's flat 2D layers, so features sit at their\n * own altitude. Orthogonal to {@link extrusionEnabled}, which extrudes flat\n * polygons by an attribute; only one of the two should be on at a time.\n */\n elevation3dEnabled: boolean;\n /** Multiplier applied to each coordinate's Z value (vertical exaggeration). */\n elevation3dVerticalScale: number;\n /** Constant altitude offset in meters added after the vertical scale. */\n elevation3dOffset: number;\n vectorStyleMode: VectorStyleMode;\n vectorStyleProperty: string;\n vectorStyleClassCount: number;\n vectorStyleColorRamp: string;\n vectorStyleClassificationScheme: string;\n vectorStyleStops: VectorStyleStop[];\n vectorStyleExpression: string;\n /**\n * Ordered rules for the `\"rule-based\"` {@link vectorStyleMode}. Compiled to a\n * MapLibre `case` color expression (first matching filter wins, catch-all\n * last). See {@link VectorRule}.\n */\n vectorRules: VectorRule[];\n /**\n * When true, the point circle radius (or line width) is sized by a numeric\n * field via an `interpolate` between {@link proportionalSizeMinValue} ..\n * {@link proportionalSizeMaxValue} mapped onto {@link proportionalSizeMinRadius}\n * .. {@link proportionalSizeMaxRadius} (QGIS \"graduated → size\" / proportional\n * symbols). Orthogonal to the color {@link vectorStyleMode}.\n */\n proportionalSizeEnabled: boolean;\n proportionalSizeProperty: string;\n proportionalSizeMinValue: number;\n proportionalSizeMaxValue: number;\n proportionalSizeMinRadius: number;\n proportionalSizeMaxRadius: number;\n /**\n * Polygon fill pattern. `\"none\"` keeps the flat fill; other values render a\n * recolorable sprite tile ({@link fillPatternColor}); `\"svg\"` rasterizes\n * {@link fillPatternSvg}. See {@link FillPattern}.\n */\n fillPattern: FillPattern;\n fillPatternColor: string;\n /** Raw SVG markup (or a data URL) used when {@link fillPattern} is `\"svg\"`. */\n fillPatternSvg: string;\n /**\n * When true, point features render as a marker icon ({@link markerShape})\n * instead of a plain circle. Built-in shapes are recolored via\n * {@link markerColor}; `\"custom\"` rasterizes {@link markerSvg}.\n */\n markerEnabled: boolean;\n markerShape: MarkerShape;\n markerColor: string;\n markerSize: number;\n /** Raw SVG markup (or a data URL) used when {@link markerShape} is `\"custom\"`. */\n markerSvg: string;\n /**\n * When true, per-feature [simplestyle-spec](https://github.com/mapbox/simplestyle-spec)\n * properties (`fill`, `fill-opacity`, `stroke`, `stroke-width`,\n * `stroke-opacity`, `marker-color`) override the flat layer style on a\n * per-feature basis. Set automatically when a GeoJSON layer is added whose\n * features carry these properties (e.g. styled KML/KMZ), so embedded\n * symbology renders without manual configuration.\n */\n simpleStyleEnabled: boolean;\n /**\n * Per-feature chart symbology (QGIS-style diagrams). `\"none\"` disables it;\n * any other value renders one {@link DiagramType} chart per feature over the\n * layer's normal symbology, built from the numeric attributes in\n * {@link diagramFields}. See `@geolibre/core`'s `diagram.ts` helpers.\n */\n diagramType: DiagramType;\n /** Attributes (and their colors) charted by the diagram renderer, in order. */\n diagramFields: DiagramField[];\n /** How the per-feature diagram size is determined. */\n diagramSizeMode: DiagramSizeMode;\n /**\n * Diagram size in pixels: the rendered diameter/height for `\"fixed\"` sizing,\n * or the diameter/height of the largest feature for scaled sizing.\n */\n diagramSize: number;\n /** Numeric attribute driving `\"attribute\"` sizing (see {@link DiagramSizeMode}). */\n diagramSizeProperty: string;\n /** Minimum zoom at which diagrams are drawn, to avoid clutter when zoomed out. */\n diagramMinZoom: number;\n /**\n * When true, diagrams that would overlap an already-placed diagram on screen\n * are skipped (largest first), decluttering dense areas. Recomputed as the\n * view changes.\n */\n diagramDeclutter: boolean;\n pointRenderer: PointRenderer;\n heatmapRadius: number;\n heatmapIntensity: number;\n /** Built-in color ramp used by the heatmap density renderer. */\n heatmapColorRamp: string;\n /** Numeric feature property used as heatmap weight; blank gives every point equal weight. */\n heatmapWeightProperty: string;\n clusterRadius: number;\n clusterMaxZoom: number;\n /**\n * When true, the polygon fill renders *inverted*: the area outside the\n * features is filled (with {@link fillColor}/{@link fillOpacity}) and the\n * features themselves become holes, mirroring the QGIS \"Inverted polygons\"\n * renderer. Feature outlines still render normally. Only applies to layers\n * with polygon geometry; ignored while {@link extrusionEnabled} is on.\n */\n invertedFillEnabled: boolean;\n /**\n * Repeated decoration symbol drawn along line features and polygon outlines\n * (QGIS marker-line / arrow lines). `\"none\"` disables it. See\n * {@link LineDecoration}.\n */\n lineDecoration: LineDecoration;\n /**\n * Decoration symbol color (6-digit hex). An empty string inherits\n * {@link strokeColor} so decorations follow the stroke by default.\n */\n lineDecorationColor: string;\n /** Decoration symbol size in pixels. */\n lineDecorationSize: number;\n /** Distance between consecutive decoration symbols in pixels. */\n lineDecorationSpacing: number;\n /**\n * Per-feature derived geometry drawn over the layer's normal symbology\n * (QGIS geometry generator). `\"none\"` disables it. See\n * {@link GeometryGeneratorType}.\n */\n geometryGenerator: GeometryGeneratorType;\n /** Buffer distance in meters for the `\"buffer\"` generator. */\n geometryGeneratorBufferDistance: number;\n /**\n * Attribute driving the `\"buffer\"` generator's distance, so each feature is\n * buffered by its own value in meters (QGIS data-defined override on a\n * geometry-generator symbol). An empty string buffers every feature by the\n * flat {@link geometryGeneratorBufferDistance}, which also stands in for\n * features whose value is missing or non-numeric.\n */\n geometryGeneratorBufferProperty: string;\n /** Fill color (6-digit hex) for generated polygons and centroid points. */\n geometryGeneratorFillColor: string;\n /** Outline color (6-digit hex) for generated geometry. */\n geometryGeneratorStrokeColor: string;\n /** Outline width in pixels for generated geometry. */\n geometryGeneratorStrokeWidth: number;\n /** Fill opacity (0..1) for generated polygons and centroid points. */\n geometryGeneratorOpacity: number;\n /** Circle radius in pixels for generated centroid points. */\n geometryGeneratorCircleRadius: number;\n /**\n * Attribute driving the radius of generated centroid points, scaling them\n * between {@link geometryGeneratorSizeMinRadius} and\n * {@link geometryGeneratorSizeMaxRadius} across\n * {@link geometryGeneratorSizeMinValue} ..\n * {@link geometryGeneratorSizeMaxValue} (proportional symbols on the derived\n * centroids). An empty string draws every centroid at the flat\n * {@link geometryGeneratorCircleRadius}.\n *\n * Deliberately separate from {@link proportionalSizeProperty}: that one also\n * drives line width, so reusing it here would resize a polygon layer's\n * outlines as a side effect of sizing its centroids.\n */\n geometryGeneratorSizeProperty: string;\n geometryGeneratorSizeMinValue: number;\n geometryGeneratorSizeMaxValue: number;\n geometryGeneratorSizeMinRadius: number;\n geometryGeneratorSizeMaxRadius: number;\n rasterBrightnessMin: number;\n rasterBrightnessMax: number;\n rasterSaturation: number;\n rasterContrast: number;\n rasterHueRotate: number;\n /**\n * How this layer composites onto whatever is drawn beneath it. `\"normal\"`\n * (the default) is ordinary alpha compositing; `\"multiply\"` is the classic\n * cartographic case of laying colour over a hillshade so the relief still\n * reads through. Applies to the layer's own rendered geometry and raster\n * tiles, not to its labels, which stay legible on top. See\n * {@link BLEND_MODES} for the available modes and why the list is what it is.\n */\n blendMode?: BlendMode;\n}\n\nexport const DEFAULT_LAYER_STYLE: LayerStyle = {\n minZoom: 0,\n maxZoom: 24,\n fillColor: \"#3b82f6\",\n strokeColor: \"#1e40af\",\n strokeWidth: 2,\n strokeWidthUnit: \"pixels\",\n fillOpacity: 0.6,\n circleRadius: 6,\n textColor: \"#111827\",\n textHaloColor: \"#ffffff\",\n textHaloWidth: 2,\n textSize: 16,\n labels: {\n enabled: false,\n field: \"\",\n expression: \"\",\n placement: \"point\",\n size: 13,\n color: \"#111827\",\n haloColor: \"#ffffff\",\n haloWidth: 1.5,\n minZoom: 0,\n maxZoom: 24,\n allowOverlap: false,\n anchor: \"center\",\n offsetX: 0,\n offsetY: 0,\n rotation: 0,\n maxWidth: 10,\n transform: \"none\",\n numberFormatEnabled: false,\n numberDecimals: 0,\n numberLocale: \"\",\n dedupe: \"off\",\n sizeExpression: \"\",\n colorExpression: \"\",\n opacityExpression: \"\",\n visibilityExpression: \"\",\n priorityExpression: \"\",\n },\n extrusionEnabled: false,\n extrusionColor: \"#3b82f6\",\n extrusionOpacity: 0.8,\n extrusionHeightProperty: \"height\",\n extrusionHeightScale: 1,\n extrusionBase: 0,\n extrusionAdvancedStyleEnabled: false,\n extrusionColorExpression: \"\",\n extrusionHeightExpression: \"\",\n elevation3dEnabled: false,\n elevation3dVerticalScale: 1,\n elevation3dOffset: 0,\n vectorStyleMode: \"single\",\n vectorStyleProperty: \"\",\n vectorStyleClassCount: 5,\n vectorStyleColorRamp: \"viridis\",\n vectorStyleClassificationScheme: \"equal-interval\",\n vectorStyleStops: [\n { value: 0, color: \"#dbeafe\" },\n { value: 1, color: \"#2563eb\" },\n ],\n vectorStyleExpression: \"\",\n vectorRules: [],\n proportionalSizeEnabled: false,\n proportionalSizeProperty: \"\",\n proportionalSizeMinValue: 0,\n proportionalSizeMaxValue: 100,\n proportionalSizeMinRadius: 4,\n proportionalSizeMaxRadius: 24,\n fillPattern: \"none\",\n fillPatternColor: \"#1e40af\",\n fillPatternSvg: \"\",\n markerEnabled: false,\n markerShape: \"circle\",\n markerColor: \"#3b82f6\",\n markerSize: 18,\n markerSvg: \"\",\n simpleStyleEnabled: false,\n diagramType: \"none\",\n diagramFields: [],\n diagramSizeMode: \"fixed\",\n diagramSize: 30,\n diagramSizeProperty: \"\",\n diagramMinZoom: 0,\n diagramDeclutter: false,\n pointRenderer: \"single\",\n heatmapRadius: 30,\n heatmapIntensity: 1,\n heatmapColorRamp: \"turbo\",\n heatmapWeightProperty: \"\",\n clusterRadius: 50,\n clusterMaxZoom: 14,\n invertedFillEnabled: false,\n lineDecoration: \"none\",\n lineDecorationColor: \"\",\n lineDecorationSize: 12,\n lineDecorationSpacing: 80,\n geometryGenerator: \"none\",\n geometryGeneratorBufferDistance: 1000,\n geometryGeneratorBufferProperty: \"\",\n geometryGeneratorFillColor: \"#f59e0b\",\n geometryGeneratorStrokeColor: \"#b45309\",\n geometryGeneratorStrokeWidth: 2,\n geometryGeneratorOpacity: 0.4,\n geometryGeneratorCircleRadius: 5,\n geometryGeneratorSizeProperty: \"\",\n geometryGeneratorSizeMinValue: 0,\n geometryGeneratorSizeMaxValue: 100,\n geometryGeneratorSizeMinRadius: 4,\n geometryGeneratorSizeMaxRadius: 24,\n rasterBrightnessMin: 0,\n rasterBrightnessMax: 1,\n rasterSaturation: 0,\n rasterContrast: 0,\n rasterHueRotate: 0,\n blendMode: DEFAULT_BLEND_MODE,\n};\n\n/**\n * Read a layer style property, falling back to the shared default when the\n * layer does not define it. Shared by `@geolibre/map` and the desktop app so\n * the two consumers cannot drift.\n */\nexport function styleValue<K extends keyof LayerStyle>(style: LayerStyle, key: K): LayerStyle[K] {\n return style[key] ?? DEFAULT_LAYER_STYLE[key];\n}\n\n/**\n * Feature-count threshold above which a local vector (GeoJSON) layer is rendered\n * through client-side vector tiles (geojson-vt / supercluster served by a custom\n * MapLibre protocol) instead of one in-memory geojson source pushed via\n * `setData`. Small layers stay on the simpler inline path. Mirrors the\n * `MAX_CEREUS_FEATURES` precedent in the desktop SQL engine.\n */\nexport const LARGE_VECTOR_FEATURE_THRESHOLD = 50_000;\n\n/**\n * Decide whether a GeoJSON layer should use the tiled rendering path.\n *\n * @param geojson - The layer's feature collection (may be undefined for\n * non-vector layers).\n * @returns `true` when the collection exceeds\n * {@link LARGE_VECTOR_FEATURE_THRESHOLD} features.\n */\nexport function shouldUseTiledRendering(geojson: GeoJSON.FeatureCollection | undefined): boolean {\n return (geojson?.features.length ?? 0) > LARGE_VECTOR_FEATURE_THRESHOLD;\n}\n\n/**\n * Match statistics from the last time a {@link LayerJoin} was applied, shown in\n * the Joins UI so silent key mismatches are visible (mirrors QGIS's join\n * feedback). Recomputed on every apply; persisted harmlessly with the join.\n */\nexport interface LayerJoinStats {\n /** Target features whose key matched a join-table row. */\n matchedCount: number;\n /** Target features with no matching join-table row (their joined columns are null). */\n unmatchedTargetCount: number;\n /** Join-table rows (with a non-empty key) that matched no target feature. */\n unmatchedJoinCount: number;\n}\n\n/**\n * A persistent attribute join attached to a layer (QGIS Layer Properties →\n * Joins): a live left join that augments the layer's attribute table with\n * columns from another layer (typically a geometry-less table added via\n * Delimited Text with no coordinate fields) matched on a key field. Unlike the\n * Processing → Vector attribute join, the layer keeps its identity — styles,\n * labels, and position — and the joined columns refresh whenever the join\n * table's data changes. Definitions persist in `.geolibre.json` and re-resolve\n * on project load. See `joins.ts` for the engine.\n */\nexport interface LayerJoin {\n /** Stable id for list edits. */\n id: string;\n /** Id of the layer providing the joined columns (its geometry is ignored). */\n joinLayerId: string;\n /** Key field on the layer that owns this join. */\n targetField: string;\n /** Key field on the join layer. */\n joinField: string;\n /**\n * Join-layer fields to bring over; `undefined` brings every field except the\n * key. Names are the join layer's own (pre-prefix) field names.\n */\n fields?: string[];\n /** Optional prefix prepended to every joined column name (as in QGIS). */\n prefix?: string;\n /** `false` detaches the joined columns without deleting the definition. */\n enabled?: boolean;\n /**\n * Bookkeeping written by the engine: the output column names this join added\n * to the layer's features. Applying joins strips these first, which makes\n * re-application idempotent without keeping a duplicate copy of the base\n * data (base columns always win a name collision, so a joined column never\n * shadows one). Not user-editable.\n */\n addedFields?: string[];\n /** Last-run match statistics; see {@link LayerJoinStats}. */\n stats?: LayerJoinStats;\n}\n\n/** Edit-widget kinds the Attribute Form designer can assign to a field. */\nexport type AttributeFormWidget = \"text\" | \"number\" | \"range\" | \"checkbox\" | \"date\" | \"valueMap\";\n\n/** One selectable entry of a `valueMap` widget (stored value + display label). */\nexport interface AttributeFormValueMapEntry {\n /** The value written to the feature property (compared as a string). */\n value: string;\n /** Human-readable label shown in the dropdown; defaults to {@link value}. */\n label?: string;\n}\n\n/**\n * Per-field configuration authored in the Attribute Form designer (layer\n * properties → Attributes Form, QGIS-style): which edit widget the attribute\n * editing surfaces render for the field, plus optional expression-based\n * constraints and conditional visibility. Consumed by the attribute table's\n * inline editor and the Field Collection capture form; helpers live in\n * `attribute-form.ts`.\n */\nexport interface AttributeFormFieldConfig {\n /** Feature property key this configuration applies to. */\n field: string;\n widget: AttributeFormWidget;\n /** Display label override shown in forms instead of the raw field name. */\n alias?: string;\n /**\n * When true, a null/empty value fails validation. Checkbox widgets are\n * exempt: unchecked is a valid state, not a missing value.\n */\n required?: boolean;\n /** Dropdown entries for the `valueMap` widget. */\n valueMap?: AttributeFormValueMapEntry[];\n /** Lower bound for `number`/`range` widgets (inclusive). */\n min?: number;\n /** Upper bound for `number`/`range` widgets (inclusive). */\n max?: number;\n /** Step for the `range` widget's input. */\n step?: number;\n /**\n * Boolean MapLibre expression that must evaluate to `true` against the\n * feature's (candidate) properties for the value to be accepted, e.g.\n * `[\">\", [\"get\", \"population\"], 0]`. Stored as the expression source string\n * the Expression Builder edits.\n */\n constraintExpression?: string;\n /** Human-readable message shown when the constraint fails. */\n constraintDescription?: string;\n /**\n * Boolean MapLibre expression controlling whether the field is shown in\n * attribute forms; `false` hides the field (and skips its validation).\n * Empty/invalid expressions fail open so a typo cannot hide data entry.\n */\n visibilityExpression?: string;\n}\n\n/** The Attribute Form designer's whole per-layer configuration. */\nexport interface AttributeFormConfig {\n fields: AttributeFormFieldConfig[];\n}\n\n/** Point of contact recorded in a layer's {@link LayerDescriptiveMetadata}. */\nexport interface LayerMetadataContact {\n name?: string;\n email?: string;\n organization?: string;\n}\n\n/** A related resource recorded in a layer's {@link LayerDescriptiveMetadata}. */\nexport interface LayerMetadataLink {\n /** Absolute URL of the resource. */\n href: string;\n /** Relation type (STAC/IANA link relation such as `\"license\"` or `\"related\"`). */\n rel?: string;\n /** Human-readable label. */\n title?: string;\n}\n\n/**\n * Time span the layer's data covers. Each bound is an ISO 8601 date\n * (`YYYY-MM-DD`) or date-time; either may be omitted for an open interval.\n */\nexport interface LayerMetadataTemporalExtent {\n start?: string;\n end?: string;\n}\n\n/**\n * User-authored, catalog-style description of a layer (issue #2858), edited in\n * the layer's Metadata dialog and exported as a STAC Item. Distinct from the\n * internal {@link GeoLibreLayer.metadata} record the renderers and plugins key\n * off: every field here is free-form documentation that changes nothing about\n * how the layer renders. Helpers live in `layer-descriptive-metadata.ts`.\n */\nexport interface LayerDescriptiveMetadata {\n title?: string;\n /** Free-text summary of the data (STAC `description`, ISO abstract). */\n abstract?: string;\n keywords?: string[];\n /** SPDX license identifier (e.g. `\"CC-BY-4.0\"`) or free text. */\n license?: string;\n /** Credit line for the data's producers. */\n attribution?: string;\n contact?: LayerMetadataContact;\n /** How the data was produced: sources and processing steps. */\n lineage?: string;\n temporalExtent?: LayerMetadataTemporalExtent;\n links?: LayerMetadataLink[];\n}\n\n/**\n * How a popup renders one field's value (issue #2113). `\"auto\"` reproduces the\n * untyped rendering the Identify popup has always done — sanitized KML\n * `description` markup, an inline `data:image/*;base64` value as a thumbnail,\n * anything else stringified. The remaining kinds are explicit author choices\n * so a popup never has to guess from the value.\n */\nexport type PopupFieldKind = \"auto\" | \"text\" | \"number\" | \"date\" | \"link\" | \"image\";\n\n/** How a `\"date\"` field's value is written out. */\nexport type PopupDateFormat = \"date\" | \"datetime\" | \"time\" | \"iso\" | \"year\";\n\n/** Value formatting for one popup field. Every part is optional. */\nexport interface PopupFieldFormat {\n /** Fixed number of decimals for a `\"number\"` field. */\n decimals?: number;\n /** Group thousands with the locale's separator (`\"number\"` fields). */\n thousands?: boolean;\n /** Date rendering for a `\"date\"` field; defaults to `\"date\"`. */\n dateFormat?: PopupDateFormat;\n /** Text placed before the formatted value. */\n prefix?: string;\n /** Text placed after the formatted value, e.g. a unit suffix. */\n suffix?: string;\n /** Link text for a `\"link\"` field; the value itself is used when unset. */\n linkLabel?: string;\n}\n\n/** One field's entry in a layer's popup configuration. */\nexport interface PopupFieldConfig {\n /** Feature property key. */\n field: string;\n /** Display label shown instead of the raw field name. */\n label?: string;\n /** How the value renders; defaults to `\"auto\"`. */\n kind?: PopupFieldKind;\n format?: PopupFieldFormat;\n /** Include this field in the hover tooltip's short subset. */\n hover?: boolean;\n}\n\n/**\n * Per-layer configuration for what a viewer sees when they interact with a\n * feature (issue #2113): which fields the Identify popup shows, in what order,\n * under what labels and formatting, plus an optional hover tooltip.\n *\n * A layer with no `popup` block behaves exactly as it did before this existed:\n * the layer name as the heading, then every property as a key/value row.\n * {@link GeoLibreLayer.fieldVisibility} stays authoritative — a `\"hidden\"` or\n * `\"excluded\"` field is dropped even when a popup config names it.\n */\nexport interface LayerPopupConfig {\n /** `false` suppresses the Identify popup for this layer. Defaults to `true`. */\n click?: boolean;\n /** `true` shows a hover tooltip built from the `hover` fields. Defaults to `false`. */\n hover?: boolean;\n /** Field whose value titles the popup instead of the layer name. */\n titleField?: string;\n /**\n * MapLibre expression source producing the popup title. Wins over\n * {@link titleField}; when it fails or produces nothing the title falls\n * through to `titleField`, and only then to the layer name.\n */\n titleExpression?: string;\n /**\n * MapLibre expression source producing the whole popup body as text, for\n * authors who want a sentence rather than a table. When it evaluates, it\n * replaces the field rows.\n */\n bodyExpression?: string;\n /** `false` drops the synthetic `id` row. Defaults to `true`. */\n showFeatureId?: boolean;\n /**\n * Widest the click popup may grow, in CSS pixels. Unset keeps the default\n * (520px, or 420px for a popup carrying an image). Clamped to the range\n * `resolvePopupMaxWidth` enforces and always capped by the viewport, so a\n * value wider than the window still leaves the map usable.\n */\n maxWidth?: number;\n /**\n * Tallest an `\"image\"` field's thumbnail may draw inside the popup, in CSS\n * pixels. Unset keeps the default (`min(50vh, 420px)`). Clamped by\n * `resolvePopupImageHeight`. Pair it with {@link maxWidth} for a\n * bigger picture: the thumbnail keeps its aspect ratio, so widening the\n * popup is what lets a landscape photo use the extra height.\n */\n imageHeight?: number;\n /**\n * The fields to show and their order. An empty or absent list keeps today's\n * behavior: every visible property, in the feature's own key order.\n */\n fields?: PopupFieldConfig[];\n}\n\n/**\n * A virtual field attached to a vector layer (QGIS Field Calculator → \"Create\n * virtual field\", issue #1321): a column defined by a MapLibre expression that\n * recomputes live instead of being written once as static values. The engine\n * (`virtual-fields.ts`) materializes the computed values into the layer's\n * feature properties — so the attribute table, Expression Builder,\n * data-driven styling, labels, and selection all see the column with no\n * further wiring — and re-derives them whenever the layer's data (or its\n * joins) change. Definitions persist in `.geolibre.json` and re-resolve on\n * project load; the expression is a declarative MapLibre expression (never\n * arbitrary code), so re-evaluating it from a shared project file is safe.\n */\nexport interface LayerVirtualField {\n /** Stable id for list edits. */\n id: string;\n /** The output column name. A name already taken by a base column is skipped. */\n name: string;\n /**\n * MapLibre expression source (JSON text, e.g. `[\"/\", [\"get\", \"pop\"],\n * [\"get\", \"area_km2\"]]`) evaluated against each feature.\n */\n expression: string;\n /** `false` detaches the computed column without deleting the definition. */\n enabled?: boolean;\n /**\n * Bookkeeping written by the engine: the column name actually materialized\n * on the last apply, absent when the field was disabled, failed to compile,\n * or was skipped because the name collided with an existing column.\n * Applying virtual fields strips these first, which makes re-application\n * idempotent (an existing column is never shadowed, so stripping exactly\n * restores the pre-apply properties). Not user-editable.\n */\n addedField?: string;\n /** Compile error from the last apply, when the expression failed to parse. */\n error?: string;\n /**\n * Features whose evaluation threw at runtime on the last apply (their cell\n * is null). Surfaced so the UI can warn without one bad feature aborting\n * the whole column.\n */\n errorCount?: number;\n}\n\n/**\n * Quick filters (issue #2114): the data-driven filter controls a layer offers\n * in its Quick Filters section. What persists is the *control state* — field,\n * kind, chosen values — not the compiled output; `quick-filters.ts` compiles it\n * to a MapLibre filter at sync time so a saved filter can always be reopened\n * and edited.\n */\n\n/** Which control a quick filter renders, and how it compiles. */\nexport type QuickFilterKind = \"categorical\" | \"range\" | \"date\" | \"text\";\n\n/** The comparison a `text` quick filter applies (always case-insensitive). */\nexport type QuickFilterTextOperator = \"contains\" | \"startsWith\" | \"equals\";\n\n/**\n * How a date field stores its values, deciding whether a `date` quick filter\n * compares ISO text or epoch numbers. `iso` covers both `YYYY-MM-DD` and full\n * `YYYY-MM-DDTHH:MM:SSZ` timestamps: only the leading `YYYY-MM-DD` slice is\n * compared, so a trailing time, a milliseconds fraction, or a `Z` cannot break\n * a boundary. Mirrors (deliberately, in a smaller form) the value kinds the\n * Time Slider's `detectValueKind` reports.\n */\nexport type QuickFilterDateKind = \"iso\" | \"epochMs\" | \"epochS\";\n\n/** A single filter control persisted on a layer. */\nexport interface LayerQuickFilter {\n /** Stable id, used as the React key and for edits/removal. */\n id: string;\n /** The feature property this control narrows. */\n field: string;\n kind: QuickFilterKind;\n /**\n * `false` keeps the control configured but inert, so a filter can be muted\n * without losing the values chosen for it. Defaults to enabled.\n */\n enabled?: boolean;\n /**\n * `categorical`: the chosen values. An empty (or omitted) selection means\n * \"every value\" and compiles to nothing — unchecking the last box clears the\n * filter rather than emptying the map.\n */\n values?: (string | number | boolean)[];\n /** `range`: inclusive bounds. `null`/omitted leaves that side open. */\n min?: number | null;\n max?: number | null;\n /**\n * `date`: inclusive `YYYY-MM-DD` bounds. The end day is included in full, so\n * a timestamp field filtered to a single day keeps that whole day.\n */\n start?: string | null;\n end?: string | null;\n /** `date`: how the field stores its values. Defaults to `iso`. */\n dateKind?: QuickFilterDateKind;\n /** `text`: the comparison. Defaults to `contains`. */\n operator?: QuickFilterTextOperator;\n /** `text`: the needle. Blank means no constraint. */\n text?: string;\n}\n\n/** Persisted refresh policy and most recent synchronization result for a layer. */\nexport interface LayerConnection {\n /** Owning layer id. Repeated here so records remain self-describing when exported. */\n layerId: string;\n /** Automatic refresh cadence in seconds, or null for manual synchronization only. */\n interval: number | null;\n /** ISO timestamp of the most recent successful synchronization. */\n lastSyncedAt: string | null;\n /** Most recent synchronization error. Cleared by a successful synchronization. */\n lastError: string | null;\n /** Whether a failed synchronization retains the last good data or clears it. */\n onFailure: \"keep-last\" | \"clear\";\n}\n\n/**\n * Configuration for automatic feature editor tracking (Issue #1677).\n * Maintains `created_by`, `created_at`, `edited_by`, `edited_at` fields\n * automatically when features are created or updated.\n */\nexport interface EditorTrackingConfig {\n /** `true` enables automatic creation/edit timestamp and author stamping. */\n enabled: boolean;\n /** Field name for creation author (default `\"created_by\"`). */\n createdByField?: string;\n /** Field name for creation timestamp (default `\"created_at\"`). */\n createdAtField?: string;\n /** Field name for last-edit author (default `\"edited_by\"`). */\n editedByField?: string;\n /** Field name for last-edit timestamp (default `\"edited_at\"`). */\n editedAtField?: string;\n}\n\n/**\n * Visibility of a layer's attribute field.\n * - \"hidden\": Not shown in the attribute table, identify popup, tooltips, or field pickers, but remains in the data.\n * - \"excluded\": Removed entirely from the data when the project is shared or exported.\n */\nexport type FieldVisibility = \"hidden\" | \"excluded\";\n\n/**\n * Explicit capability flags defining what actions a user/session may perform on a layer.\n * Any omitted capability falls back to the inferred default behavior for that layer's source.\n */\nexport interface LayerCapabilities {\n /** Can the layer's features or attributes be queried / identified. */\n query?: boolean;\n /** Can new features be created/added to the layer. */\n create?: boolean;\n /** Can existing features/attributes be modified. */\n update?: boolean;\n /** Can features be deleted from the layer. */\n delete?: boolean;\n /** Can the layer data/symbology be exported/downloaded. */\n export?: boolean;\n}\n\n/**\n * Application privilege identifiers defining discrete capabilities in GeoLibre.\n */\nexport type AppPrivilege =\n | \"layers:edit\"\n | \"layers:add-remote\"\n | \"layers:add-local\"\n | \"processing:run\"\n | \"processing:sidecar\"\n | \"project:save\"\n | \"project:share\"\n | \"project:share-public\"\n | \"plugins:install\"\n | \"assistant:use\"\n | \"connections:manage\"\n | \"export:data\"\n | \"export:image\"\n | \"settings:manage\";\n\n/**\n * Standard named roles bundling application privileges.\n */\nexport type AppRole = \"viewer\" | \"editor\" | \"publisher\" | \"administrator\" | \"custom\";\n\n/**\n * Ephemeral application capabilities state defining the active role, effective privileges,\n * and optional restriction reason for the current session/deployment.\n */\nexport interface AppCapabilities {\n /**\n * The assigned application role. `custom` once an ad-hoc `grantAppPrivilege` /\n * `revokeAppPrivilege` has moved the set away from the bundle a named role\n * defines, so the role never claims a shape the privileges do not have.\n */\n role: AppRole;\n /** List of granted privileges for the active role or custom configuration. */\n privileges: AppPrivilege[];\n /**\n * Optional human-readable reason covering the whole set (e.g. \"Action disabled\n * by deployment policy\"), used for any privilege without its own.\n */\n reason?: string;\n /**\n * Per-privilege reasons, and why they exist: two privileges can be withheld by\n * different causes — a role bundle plus a licence limit, say — and a single\n * `reason` would make the second revocation relabel the first, so every gate\n * would explain itself with whichever cause happened to be recorded last.\n * Takes precedence over `reason` for the privileges it names.\n */\n privilegeReasons?: Partial<Record<AppPrivilege, string>>;\n}\n\nexport interface GeoLibreLayer {\n id: string;\n name: string;\n type: LayerType;\n source: Record<string, unknown>;\n visible: boolean;\n opacity: number;\n style: LayerStyle;\n metadata: Record<string, unknown>;\n beforeId?: string;\n geojson?: FeatureCollection;\n /** A SQL Server write needs a successful table reread before it may be submitted again. */\n mssqlWritebackPending?: boolean;\n /**\n * Explicit capability set for the layer (query, create, update, delete, export).\n * Unset capabilities default to the inferred behavior for the layer's source kind.\n */\n capabilities?: LayerCapabilities;\n /**\n * Automatic editor tracking configuration for feature creation/updates.\n */\n editorTracking?: EditorTrackingConfig;\n /**\n * Field-level visibility overrides. Fields marked as \"excluded\" are physically\n * removed from the data during export and sharing.\n */\n fieldVisibility?: Record<string, FieldVisibility>;\n /**\n * Per-field edit-widget, constraint, and visibility configuration authored\n * in the Attribute Form designer. Applied by the attribute editing surfaces\n * (attribute table inline editor, Field Collection capture form); persists\n * with the project like {@link joins}.\n */\n attributeForm?: AttributeFormConfig;\n /**\n * Popup and hover-tooltip design for this layer, authored in the Style\n * panel's Popup section. Absent means the default full-property dump.\n */\n popup?: LayerPopupConfig;\n /**\n * User-authored catalog metadata (title, abstract, keywords, license,\n * contact, lineage, temporal extent, links), edited in the layer's Metadata\n * dialog. Absent when nothing has been entered; an empty record is never\n * written to a project file.\n */\n descriptiveMetadata?: LayerDescriptiveMetadata;\n /**\n * Persistent attribute joins applied to this layer's features, in order.\n * The joined columns are materialized into `geojson` feature properties (so\n * the attribute table, Expression Builder, styling, and labels all see them)\n * and re-derived whenever the layer's or a join table's data changes.\n */\n joins?: LayerJoin[];\n /**\n * Expression-backed virtual fields computed for this layer's features, in\n * order. Applied after joins (so an expression can read joined columns) and\n * materialized into `geojson` feature properties; re-derived whenever the\n * layer's data changes. See {@link LayerVirtualField}.\n */\n virtualFields?: LayerVirtualField[];\n /**\n * Transient MapLibre filter expression applied on top of every rendered\n * sub-layer's geometry filter. The Time Slider plugin sets this on a bound\n * vector layer so scrubbing the timeline narrows the visible features to the\n * current time window, while the layer's own styling and opacity stay\n * untouched. It is derived from the slider's current date, so it is NOT\n * persisted (stripped by `prepareLayerForSave`); the binding config lives in\n * `metadata.timeBinding` and the filter is recomputed live on the next\n * activation. `undefined` means no time filter is applied.\n */\n timeFilter?: unknown[];\n /** Transient MapLibre expression applied by the iframe embed API. */\n embedFilter?: unknown[];\n /**\n * Project-persisted boolean MapLibre expression that narrows the features\n * rendered for this layer. Unlike a selection, this leaves the source data\n * intact and keeps non-matching features hidden until the filter is cleared.\n * It is composed with transient filters, quick filters, and rule visibility\n * by the map renderers.\n */\n filterExpression?: unknown[];\n /**\n * Data-driven filter controls authored in the layer's Quick Filters section\n * (issue #2114). Unlike {@link timeFilter} and {@link embedFilter} this is\n * persisted control *state*, not a compiled expression: `@geolibre/map`\n * compiles it at sync time (see `compileQuickFilters`) and combines the\n * result with the transient filters and the rule-based visibility filter, so\n * a host page's filter and a user's filter narrow the layer together.\n */\n quickFilters?: LayerQuickFilter[];\n sourcePath?: string;\n /**\n * Id of the {@link LayerGroup} this layer belongs to, or `undefined` when the\n * layer sits at the top level of the layer panel. Layers sharing a `groupId`\n * are kept contiguous in the store's flat `layers` array so the group renders\n * as one block; see `@geolibre/core`'s `layer-groups` helpers.\n */\n groupId?: string;\n /**\n * Project-persisted connection policy for reloadable layers. Runtime timers\n * are reconstructed from this record when a project opens.\n */\n connection?: LayerConnection;\n}\n\n/**\n * Options for {@link AppState.addTileLayer}: a native raster tile layer (XYZ,\n * WMS, or WMTS) that appears in the Layers panel and persists with the project,\n * just like a layer added through the Add Data dialog. Mirrors the raster\n * `source` fields MapLibre understands so an external plugin can register tile\n * layers without touching the map directly.\n */\nexport interface AddTileLayerOptions {\n /**\n * One or more XYZ tile URL templates (with `{x}`/`{y}`/`{z}` placeholders).\n * At least one non-empty template is required, or the layer renders nothing.\n */\n tiles: string[];\n /**\n * Layer discriminator, controlling how the layer is labelled and (for WMS)\n * dev-server proxied. Defaults to `\"xyz\"`. The layer's `source.type` is\n * always `\"raster\"`, so any other value (such as `\"vector-tiles\"` from an\n * untyped JS caller) throws rather than persisting a mislabelled source.\n */\n type?: \"xyz\" | \"wms\" | \"wmts\" | \"raster\";\n /** Service or base URL recorded on the source for display and restore. */\n url?: string;\n /** Tile size in pixels (default 256). */\n tileSize?: number;\n /** Attribution string shown in the map's attribution control. */\n attribution?: string;\n /** Visible extent as `[west, south, east, north]` in WGS84 degrees. */\n bounds?: [number, number, number, number];\n /** Minimum zoom at which tiles are requested. */\n minzoom?: number;\n /** Maximum zoom at which tiles are requested. */\n maxzoom?: number;\n /** Tile y-axis scheme; `\"tms\"` flips the y origin. Defaults to `\"xyz\"`. */\n scheme?: \"xyz\" | \"tms\";\n /** Initial visibility (default true). */\n visible?: boolean;\n /** Initial opacity in [0, 1] (default 1). */\n opacity?: number;\n /**\n * Extra source fields merged onto the layer's `source` (e.g. the WMS\n * `layers`/`styles`/`format` recorded for restore). The required `type`,\n * `tiles`, and `tileSize` always win over keys supplied here.\n */\n source?: Record<string, unknown>;\n /** Extra metadata merged onto the layer record. */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * A named, collapsible folder in the layer panel that organizes a contiguous\n * run of layers. Groups may be nested by referencing another group as their\n * parent; the root groups omit `parentId`.\n *\n * The group's `visible` flag and `opacity` multiplier are folded into each\n * child layer's effective render state by `applyGroupEffects` before the map\n * syncs, so children keep their own stored `visible`/`opacity` values.\n */\nexport interface LayerGroup {\n id: string;\n name: string;\n /** Parent folder id, or undefined when this folder is at the panel root. */\n parentId?: string;\n /** When true, the group's children are hidden in the panel (not on the map). */\n collapsed: boolean;\n /** Group-level visibility; ANDed with each child layer's own visibility. */\n visible: boolean;\n /** Group-level opacity in [0, 1]; multiplied into each child's opacity. */\n opacity: number;\n}\n\n/**\n * Metadata `sourceKind` marking a live SQL query layer: a GeoJSON-backed layer\n * created from a SQL Workspace result whose DuckDB statement is stored on the\n * layer metadata and re-executed on refresh. Defined here so the desktop app\n * (which owns the query/refresh logic) and `@geolibre/plugins` (which excludes\n * these layers from in-place geometry editing) share one value.\n */\nexport const SQL_QUERY_SOURCE_KIND = \"sql-query\";\n\n/**\n * Metadata `sourceKind` marking a NetCDF/HDF grid baked into an `image` overlay,\n * as opposed to the KML ground overlays that otherwise use that layer type.\n *\n * Defined here so `@geolibre/map` (which must not run its feature-query identify\n * on these) and the desktop app (which owns the symbology panel, the pixel\n * readout, and the spectral profile) share one value.\n */\nexport const NETCDF_IMAGE_SOURCE_KIND = \"netcdf-image\";\n\n/**\n * Detect a DuckDB query layer rendered through the plugin's external deck.gl\n * overlay. Shared by `@geolibre/map`, `@geolibre/plugins`, and the desktop\n * app so the detection criteria cannot drift.\n */\nexport function isDuckDBQueryLayer(\n layer: Pick<GeoLibreLayer, \"metadata\" | \"type\"> | undefined,\n): boolean {\n return (\n layer?.type === \"duckdb-query\" &&\n layer.metadata.sourceKind === \"duckdb-query\" &&\n layer.metadata.externalDeckLayer === true\n );\n}\n\nexport interface MapViewState {\n center: [number, number];\n zoom: number;\n bearing: number;\n pitch: number;\n bbox?: [number, number, number, number];\n}\n\n/**\n * Multi-map (split/grid) support. The workspace can show several map panes in a\n * `rows x cols` grid. Pane 0 is always the *primary* map: it keeps the existing\n * single-map wiring (the global `mapView` + `basemap*` fields, the layer/style\n * panels, plugins, deck.gl). Panes 1..N are *secondary* maps described by\n * `SecondaryMapView` records: every pane shares the same basemap and the same\n * `layers`, but each secondary pane may override which layers are visible so\n * different panes can show different layers. When `syncView` is on,\n * panning/zooming any pane mirrors the camera to every other pane.\n */\nexport interface MapGridLayout {\n /** Grid rows (>= 1). */\n rows: number;\n /** Grid columns (>= 1). `rows * cols` is the total number of panes. */\n cols: number;\n /** When true, all panes share a synchronized camera. */\n syncView: boolean;\n}\n\n/**\n * Which engine draws a map pane.\n *\n * `\"maplibre\"` is the 2D MapLibre GL map that owns the app's plugin, styling,\n * and deck.gl integrations. `\"cesium\"` is the 3D globe (see `CesiumCanvas`),\n * which renders the same shared store state — camera, basemap, layers, group\n * effects — through CesiumJS. `\"mapbox\"` is Mapbox GL JS and `\"arcgis\"` the\n * ArcGIS Maps SDK for JavaScript, loaded from Esri's CDN at runtime (see\n * `ArcgisCanvas`); both draw the same store state through their own engines.\n *\n * Used both for secondary panes ({@link SecondaryMapView.viewKind}) and for the\n * primary workspace ({@link GeoLibreProject.primaryRenderer}), so the two never\n * drift apart.\n */\nexport type MapRendererKind = \"maplibre\" | \"cesium\" | \"mapbox\" | \"arcgis\";\n\n/**\n * The engine that draws the primary map area when a project says nothing. The\n * 2D map: it is the renderer every tool, plugin, and panel is wired to, so an\n * existing project (and a new one) opens exactly as it always did.\n */\nexport const DEFAULT_PRIMARY_RENDERER: MapRendererKind = \"maplibre\";\n\n/**\n * A non-primary map pane: shares the primary map's basemap and layers, with its\n * own camera and per-layer visibility overrides.\n */\nexport interface SecondaryMapView {\n /** Stable id, used as the React key and the sync-group registration id. */\n id: string;\n view: MapViewState;\n /** Optional user-entered label shown on the pane (e.g. a date or scenario). */\n label?: string;\n /**\n * Which rendering engine draws this pane. Defaults to `\"maplibre\"` (the 2D\n * map) when absent, so existing projects and panes are unchanged. `\"cesium\"`\n * renders a 3D globe (see {@link CesiumCanvas}) over the same shared layers.\n */\n viewKind?: MapRendererKind;\n /**\n * Per-layer visibility overrides keyed by layer id. A layer absent from this\n * map inherits the primary map's visibility (`layer.visible`); an entry forces\n * the layer visible (`true`) or hidden (`false`) in this pane only.\n */\n layerVisibility: Record<string, boolean>;\n}\n\n/** The default single-map grid (one pane, sync enabled so it turns on cleanly). */\nexport const DEFAULT_MAP_GRID_LAYOUT: MapGridLayout = {\n rows: 1,\n cols: 1,\n syncView: true,\n};\n\n/** Maximum rows or columns in the map grid (so at most a 4x4 = 16-pane grid). */\nexport const MAX_MAP_GRID_DIM = 4;\n\n/**\n * Live multi-user collaboration (issue #307). These types describe the\n * *ephemeral* session state the store holds while a live session is active. It\n * is intentionally never written to the `.geolibre.json` project file (the\n * `project.ts` serializers never read it) and never tracked in undo history (the\n * store's `partialize` never lists it), so it resets cleanly on reload.\n */\nexport type CollaborationRole = \"host\" | \"guest\";\n\n/** Whether guests may edit (`co-edit`) or only watch (`view-only`). */\nexport type CollaborationMode = \"view-only\" | \"co-edit\";\n\nexport interface ParticipantIdentity {\n provider: string;\n userId: string;\n username: string;\n}\n\nexport interface CollabInvite {\n token: string;\n role: CollaborationMode;\n createdAt: number;\n maxUses?: number;\n useCount: number;\n revoked: boolean;\n}\n\nexport interface CollaborationParticipant {\n clientId: string;\n displayName: string;\n color: string;\n role: CollaborationRole;\n /**\n * Host-set per-participant edit override (#754). `null` follows the session\n * `mode`; `true`/`false` pins this participant to can-edit / view-only. Always\n * `null` for the host (the host can always edit).\n */\n editOverride: boolean | null;\n /** Optional account identity when signed-in identity binding is enabled. */\n identity?: ParticipantIdentity | null;\n}\n\n/** A remote participant's live cursor + viewport, used to render presence. */\nexport interface CollaborationPresence {\n displayName: string;\n color: string;\n cursor?: { lng: number; lat: number } | null;\n view?: MapViewState | null;\n}\n\n/** One in-session chat message (#754). Ephemeral; never persisted to a project. */\nexport interface CollaborationChatMessage {\n /** Server-assigned id (stable React key / dedupe). */\n id: string;\n /** clientId of the author. */\n clientId: string;\n displayName: string;\n color: string;\n text: string;\n /** Optional map coordinate the author attached; clickable to recenter. */\n coordinate?: { lng: number; lat: number } | null;\n /** Server-assigned epoch-ms timestamp. */\n ts: number;\n}\n\nexport interface CollaborationState {\n /** True once connected and joined to a session. */\n isActive: boolean;\n /** True while connecting/reconnecting (UI shows a spinner). */\n connecting: boolean;\n sessionId: string | null;\n clientId: string | null;\n role: CollaborationRole | null;\n mode: CollaborationMode;\n selfName: string;\n selfColor: string;\n participants: CollaborationParticipant[];\n /** Remote presence keyed by participant clientId (never includes self). */\n presence: Record<string, CollaborationPresence>;\n /** When true, this participant's camera follows the host's viewport. */\n followHost: boolean;\n /** Recent session chat, oldest first, capped to a bounded window (#754). */\n chat: CollaborationChatMessage[];\n /** Session flag requiring participants to be signed in. */\n requireIdentity: boolean;\n /**\n * Whether the connected relay has an identity issuer configured. False (the\n * default) means it cannot verify a sign-in, so the host UI hides the\n * \"require a signed-in account\" toggle instead of offering a gate that would\n * lock every guest out.\n */\n identitySupported: boolean;\n /** Layer IDs marked locked by the host. */\n lockedLayerIds: string[];\n /** Active session invites minted by host. */\n invites: CollabInvite[];\n /** Last human-readable error, surfaced in the Collaborate dialog. */\n error: string | null;\n}\n\n/** Map projection the renderer uses. Mirrors the GlobeControl toggle. */\nexport type MapProjection = \"globe\" | \"mercator\";\n\n/**\n * Unit system the scale bar reports distances in. `\"metric\"` uses m/km,\n * `\"imperial\"` uses ft/mi, and `\"nautical\"` uses nautical miles.\n */\nexport type MapScaleUnit = \"metric\" | \"imperial\" | \"nautical\";\n\nexport interface MapPreferences {\n restrictBounds: boolean;\n bounds: [number, number, number, number];\n minZoom: number;\n maxZoom: number;\n maxPitch: number;\n renderWorldCopies: boolean;\n projection: MapProjection;\n /**\n * Celestial body / ellipsoid the project's coordinates describe (keys into the\n * ellipsoid registry in `@geolibre/core`). Drives measurement radii and pairs\n * with planetary basemaps. Defaults to `\"earth\"` (WGS 84).\n */\n ellipsoidId: string;\n /**\n * Unit system the scale bar displays. Defaults to `\"metric\"`; switch to\n * `\"imperial\"` for feet/miles or `\"nautical\"` for nautical miles.\n */\n scaleUnit: MapScaleUnit;\n /**\n * Whether the status bar resolves and shows the ground elevation under the\n * pointer (issue #1813). **Defaults to `false`**: with 3D terrain off the\n * lookup falls back to the public Open-Meteo service, so hovering would send\n * coordinates off the device for a readout the user never asked for. Toggled\n * from Controls -> Elevation.\n */\n showPointerElevation: boolean;\n /** Whether the built-in 3D terrain control and terrain surface are enabled. */\n terrainEnabled: boolean;\n /** Mapbox-only style. New projects use Streets; absent follows the shared basemap. */\n mapboxStyleUrl?: string;\n /**\n * ArcGIS-only basemap: an Esri basemap style id (`arcgis/streets`,\n * `arcgis/imagery`, `osm/standard`, ...). New projects use Streets. Absent\n * follows the shared basemap, translated to tiles the SDK can draw; the id\n * is also set aside when no ArcGIS API key is configured, since Esri's\n * basemap styles service requires one.\n */\n arcgisBasemap?: string;\n /** Cesium imagery override; absent follows the shared project basemap. */\n cesiumBasemap?: import(\"./cesium-imagery\").CesiumBasemapId;\n /**\n * Notation the status bar reports the pointer coordinate in: `\"dd\"` decimal\n * degrees (default), `\"dms\"` degrees/minutes/seconds, `\"ddm\"` degrees and\n * decimal minutes, `\"utm\"` zone easting/northing, `\"mgrs\"`/`\"usng\"` grid\n * references, or `\"epsg\"` x/y in the CRS named by `coordinateEpsgCode`.\n * Stored as a string rather than a union so `@geolibre/core` does not have to\n * depend on the formatter, which lives with the app's DMS helpers and the\n * Gridlines plugin's UTM projection; the app normalises unknown values to\n * `\"dd\"`.\n */\n coordinateFormat: string;\n /**\n * EPSG code the `\"epsg\"` coordinate format projects the pointer into, e.g.\n * `3857` or `32618`. Kept beside `coordinateFormat` rather than inside it so\n * the code survives cycling through the other notations. Absent means the\n * app default (EPSG:3857).\n */\n coordinateEpsgCode?: number;\n /**\n * Whether the map fits to a layer's extent after data is added (Add Data\n * menus, drag-and-drop, file import, tool outputs). Defaults to `true`; turn\n * it off to keep the current view. Explicit \"Zoom to layer\" actions always\n * fit regardless of this setting.\n */\n zoomToNewLayers: boolean;\n}\n\nexport interface RuntimeEnvironmentVariable {\n key: string;\n value: string;\n enabled: boolean;\n /**\n * Absent or `true`: the value is a secret (desktop keeps it in the OS\n * keychain; every egress removes it). `false`: an ordinary value saved in the\n * project and kept on share/export.\n */\n secret?: boolean;\n}\n\ndeclare global {\n interface Window {\n // Runtime environment variables published from project preferences. Shared\n // here so the desktop app and plugins type the global from one source.\n __GEOLIBRE_RUNTIME_ENV__?: Record<string, string>;\n }\n}\n\n/**\n * Geocoding backend selection persisted in the project. The provider id keys\n * into the geocoding registry in `@geolibre/core`; API keys are stored per\n * provider so switching backends does not discard the others' keys. Empty\n * endpoint overrides fall back to the provider's default endpoints.\n */\nexport interface GeocodingPreferences {\n providerId: string;\n /** Per-provider API key / access token, keyed by provider id. */\n apiKeys: Record<string, string>;\n /** Optional custom forward endpoint (else the provider default). */\n forwardEndpoint?: string;\n /** Optional custom reverse endpoint (else the provider default). */\n reverseEndpoint?: string;\n /** Contact email sent to identify the client (used by Nominatim). */\n email?: string;\n}\n\nexport interface ProjectPreferences {\n map: MapPreferences;\n environmentVariables: RuntimeEnvironmentVariable[];\n geocoding: GeocodingPreferences;\n}\n\nexport type ProjectPluginControlPosition =\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\";\n\nexport interface ProjectPluginState {\n manifestUrls: string[];\n activePluginIds: string[];\n mapControlPositions: Record<string, ProjectPluginControlPosition>;\n settings: Record<string, unknown>;\n}\n\nexport const DEFAULT_PROJECT_PREFERENCES: ProjectPreferences = {\n map: {\n restrictBounds: false,\n bounds: [-180, -85, 180, 85],\n minZoom: 0,\n maxZoom: 24,\n maxPitch: 85,\n renderWorldCopies: true,\n projection: \"globe\",\n ellipsoidId: \"earth\",\n scaleUnit: \"metric\",\n // Off by default: turning it on can send pointer coordinates to a public\n // elevation service, which should be an explicit choice.\n showPointerElevation: false,\n terrainEnabled: false,\n coordinateFormat: \"dd\",\n zoomToNewLayers: true,\n mapboxStyleUrl: \"mapbox://styles/mapbox/standard\",\n arcgisBasemap: \"arcgis/streets\",\n // With an Ion token this is the globe's photographic default. The\n // availability gate transparently falls back to the project basemap when\n // no token is configured.\n cesiumBasemap: \"bing-aerial\",\n },\n environmentVariables: [],\n geocoding: {\n providerId: \"nominatim\",\n apiKeys: {},\n },\n};\n\n/**\n * A single user override for one legend item, keyed in {@link LegendConfig.overrides}\n * by a stable item key (a layer id for a whole entry, or `${layerId}::${index}`\n * for an individual class within a graduated/categorized entry).\n */\nexport interface LegendItemOverride {\n /** User-supplied label that replaces the auto-generated one. */\n label?: string;\n /** When true, the item is omitted from the rendered legend. */\n hidden?: boolean;\n}\n\n/** Swatch shape for a hand-authored legend item. */\nexport type LegendCustomShape = \"square\" | \"circle\" | \"line\";\n\n/** One hand-authored legend item: a color swatch plus its label. */\nexport interface LegendCustomItem {\n /** Display label (e.g. an NLCD class name). */\n label: string;\n /** Swatch color as a CSS color (typically `#rrggbb`). */\n color: string;\n /** Swatch shape; defaults to `\"square\"`. */\n shape?: LegendCustomShape;\n /**\n * Symbol size in map pixels (circle radius / line width). Carried so that\n * customizing a proportional-symbol entry keeps the graduated sizes it was\n * seeded from instead of flattening every symbol to one size.\n */\n size?: number;\n}\n\n/**\n * A hand-authored legend entry. Keyed by layer id it REPLACES that layer's\n * auto-derived classes (the fallback when automatic derivation is wrong or\n * impossible, e.g. a paletted land-cover raster like NLCD); keyed by a\n * `custom:` id it renders as a standalone section not tied to any layer.\n */\nexport interface LegendCustomEntry {\n /** Optional heading; a layer-keyed entry falls back to the layer name. */\n title?: string;\n /** The items to render, top to bottom. */\n items: LegendCustomItem[];\n}\n\n/** Map corner the on-map legend panel docks to. */\nexport type LegendPanelPosition = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n\n/**\n * User customizations for the legend (the on-map Legend panel and the Print\n * Layout legend). The legend itself is always derived from the visible layers'\n * symbology; this record only stores the edits layered on top (title, ordering,\n * per-item rename/hide, hand-authored entries), so it survives layer additions\n * and removals and is persisted in the `.geolibre.json` project.\n */\nexport interface LegendConfig {\n /** Heading drawn above the legend entries. */\n title: string;\n /** When true, classes are grouped under a per-layer heading. */\n groupByLayer: boolean;\n /**\n * Custom top-level entry order by layer id, top-first. Layer ids not listed\n * keep their default order after the listed ones.\n */\n order: string[];\n /** Per-item overrides keyed by stable item key. */\n overrides: Record<string, LegendItemOverride>;\n /**\n * Hand-authored entries keyed by layer id (replacing that layer's\n * auto-derived classes) or by a standalone `custom:` id. See\n * {@link LegendCustomEntry}.\n */\n customEntries?: Record<string, LegendCustomEntry>;\n /** Whether the on-map Legend panel is open (persisted with the project). */\n panelVisible?: boolean;\n /** Whether the open panel is collapsed to just its header bar. */\n panelCollapsed?: boolean;\n /** Map corner the on-map Legend panel docks to; defaults to `\"top-left\"`. */\n panelPosition?: LegendPanelPosition;\n /**\n * User-resized panel width in px (via the corner drag handles). Absent means\n * the default width; height absent means auto-fit to content within the map.\n */\n panelWidth?: number;\n /** User-resized panel height in px. See {@link LegendConfig.panelWidth}. */\n panelHeight?: number;\n}\n\n// Frozen so the shared singleton can be safely spread (`{ ...DEFAULT_LEGEND_CONFIG }`)\n// at call sites without risk of a future in-place mutation corrupting the nested\n// `order`/`overrides` references that the spread keeps sharing.\nexport const DEFAULT_LEGEND_CONFIG: LegendConfig = Object.freeze({\n title: \"Legend\",\n groupByLayer: true,\n order: Object.freeze([] as string[]) as string[],\n overrides: Object.freeze({} as Record<string, LegendItemOverride>) as Record<\n string,\n LegendItemOverride\n >,\n});\n\n/**\n * Camera target captured for a story chapter. `pitch` and `bearing` are\n * optional in memory: a location built in code may omit them, and every engine\n * then keeps that part of the current camera (see `storyLocationView`). A\n * chapter read from a project file or a CSV never omits them: loading fills an\n * absent value with 0 (`normalizeStoryChapter`, `parseStoryMapCsv`), so a file\n * without them still plays north-up and flat, as it always has.\n */\nexport interface StoryChapterLocation {\n center: [number, number];\n zoom: number;\n pitch?: number;\n bearing?: number;\n}\n\n/** Where a chapter's text panel sits over the map. */\nexport type StoryChapterAlignment = \"left\" | \"center\" | \"right\" | \"full\";\n\n/** How the map transitions to a chapter's location. */\nexport type StoryChapterAnimation = \"flyTo\" | \"easeTo\" | \"jumpTo\";\n\n/** A layer opacity change triggered when a chapter is entered or exited. */\nexport interface StoryLayerOpacityChange {\n /** Stable identity for React list keys; optional for older project files. */\n id?: string;\n /** GeoLibre store layer id whose opacity should change. */\n layerId: string;\n opacity: number;\n /** Transition duration in milliseconds. */\n duration?: number;\n}\n\n/** A single scene in a scroll-driven story map. */\nexport interface StoryChapter {\n id: string;\n title: string;\n description: string;\n /** Optional image shown in the chapter panel (URL or data URI). */\n image?: string;\n alignment: StoryChapterAlignment;\n /** Hide the text panel while still transitioning the map. */\n hidden: boolean;\n location: StoryChapterLocation;\n mapAnimation: StoryChapterAnimation;\n /** Slowly rotate the camera once the transition settles. */\n rotateAnimation: boolean;\n onChapterEnter: StoryLayerOpacityChange[];\n onChapterExit: StoryLayerOpacityChange[];\n}\n\nexport type StoryInsetPosition = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n\n/**\n * A non-chapter intro/outro slide shown before the first or after the last\n * chapter (#998). `\"none\"` disables it. The remaining modes are:\n * - `\"blank\"` a solid screen in the panel theme color,\n * - `\"black\"` a solid black screen,\n * - `\"global\"` a zoomed-out global view of the map with no text,\n * - `\"adjacent\"` the camera of the neighboring chapter (the first chapter for\n * the start slide, the last chapter for the closing slide) with all text and\n * media hidden, so the presenter can zoom into the full content next.\n */\nexport type StorySlideMode = \"none\" | \"blank\" | \"black\" | \"global\" | \"adjacent\";\n\n/** A start/closing slide that is actually shown (every mode except `\"none\"`). */\nexport type StoryActiveSlideMode = Exclude<StorySlideMode, \"none\">;\n\n/** Scroll-driven story map authored on top of a GeoLibre project. */\nexport interface StoryMap {\n title: string;\n subtitle: string;\n byline: string;\n footer: string;\n theme: \"light\" | \"dark\";\n showMarkers: boolean;\n markerColor: string;\n inset: boolean;\n insetPosition: StoryInsetPosition;\n /**\n * Start the presentation with the chapter itinerary/navigation panel hidden,\n * revealing chapters one at a time instead of listing every upcoming location\n * up front (#995). The presenter still offers a toggle to open the list.\n */\n hideChapterNav: boolean;\n /** Optional intro slide shown before the first chapter (#998). */\n startSlide: StorySlideMode;\n /** Optional closing slide shown after the last chapter (#998). */\n endSlide: StorySlideMode;\n chapters: StoryChapter[];\n}\n\nexport const DEFAULT_STORY_MAP: StoryMap = {\n title: \"\",\n subtitle: \"\",\n byline: \"\",\n footer: \"\",\n theme: \"dark\",\n showMarkers: false,\n markerColor: \"#3fb1ce\",\n inset: false,\n insetPosition: \"bottom-left\",\n hideChapterNav: false,\n startSlide: \"none\",\n endSlide: \"none\",\n chapters: [],\n};\n\n/**\n * One step in a {@link ProcessingModel}: a processing tool invoked with a fixed\n * set of parameters. The runner chains steps by feeding each step's output layer\n * into the next step's input layer parameter (`inputParam`, default `\"layer\"`),\n * so a step's stored `parameters` for that input is ignored for every step after\n * the first.\n */\nexport interface ProcessingModelStep {\n /** Stable id, unique within the model (used as the React key and run label). */\n id: string;\n /** The processing tool's registry id (e.g. `\"buffer\"`). */\n toolId: string;\n /** Parameter values keyed by the tool's parameter ids. */\n parameters: Record<string, unknown>;\n /**\n * Which `type: \"layer\"` parameter receives the previous step's output. Defaults\n * to `\"layer\"`; set it for tools whose primary input is named differently.\n */\n inputParam?: string;\n}\n\n/**\n * What flows along a model edge. Vector nodes exchange FeatureCollections;\n * raster nodes exchange GeoTIFF bytes. A port declaring `\"any\"` accepts either\n * and is resolved to a concrete kind at run time by whatever is wired into it.\n */\nexport type ModelPortKind = \"vector\" | \"raster\" | \"any\";\n\n/** One connection point on a {@link ModelGraphNode}. */\nexport interface ModelGraphPort {\n /**\n * Port id, unique within its node and direction. For a tool node's inputs\n * this is the underlying tool parameter id, so wiring an edge and setting the\n * parameter by hand are the same operation.\n */\n id: string;\n label: string;\n kind: ModelPortKind;\n /** Inputs only: the run fails when nothing is wired in and no value is set. */\n required?: boolean;\n}\n\n/**\n * What a node does. `input` sources an existing project layer, `tool` runs a\n * processing algorithm, and `output` names a result to add back to the map.\n */\nexport type ModelGraphNodeKind = \"input\" | \"tool\" | \"output\";\n\n/** One node on the Model Builder canvas. */\nexport interface ModelGraphNode {\n /** Stable id, unique within the graph; referenced by {@link ModelGraphEdge}. */\n id: string;\n kind: ModelGraphNodeKind;\n /** Canvas position in graph coordinates (unscaled by zoom). */\n x: number;\n y: number;\n /** `input` nodes: the project layer id this node sources. */\n layerId?: string;\n /**\n * `tool` nodes: the tool's id within {@link provider}'s registry. Kept\n * separate from the provider so the same short id can exist in both.\n */\n toolId?: string;\n /** `tool` nodes: which registry resolves {@link toolId}. */\n provider?: ModelToolProvider;\n /** `tool` nodes: parameter values for everything not supplied by an edge. */\n parameters?: Record<string, unknown>;\n /** `output` nodes: the layer name given to the result added to the map. */\n name?: string;\n}\n\n/**\n * A directed connection from one node's output port to another node's input\n * port. Ports are named, so a tool with several inputs (Clip's target and\n * overlay, say) wires each one unambiguously.\n */\nexport interface ModelGraphEdge {\n id: string;\n from: string;\n fromPort: string;\n to: string;\n toPort: string;\n}\n\n/** The node-and-edge graph authored on the Model Builder canvas. */\nexport interface ProcessingModelGraph {\n nodes: ModelGraphNode[];\n edges: ModelGraphEdge[];\n}\n\n/** Which registry a {@link ModelGraphNode.toolId} is resolved against. */\nexport type ModelToolProvider = \"vector\" | \"whitebox\";\n\n/**\n * A reusable processing pipeline (\"model\" in QGIS Graphical Modeler / ArcGIS\n * ModelBuilder terms), saved in the project file so it can be reloaded and\n * re-run.\n *\n * Two shapes coexist. {@link steps} is the original strictly linear chain, and\n * remains the only thing older builds understand. {@link graph} is the\n * Model Builder's directed graph, which supports multi-input tools, branches\n * and merges. When both are present `graph` wins; a model saved by the canvas\n * also writes a `steps` projection whenever its graph happens to be a single\n * chain, so older builds can still run it.\n */\nexport interface ProcessingModel {\n id: string;\n name: string;\n steps: ProcessingModelStep[];\n graph?: ProcessingModelGraph;\n}\n\n/**\n * Which dialog family executed a {@link ProcessingRun}. Drives the History\n * panel's re-run routing: each kind maps to the dialog (and store open-flag)\n * that can be reopened pre-filled with the run's parameters.\n */\nexport type ProcessingRunKind =\n | \"vector\"\n | \"statistics\"\n | \"network\"\n | \"whitebox\"\n | \"raster\"\n | \"conversion\"\n | \"algorithm\";\n\nexport type ProcessingRunStatus = \"success\" | \"error\";\n\n/** Upper bound on persisted processing-history entries (oldest are dropped). */\nexport const MAX_PROCESSING_HISTORY = 100;\n\n/**\n * One recorded processing tool run (issue #1292). Appended by every processing\n * dialog when a run finishes and persisted in the project file, so a saved\n * project documents how its derived layers were produced. Parameter values are\n * stored exactly as the dialog dispatched them (layer parameters hold layer\n * ids), which is what re-run pre-fills and \"Copy as Python\" emit.\n */\nexport interface ProcessingRun {\n /** Stable id, unique within the project (React key and update key). */\n id: string;\n kind: ProcessingRunKind;\n /** The tool's registry id (e.g. `\"buffer\"`, `\"slope\"`). */\n toolId: string;\n /** Human-readable tool name captured at run time. */\n toolName: string;\n /** Engine that executed the run: `client`, `wasm`, `sidecar`, `pyodide`, `browser`. */\n engine: string;\n /** Parameter values keyed by the tool's parameter ids. */\n parameters: Record<string, unknown>;\n /** Names of input layers referenced by layer parameters, keyed by layer id. */\n inputLayerNames?: Record<string, string>;\n /** Names of layers the run added to the map. */\n outputLayerNames?: string[];\n /** Input file path/name, for file-based tools (raster/conversion). */\n inputPath?: string;\n /** Output file path/name, for file-based tools (raster/conversion). */\n outputPath?: string;\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Wall-clock duration of the run in milliseconds. */\n durationMs?: number;\n status: ProcessingRunStatus;\n /** Error message when `status` is `\"error\"`. */\n error?: string;\n}\n\n/**\n * A pending \"re-run from History\" request. The History panel writes it to the\n * store and opens the target dialog; the dialog consumes it (pre-filling its\n * parameter form) and clears it. `autoRun` asks the dialog to start the run\n * immediately after pre-filling (plain Re-run vs Edit & re-run).\n */\nexport interface ProcessingRerunRequest {\n kind: ProcessingRunKind;\n toolId: string;\n parameters: Record<string, unknown>;\n /** Engine to preselect, when the dialog offers a choice. */\n engine?: string;\n autoRun?: boolean;\n}\n\n/** Column-count bounds for the Dashboard panel's widget grid. */\nexport const MIN_DASHBOARD_COLUMNS = 1;\nexport const MAX_DASHBOARD_COLUMNS = 6;\nexport const DEFAULT_DASHBOARD_COLUMNS = 2;\n\n/** The chart a {@link DashboardWidget} draws. Mirrors the attribute Charts\n * panel's types so a widget reuses the same rendering. The `\"indicator\"` type\n * is a non-chart KPI tile (issue #1381). */\nexport type DashboardWidgetType =\n | \"histogram\"\n | \"scatter\"\n | \"bar\"\n | \"line\"\n | \"box\"\n | \"pie\"\n | \"indicator\"\n | \"selector\"\n | \"list\";\n\n/** How a bar widget reduces its category groups. */\nexport type DashboardWidgetAggregation = \"count\" | \"sum\" | \"mean\";\n\n/**\n * One chart in the Dashboard panel: a chart type bound to a layer and the\n * field(s) it plots. Which `field*`/`category`/`aggregation` keys apply depends\n * on `type` (histogram/line/box use `field`; scatter uses `xField`/`yField`;\n * bar uses `category` + `aggregation` and, for sum/mean, `valueField`). Unused\n * keys are simply ignored, so the record stays flat and easy to hand-edit.\n * Saved in the project file so a dashboard reopens with its widgets intact.\n */\nexport interface DashboardWidget {\n /** Stable id, unique within the project (React key and store key). */\n id: string;\n /** The layer whose features feed this widget. */\n layerId: string;\n /** The chart to draw. */\n type: DashboardWidgetType;\n /** Optional custom title; the panel derives a label from the fields if absent. */\n title?: string;\n /** Optional hex color (`#rgb`/`#rrggbb`) for the chart's marks. Single-series\n * charts use it as the series color; bar/pie use it as the base of a\n * monochromatic ramp. Defaults to the theme primary / multi-color palette. */\n color?: string;\n /** Value field for histogram/line/box. */\n field?: string;\n /** X-axis field for scatter. */\n xField?: string;\n /** Y-axis field for scatter. */\n yField?: string;\n /** Number of bins for a histogram. */\n bins?: number;\n /** Category field for a bar chart. */\n category?: string;\n /** Aggregation for a bar chart (default `count`). */\n aggregation?: DashboardWidgetAggregation;\n /** Value field a bar chart's sum/mean reduces (ignored for `count`). */\n valueField?: string;\n /** Indicator widget: aggregation function for the KPI value (issue #1381).\n * Extends bar aggregation with min, max, and median. */\n indicatorAggregation?: IndicatorAggregation;\n /** Indicator widget: optional prefix (e.g. \"€\", \"$\"). */\n prefix?: string;\n /** Indicator widget: optional suffix (e.g. \" kg\", \" ha\"). */\n suffix?: string;\n /** Selector widget: whether multiple values can be picked (default false). */\n multiple?: boolean;\n /** List widget: columns to display. */\n listFields?: string[];\n /** List widget: field to sort by. */\n sortBy?: string;\n /** List widget: sort direction (default \"desc\"). */\n sortDir?: \"asc\" | \"desc\";\n /** List widget: max rows to show (default 20). */\n limit?: number;\n}\n\n/** Aggregation functions for indicator widgets (issue #1381). Extends the bar\n * widget aggregation with min, max, and median. */\nexport type IndicatorAggregation = \"count\" | \"sum\" | \"mean\" | \"min\" | \"max\" | \"median\";\n\n/**\n * What slice of a layer's styling a Style Manager entry captures (issue #1294).\n *\n * - `\"style\"`: the layer's complete {@link LayerStyle} snapshot.\n * - `\"symbol\"`: fill/stroke/marker/pattern symbology only, so it can restyle a\n * layer without touching its labels or renderer configuration.\n * - `\"labels\"`: the {@link LabelStyle} block only.\n * - `\"ramp\"`: the color ramp + classification settings only (ramp name, class\n * count, classification scheme), independent of the attribute the target\n * layer classifies on.\n */\nexport type StyleLibraryEntryKind = \"style\" | \"symbol\" | \"labels\" | \"ramp\";\n\n/**\n * One saved, reusable style in the Style Manager library (issue #1294). The\n * payload is a {@link LayerStyle} subset chosen by {@link kind}; applying an\n * entry merges that subset onto the target layer's style. Entries live either\n * in the app-level library (persisted across projects) or embedded in a\n * project file's `styleLibrary` array.\n */\nexport interface StyleLibraryEntry {\n /** Stable id, used as the IndexedDB/store key; upserts overwrite by id. */\n id: string;\n /** Display name shown in the Style Manager. */\n name: string;\n /** Which style subset {@link style} carries. */\n kind: StyleLibraryEntryKind;\n /** Free-form tags for filtering the library. */\n tags: string[];\n /** The saved {@link LayerStyle} subset (see {@link kind}). */\n style: Partial<LayerStyle>;\n /** ISO timestamp of the last save; empty for built-in presets. */\n updatedAt: string;\n}\n\n/**\n * One saved, re-addable layer in the Layer Library (issue #1520) — the \"My\n * Data\" section of the Browser panel. Stores the layer's **source\n * specification** plus its full presentation state (style, labels, filters,\n * joins, virtual fields, attribute form), deliberately *not* the data, so the\n * library stays small and an entry always reflects the current contents of its\n * source.\n *\n * The exception is a layer whose features exist only in memory (drawn features,\n * processing output) or in a local file: those have no re-fetchable source, so\n * their features are embedded in {@link geojson} behind a size cap. See\n * `layer-library.ts` for how an entry is captured and re-added.\n */\nexport interface LayerLibraryEntry {\n /** Stable id, used as the IndexedDB/store key; upserts overwrite by id. */\n id: string;\n /** Display name shown in the Browser panel's My Data section. */\n name: string;\n /** ISO timestamp of the last save. */\n addedAt: string;\n /** The layer type to recreate ({@link GeoLibreLayer.type}). */\n layerType: LayerType;\n /** The MapLibre/plugin source spec to recreate the layer from. */\n source: Record<string, unknown>;\n /** The saved layer's complete {@link LayerStyle} (labels included). */\n style: LayerStyle;\n /** Layer opacity in [0, 1]. */\n opacity: number;\n /** The layer metadata the renderers and plugin sync modules key off. */\n metadata: Record<string, unknown>;\n /** Absolute local path, for a layer read from a file on disk. */\n sourcePath?: string;\n /** Persistent attribute joins to reapply. */\n joins?: LayerJoin[];\n /** Expression-backed virtual fields to reapply. */\n virtualFields?: LayerVirtualField[];\n /** Per-field edit-widget/constraint configuration to reapply. */\n attributeForm?: AttributeFormConfig;\n /** Popup/tooltip design to reapply. */\n popup?: LayerPopupConfig;\n /** User-authored catalog metadata to reapply. */\n descriptiveMetadata?: LayerDescriptiveMetadata;\n /**\n * Embedded features, present only for a layer whose source cannot be\n * re-read (in-memory features) or whose local file may be unavailable.\n */\n geojson?: FeatureCollection;\n /**\n * True when the entry can only be re-added by a host that can read\n * {@link sourcePath} — i.e. its features were too large to embed, so the\n * desktop app must re-read the file and the browser build cannot.\n */\n needsLocalFile?: boolean;\n}\n\n/** One saved template in the Template Library. */\nexport interface ProjectTemplateEntry {\n id: string;\n name: string;\n description?: string;\n project: GeoLibreProject;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface GeoLibreProject {\n id?: string;\n version: string;\n name: string;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n /** Custom colour for the Blank background. Null uses the current theme default. */\n blankBackgroundColor?: string | null;\n layers: GeoLibreLayer[];\n /**\n * Layer selected in the Layers panel when the project was saved. Omitted by\n * legacy projects; `null` deliberately restores no active layer.\n */\n selectedLayerId?: string | null;\n /** Named folders that organize the flat `layers` list in the layer panel. */\n layerGroups?: LayerGroup[];\n styles: Record<string, LayerStyle>;\n preferences: ProjectPreferences;\n plugins?: ProjectPluginState;\n /** User customizations for the Print Layout legend. */\n legend?: LegendConfig;\n /**\n * Print Layout composer settings (title, page size, orientation, blocks,\n * atlas). Omitted while the composer is untouched, so projects that never\n * opened it are unaffected (GeoLibre discussion #1992).\n */\n printLayout?: PrintLayoutConfig;\n storymap?: StoryMap;\n /** Saved processing pipelines (batch/model chaining; issue #344). */\n models?: ProcessingModel[];\n /** Recorded processing tool runs (Processing History; issue #1292). */\n processingHistory?: ProcessingRun[];\n /** Saved Dashboard panel chart widgets (issue #401). */\n widgets?: DashboardWidget[];\n /** Number of columns in the Dashboard widget grid; omitted when default. */\n dashboardColumns?: number;\n /**\n * Multi-map grid layout; omitted (single 1x1 pane) for default projects so\n * legacy readers and single-map files are unaffected.\n */\n mapLayout?: MapGridLayout;\n /**\n * Secondary map panes (everything past the primary pane). Omitted when the\n * grid is a single pane. The primary pane uses the top-level `mapView` /\n * `basemap*` fields.\n */\n secondaryMapViews?: SecondaryMapView[];\n /** User-entered label for the primary pane; omitted when empty. */\n primaryMapLabel?: string;\n /**\n * Which engine draws the primary map area (issue #2217). Omitted for the\n * default 2D map, so every project written before this existed — and every\n * project that never leaves MapLibre — is byte-identical to before. A project\n * saved as `\"cesium\"` reopens directly on the 3D globe.\n *\n * This is independent of {@link mapLayout}: a 1x1 workspace can be either\n * renderer, and a multi-pane grid can still mix the two through each pane's\n * {@link SecondaryMapView.viewKind}.\n */\n primaryRenderer?: MapRendererKind;\n /**\n * Project-scoped Style Manager entries (issue #1294), so a project can carry\n * its reusable styles to teammates. Omitted when empty; the app-level\n * library is persisted outside the project file and never serialized here.\n */\n styleLibrary?: StyleLibraryEntry[];\n /** Anchored review comments on map points or features (issue #1518). */\n comments?: ProjectComment[];\n /**\n * Interaction state applied when the project opens: the Identify target and\n * which map controls and toolbar panels start shown (issue #2688). Written by\n * the Python `geolibre` package from `set_identify` / `show_control`, so a\n * saved or HTML-exported notebook map opens the way it was set up. Omitted\n * by default; the app keeps what it loaded and writes it back unchanged.\n */\n interaction?: ProjectInteraction;\n metadata: Record<string, unknown>;\n}\n\n/**\n * Startup interaction state carried by a project (see\n * {@link GeoLibreProject.interaction}).\n */\nexport interface ProjectInteraction {\n /**\n * Identify target on open: a layer id, `\"all\"` for every visible queryable\n * layer, a list of layer ids to identify only those, or `null` for off.\n */\n identify?: string | string[] | null;\n /**\n * Map control or toolbar panel name (`\"search\"`, `\"bookmark\"`, `\"globe\"`,\n * ...) to whether it starts shown. Unknown names are ignored on open.\n */\n controls?: Record<string, boolean>;\n}\n\nexport type CommentAnchor =\n | { type: \"point\"; lngLat: [number, number] }\n | { type: \"feature\"; layerId: string; featureId: string | number; lngLat?: [number, number] };\n\nexport interface CommentAuthor {\n name: string;\n color: string;\n}\n\nexport interface CommentReply {\n id: string;\n author: CommentAuthor;\n body: string;\n createdAt: string;\n}\n\nexport interface ProjectComment {\n id: string;\n anchor: CommentAnchor;\n author: CommentAuthor;\n body: string;\n createdAt: string;\n resolved: boolean;\n replies: CommentReply[];\n}\n\nexport interface RecentProjectEntry {\n path: string;\n name: string;\n openedAt: string;\n}\n\n/**\n * Size at or above which a local vector file is read through DuckDB instead of\n * the in-memory JavaScript readers, and the feature count above which the user\n * is asked before every feature is materialized as GeoJSON.\n *\n * These live in core so the desktop loaders (`duckdb-vector-guard.ts`) and the\n * Add Vector Layer panel (`@geolibre/plugins`, which configures the\n * `maplibre-gl-vector` control's `autoThreshold`) switch strategy at the *same*\n * numbers. Before they were unified the two entry points disagreed — the panel\n * tiled at 25 MB / 50k while drag-and-drop stayed in memory to 100 MB — so the\n * same file behaved differently depending on how it was added, and no single\n * number could be documented.\n */\nexport const DUCKDB_VECTOR_ROUTE_BYTES = 100 * 1024 * 1024; // 100 MB\nexport const DUCKDB_VECTOR_FEATURE_WARN_COUNT = 100_000;\n\n/** A collection whose coordinates cannot be WGS84 longitude/latitude. */\nexport interface NonGeographicCoordinates {\n /** How many coordinates were inspected. */\n sampled: number;\n /** The largest |x| seen — a longitude may not exceed 180. */\n maxAbsX: number;\n /** The largest |y| seen — a latitude may not exceed 90. */\n maxAbsY: number;\n}\n\nconst MAX_WGS84_LON = 180;\nconst MAX_WGS84_LAT = 90;\n\n/**\n * Detect a collection that declares (or is assumed to be) WGS84 but carries\n * projected coordinates — the failure mode where a layer loads cleanly, appears\n * in the Layers panel, and renders nowhere because its \"longitude\" is a easting\n * in metres or feet.\n *\n * GeoLibre honours whatever CRS a file declares, so a file that declares\n * `CRS84`/`GCS_WGS_1984` while holding State Plane or Albers coordinates is\n * passed through untouched and lands off the map with no error. Callers use\n * this to warn instead of failing silently; it does not guess the true CRS,\n * which only the user knows.\n *\n * Sampling stops at `sampleLimit` coordinates: out-of-range values are a\n * property of the whole file, so a prefix is enough and a 3-million-coordinate\n * collection is not walked twice.\n *\n * @returns Details when a coordinate is out of geographic range, else null.\n */\nexport function detectNonGeographicCoordinates(\n geojson: GeoJSON.FeatureCollection | undefined,\n sampleLimit = 1000,\n): NonGeographicCoordinates | null {\n if (!geojson?.features?.length) return null;\n let sampled = 0;\n let maxAbsX = 0;\n let maxAbsY = 0;\n let offending = false;\n\n const visit = (coords: unknown): void => {\n if (sampled >= sampleLimit || !Array.isArray(coords)) return;\n if (typeof coords[0] === \"number\" && typeof coords[1] === \"number\") {\n const x = Math.abs(coords[0]);\n const y = Math.abs(coords[1]);\n sampled += 1;\n // Gated on finiteness for the same reason as `offending` below: an\n // Infinity would otherwise be reported as the offending magnitude in the\n // warning, hiding the real out-of-range value.\n if (Number.isFinite(x) && x > maxAbsX) maxAbsX = x;\n if (Number.isFinite(y) && y > maxAbsY) maxAbsY = y;\n // NaN/Infinity are a different defect (a broken file, not a CRS mismatch),\n // so only finite out-of-range values count.\n if (Number.isFinite(x) && Number.isFinite(y) && (x > MAX_WGS84_LON || y > MAX_WGS84_LAT)) {\n offending = true;\n }\n return;\n }\n for (const part of coords) {\n if (sampled >= sampleLimit) return;\n visit(part);\n }\n };\n\n for (const feature of geojson.features) {\n if (sampled >= sampleLimit) break;\n const geometry = feature?.geometry as {\n coordinates?: unknown;\n geometries?: { coordinates?: unknown }[];\n } | null;\n // A GeometryCollection holds its coordinates one level down, under\n // `geometries[]`, so it has no `coordinates` of its own to visit.\n if (geometry?.coordinates !== undefined) visit(geometry.coordinates);\n else if (Array.isArray(geometry?.geometries)) {\n for (const member of geometry.geometries) {\n if (sampled >= sampleLimit) break;\n if (member?.coordinates !== undefined) visit(member.coordinates);\n }\n }\n }\n\n return offending ? { sampled, maxAbsX, maxAbsY } : null;\n}\n","/**\n * Pure helpers for the diagram renderer (per-feature pie/donut/bar charts,\n * QGIS-style diagram symbology). Everything here is DOM-free so the desktop\n * app, the deck.gl overlay, and the node test suite share one implementation:\n * anchor placement, value extraction, and size scaling. The canvas drawing and\n * deck.gl layer construction live in `@geolibre/plugins`.\n */\nimport type { Feature, FeatureCollection, Geometry, Position } from \"geojson\";\nimport { styleValue, type LayerStyle } from \"./types\";\n\n/**\n * Hard cap on how many features get a diagram. Each diagram is rasterized into\n * an icon atlas, so an unbounded layer (e.g. a million-point parquet) would\n * hang the UI; typical thematic layers (counties, districts) sit well below\n * this.\n */\nexport const MAX_DIAGRAM_FEATURES = 2000;\n\n/**\n * Hard cap on how many raw features {@link collectDiagramData} visits while\n * looking for drawable diagrams. Without it, a layer whose drawable ratio is\n * low (sparse attributes, degenerate geometry) degenerates to a full scan of\n * an arbitrarily large collection on every rebuild. Features past this bound\n * are treated as truncation, which the Style Panel surfaces.\n */\nexport const MAX_DIAGRAM_SCAN_FEATURES = 100_000;\n\n/** Smallest rendered diagram size in pixels under scaled sizing. */\nexport const MIN_DIAGRAM_SIZE = 8;\n\n/** One feature's chart, ready for drawing. */\nexport interface DiagramDatum {\n /** Anchor position: the point location or polygon/line midpoint centroid. */\n position: [number, number];\n /** One value per configured diagram field (negatives clamped to 0). */\n values: number[];\n /** Sum of {@link values}. */\n total: number;\n /** The value driving scaled sizing (total, attribute value, or 1 for fixed). */\n sizeValue: number;\n}\n\n/** The per-layer diagram dataset plus the maxima needed for scaling. */\nexport interface DiagramData {\n data: DiagramDatum[];\n /** Largest {@link DiagramDatum.sizeValue} (0 when there is no data). */\n maxSizeValue: number;\n /** Largest single field value across all features (for bar axis scaling). */\n maxFieldValue: number;\n /** Largest {@link DiagramDatum.total} across all features. */\n maxTotal: number;\n /** True when the feature cap truncated the dataset. */\n truncated: boolean;\n}\n\n/**\n * Whether a layer style has a drawable diagram configuration: a chart type and\n * at least one mapped attribute.\n *\n * @param style - The layer style to test.\n */\nexport function isDiagramStyleEnabled(style: LayerStyle): boolean {\n return (\n styleValue(style, \"diagramType\") !== \"none\" &&\n styleValue(style, \"diagramFields\").some((field) => field.property !== \"\")\n );\n}\n\n/**\n * Whether a heatmap/cluster point renderer suppresses the layer's diagrams.\n * The point renderer only applies (and is only user-editable) on point-only\n * layers, so a stale `pointRenderer` value on a layer that has since gained\n * non-point geometry must not block diagrams — this mirrors the Style Panel's\n * own `!supportsPointRenderer || pointRenderer === \"single\"` visibility gate,\n * keeping the render and legend gates consistent with what the panel lets the\n * user configure. The full feature list is scanned (matching the panel's\n * isPointOnlyGeoJsonLayer, which uses an uncapped every()) so the two gates\n * can never disagree on layers whose non-point features sit past a sampling\n * window; the scan only runs for the uncommon non-\"single\" renderer values.\n *\n * @param geojson - The layer's feature collection (undefined suppresses nothing).\n * @param style - The layer style carrying `pointRenderer`.\n */\nexport function diagramsSuppressedByPointRenderer(\n geojson: FeatureCollection | undefined,\n style: LayerStyle,\n): boolean {\n if (styleValue(style, \"pointRenderer\") === \"single\") return false;\n const features = geojson?.features ?? [];\n if (features.length === 0) return false;\n return features.every((feature) => {\n const type = feature?.geometry?.type;\n return type === \"Point\" || type === \"MultiPoint\";\n });\n}\n\n/** Shoelace area of a linear ring (planar, in degree space; sign = winding). */\nfunction ringArea(ring: Position[]): number {\n let area = 0;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n // Same sign convention as ringCentroid's cross terms, so centroid / area\n // divisions stay consistent for either winding.\n area += ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];\n }\n return area / 2;\n}\n\n/**\n * Area-weighted centroid of a polygon's outer ring, falling back to the ring's\n * vertex average when the area is degenerate. Planar math in degree space is\n * fine at diagram-placement accuracy.\n */\nfunction ringCentroid(ring: Position[]): [number, number] | null {\n if (ring.length === 0) return null;\n const area = ringArea(ring);\n if (Math.abs(area) < 1e-12) {\n let x = 0;\n let y = 0;\n for (const [lng, lat] of ring) {\n x += lng;\n y += lat;\n }\n return [x / ring.length, y / ring.length];\n }\n let cx = 0;\n let cy = 0;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n const cross = ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];\n cx += (ring[j][0] + ring[i][0]) * cross;\n cy += (ring[j][1] + ring[i][1]) * cross;\n }\n return [cx / (6 * area), cy / (6 * area)];\n}\n\nfunction finitePosition(position: Position | undefined): [number, number] | null {\n if (!position) return null;\n const [lng, lat] = position;\n return Number.isFinite(lng) && Number.isFinite(lat) ? [lng as number, lat as number] : null;\n}\n\n/**\n * Where a feature's diagram is anchored: the point itself, a line's middle\n * vertex, or the polygon centroid (largest part of a multi-polygon). Returns\n * null for empty/degenerate geometry so the feature is skipped.\n *\n * @param geometry - The feature geometry (may be null for null-geometry rows).\n */\nexport function diagramAnchor(geometry: Geometry | null | undefined): [number, number] | null {\n if (!geometry) return null;\n switch (geometry.type) {\n case \"Point\":\n return finitePosition(geometry.coordinates);\n case \"MultiPoint\":\n return finitePosition(geometry.coordinates[0]);\n case \"LineString\": {\n const coords = geometry.coordinates;\n return finitePosition(coords[Math.floor(coords.length / 2)]);\n }\n case \"MultiLineString\": {\n // Anchor on the longest part (by vertex count) so the diagram sits on\n // the feature's dominant line.\n let longest: Position[] = [];\n for (const part of geometry.coordinates) {\n if (part.length > longest.length) longest = part;\n }\n return finitePosition(longest[Math.floor(longest.length / 2)]);\n }\n case \"Polygon\":\n return ringCentroid(geometry.coordinates[0] ?? []);\n case \"MultiPolygon\": {\n let best: Position[] = [];\n let bestArea = -1;\n for (const polygon of geometry.coordinates) {\n const ring = polygon[0] ?? [];\n const area = Math.abs(ringArea(ring));\n if (area > bestArea) {\n bestArea = area;\n best = ring;\n }\n }\n return ringCentroid(best);\n }\n case \"GeometryCollection\": {\n for (const child of geometry.geometries) {\n const anchor = diagramAnchor(child);\n if (anchor) return anchor;\n }\n return null;\n }\n default:\n return null;\n }\n}\n\n/** Reads a feature property as a non-negative finite number (else 0). */\nfunction readValue(feature: Feature, property: string): number {\n const raw = feature.properties?.[property];\n const num = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(num) && num > 0 ? num : 0;\n}\n\n/**\n * Extract the per-feature diagram dataset from a layer's GeoJSON and style:\n * one {@link DiagramDatum} per feature that has an anchor and at least one\n * positive value, capped at {@link MAX_DIAGRAM_FEATURES}, plus the global\n * maxima scaled sizing and bar axes need.\n *\n * @param geojson - The layer's feature collection.\n * @param style - The layer style carrying the diagram configuration.\n */\nexport function collectDiagramData(geojson: FeatureCollection, style: LayerStyle): DiagramData {\n const properties = styleValue(style, \"diagramFields\")\n .map((field) => field.property)\n .filter((property) => property !== \"\");\n const sizeMode = styleValue(style, \"diagramSizeMode\");\n const sizeProperty = styleValue(style, \"diagramSizeProperty\");\n\n const data: DiagramDatum[] = [];\n let maxSizeValue = 0;\n let maxFieldValue = 0;\n let maxTotal = 0;\n let truncated = false;\n let visited = 0;\n\n for (const feature of geojson.features) {\n if (data.length >= MAX_DIAGRAM_FEATURES || visited >= MAX_DIAGRAM_SCAN_FEATURES) {\n truncated = true;\n break;\n }\n visited += 1;\n const position = diagramAnchor(feature.geometry);\n if (!position) continue;\n const values = properties.map((property) => readValue(feature, property));\n const total = values.reduce((sum, value) => sum + value, 0);\n if (total <= 0) continue;\n // Attribute sizing with no attribute chosen yet falls back to a constant\n // (full-size diagrams) instead of collapsing everything to the floor.\n const sizeValue =\n sizeMode === \"sum\"\n ? total\n : sizeMode === \"attribute\" && sizeProperty !== \"\"\n ? readValue(feature, sizeProperty)\n : 1;\n if (sizeValue > maxSizeValue) maxSizeValue = sizeValue;\n if (total > maxTotal) maxTotal = total;\n for (const value of values) {\n if (value > maxFieldValue) maxFieldValue = value;\n }\n data.push({ position, values, total, sizeValue });\n }\n\n return { data, maxSizeValue, maxFieldValue, maxTotal, truncated };\n}\n\n/**\n * The rendered size (diameter for pie/donut, box height/width for bars) of one\n * diagram in pixels. Fixed sizing returns the configured size; scaled sizing\n * maps the datum's size value onto the configured size by square root, so\n * diagram *area* tracks the value, with a small floor to keep every drawn\n * diagram legible.\n *\n * @param datum - The feature's diagram datum.\n * @param style - The layer style carrying the diagram configuration.\n * @param maxSizeValue - The dataset's largest size value (from\n * {@link collectDiagramData}).\n */\nexport function diagramPixelSize(\n datum: DiagramDatum,\n style: LayerStyle,\n maxSizeValue: number,\n): number {\n const size = Math.max(4, styleValue(style, \"diagramSize\"));\n if (styleValue(style, \"diagramSizeMode\") === \"fixed\") return size;\n if (maxSizeValue <= 0 || datum.sizeValue <= 0) return MIN_DIAGRAM_SIZE;\n return Math.max(MIN_DIAGRAM_SIZE, size * Math.sqrt(datum.sizeValue / maxSizeValue));\n}\n","import type { MarkerShape } from \"./types\";\n\n/**\n * Trace the outline of a built-in marker {@link MarkerShape} onto a 2D canvas\n * path, centered in a `size`×`size` box. The caller supplies the fill/stroke;\n * this only builds the path (via `beginPath`/`closePath`), so it stays free of\n * any DOM other than the passed context and can be shared by the map's sprite\n * baker (`@geolibre/map`) and the Print Layout legend renderer.\n *\n * `\"custom\"` (SVG) markers have no built-in path and are handled by the caller\n * (rasterized separately); passing one draws the default circle so the marker\n * never vanishes silently.\n *\n * @param ctx - Any 2D path-drawing context (a real `CanvasRenderingContext2D`\n * satisfies this structurally).\n * @param shape - The built-in marker shape to trace.\n * @param size - The box edge length in pixels; the shape is inset slightly so a\n * stroke is not clipped at the edge.\n */\nexport function drawMarkerPath(\n ctx: CanvasRenderingContext2D,\n shape: MarkerShape,\n size: number,\n): void {\n const c = size / 2;\n // Leave a small inset so the stroke is not clipped at the tile edge.\n const r = c * 0.82;\n ctx.beginPath();\n switch (shape) {\n case \"square\":\n ctx.rect(c - r, c - r, r * 2, r * 2);\n break;\n case \"triangle\":\n ctx.moveTo(c, c - r);\n ctx.lineTo(c + r, c + r);\n ctx.lineTo(c - r, c + r);\n ctx.closePath();\n break;\n case \"diamond\":\n ctx.moveTo(c, c - r);\n ctx.lineTo(c + r, c);\n ctx.lineTo(c, c + r);\n ctx.lineTo(c - r, c);\n ctx.closePath();\n break;\n case \"star\": {\n const outer = r;\n const inner = r * 0.42;\n for (let point = 0; point < 10; point += 1) {\n const radius = point % 2 === 0 ? outer : inner;\n const angle = (Math.PI / 5) * point - Math.PI / 2;\n const x = c + radius * Math.cos(angle);\n const y = c + radius * Math.sin(angle);\n if (point === 0) ctx.moveTo(x, y);\n else ctx.lineTo(x, y);\n }\n ctx.closePath();\n break;\n }\n case \"cross\": {\n const arm = r * 0.42;\n ctx.moveTo(c - arm, c - r);\n ctx.lineTo(c + arm, c - r);\n ctx.lineTo(c + arm, c - arm);\n ctx.lineTo(c + r, c - arm);\n ctx.lineTo(c + r, c + arm);\n ctx.lineTo(c + arm, c + arm);\n ctx.lineTo(c + arm, c + r);\n ctx.lineTo(c - arm, c + r);\n ctx.lineTo(c - arm, c + arm);\n ctx.lineTo(c - r, c + arm);\n ctx.lineTo(c - r, c - arm);\n ctx.lineTo(c - arm, c - arm);\n ctx.closePath();\n break;\n }\n case \"pin\": {\n // A teardrop: a circle bowl with a point at the bottom.\n const bowlR = r * 0.7;\n const bowlY = c - r * 0.2;\n ctx.moveTo(c, c + r);\n ctx.quadraticCurveTo(c - bowlR, bowlY + bowlR * 0.4, c - bowlR, bowlY);\n ctx.arc(c, bowlY, bowlR, Math.PI, Math.PI * 2);\n ctx.quadraticCurveTo(c + bowlR, bowlY + bowlR * 0.4, c, c + r);\n ctx.closePath();\n break;\n }\n case \"circle\":\n default:\n ctx.arc(c, c, r, 0, Math.PI * 2);\n }\n}\n\n// Remote SVG sources we have already warned about, so the console message below\n// fires once per distinct URL instead of on every image regeneration.\nconst warnedRemoteSvgSources = new Set<string>();\n\n/**\n * Resolve user-supplied SVG input to an `Image.src`: inline markup (starting\n * with `<`) is encoded as a data URL; otherwise only `data:` and `http(s):`\n * URLs are accepted. Returns null for empty input or an unsupported scheme\n * (e.g. `file:`), which the caller treats as \"no image\" rather than letting an\n * arbitrary URL be loaded.\n *\n * Remote `http(s):` URLs are supported intentionally (custom marker/pattern\n * SVGs) but trigger a cross-origin request when rendered. Because a shared\n * `.geolibre.json` can carry such a URL, we log a one-time warning so the\n * outbound request is visible; prefer inline `<svg>` or `data:` in shared\n * projects.\n *\n * Lives here (rather than beside the map's sprite baker) because every surface\n * that previews a custom marker — the map, the Print Layout legend, the on-map\n * Legend panel — must resolve the same input the same way. A second, local\n * `data:image/svg+xml,…` wrapper is exactly what left URL and `data:` markers\n * blank in the on-map legend while the map drew them fine.\n *\n * @param markup - Raw SVG markup, a `data:` URL, or an `http(s)` URL.\n * @returns An `Image.src` value, or `null` when nothing loadable was given.\n */\nexport function resolveSvgSource(markup: string): string | null {\n const trimmed = markup.trim();\n if (!trimmed) return null;\n if (trimmed.startsWith(\"<\")) {\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(trimmed)}`;\n }\n if (/^https?:\\/\\//i.test(trimmed)) {\n if (!warnedRemoteSvgSources.has(trimmed)) {\n warnedRemoteSvgSources.add(trimmed);\n console.warn(\n `[geolibre] Loading a custom SVG from a remote URL triggers a ` +\n `cross-origin request: ${trimmed}. Prefer inline <svg> markup or a ` +\n `data: URL in shared projects.`,\n );\n }\n return trimmed;\n }\n if (trimmed.startsWith(\"data:\")) {\n return trimmed;\n }\n return null;\n}\n","/**\n * Return a whole-value HTTP(S) URL that is safe to expose as an attribute link.\n *\n * Attribute values may contain arbitrary user data, so only explicit web URLs\n * are accepted. Substrings in prose and schemes such as `javascript:` and\n * `file:` remain plain text.\n */\nexport function attributeLinkUrl(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed || /\\s/.test(trimmed)) return null;\n // Bidirectional controls can make an untrusted hostname appear to read as a\n // different domain even though the browser opens the original URL.\n if (/[\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069]/u.test(trimmed)) return null;\n if (!/^https?:\\/\\/[^/?#]/i.test(trimmed)) return null;\n\n try {\n const { protocol } = new URL(trimmed);\n return protocol === \"http:\" || protocol === \"https:\" ? trimmed : null;\n } catch {\n return null;\n }\n}\n","/**\n * Feature-property keys for inline photo images, shared by the app and the map\n * package so neither hardcodes a second copy that could drift. These keys are\n * part of the project schema: they are serialized into `.geolibre.json` on a\n * geotagged-photo or field-collection layer.\n */\n\n/** Property key under which a photo's downscaled thumbnail (data URL) is stored. */\nexport const PHOTO_PROPERTY = \"photo\";\n\n/**\n * Property key under which a geotagged photo's full-resolution image (a data URL\n * of the original, un-re-encoded bytes) is stored. {@link PHOTO_PROPERTY} holds\n * the small thumbnail shown on the map marker/popup; this holds the\n * native-resolution original used by the enlarged/fullscreen viewer and a \"Save\n * image\". Absent when the source is already at or below the thumbnail cap, or a\n * format a browser cannot display at native size (TIFF/HEIC).\n */\nexport const PHOTO_FULL_PROPERTY = \"photo_full\";\n","/**\n * Raster basemaps for regions the default catalog does not serve well.\n *\n * Today that means mainland China: GeoLibre's defaults (OpenFreeMap, Protomaps)\n * and almost every provider in the Basemaps control are hosted outside it with\n * no presence inside, so from there they range from slow to unreachable. These\n * entries are served from inside China and give those users a basemap that\n * loads.\n *\n * The mechanism mirrors {@link PlanetaryBasemap}: `styleUrl` is a\n * `geolibre://regional-basemap/<id>` sentinel that the map controller's\n * `resolveMapStyle` expands into a raster style at apply time (it is not a\n * fetchable URL). A separate sentinel prefix from the planetary one keeps the\n * two apart, because selecting a planetary basemap also switches the project's\n * celestial body, which must not happen here.\n */\n\n/**\n * The regions the pickers group these basemaps under. Each gets its own\n * heading and explanatory note inside the single \"Regional\" section, so a\n * future region slots in without the section itself becoming country-specific.\n */\nexport type RegionalBasemapRegionId = \"china\";\n\n/**\n * A raster basemap for a region, rendered from XYZ (or TMS) tiles.\n */\nexport interface RegionalBasemap {\n id: string;\n /** Which region heading this basemap sits under. */\n region: RegionalBasemapRegionId;\n /**\n * Display name, in the language of the region the basemap serves. These are\n * Chinese-market services whose users know them by their Chinese names, and\n * the section heading already says which region they belong to, so the labels\n * are not translated the way UI strings are.\n */\n name: string;\n /** Sentinel stored as the basemap style URL. */\n styleUrl: string;\n /** XYZ (or TMS, see {@link scheme}) tile template. */\n tileUrl: string;\n /**\n * An optional transparent overlay drawn above {@link tileUrl}, so a satellite\n * basemap can ship with its roads and labels burnt in as one selectable\n * basemap rather than something the user has to stack by hand.\n */\n overlayTileUrl?: string;\n /**\n * Tile row ordering. Tencent numbers rows from the bottom (**TMS**); Amap is\n * standard XYZ. Omit for XYZ; MapLibre defaults to `\"xyz\"` when absent.\n */\n scheme?: \"tms\";\n /** Max native zoom of the source (MapLibre overzooms beyond this). */\n maxZoom: number;\n /** Attribution shown on the map. */\n attribution: string;\n /**\n * True when the tiles are drawn in GCJ-02, the offset datum Chinese law\n * mandates for public map services. Neither GeoLibre nor MapLibre applies the\n * shift, so WGS84 data laid over these lands roughly 100 to 700 m off (see\n * docs/getting-started.md). Not every regional basemap is offset — Tianditu\n * publishes in CGCS2000 and lines up — so this records which are, rather than\n * leaving a caveat that applies to some entries implied by the region.\n */\n gcj02?: boolean;\n}\n\nexport const REGIONAL_BASEMAP_SENTINEL_PREFIX = \"geolibre://regional-basemap/\";\n\nconst AMAP_ATTRIBUTION = '&copy; <a href=\"https://www.amap.com\">高德地图 Amap</a>';\nconst TENCENT_ATTRIBUTION = '&copy; <a href=\"https://map.qq.com\">腾讯地图 Tencent Maps</a>';\n\nconst sentinel = (id: string) => `${REGIONAL_BASEMAP_SENTINEL_PREFIX}${id}`;\n\n// Amap serves each product from four numbered hosts; MapLibre takes a single\n// template per source, so pin host 01. Browsers multiplex over HTTP/2, which\n// makes the sharding a non-issue. `style` selects the product: 7 street, 6\n// imagery, 8 roads-and-labels.\nconst AMAP_STREET_URL =\n \"https://wprd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scl=1&style=7&x={x}&y={y}&z={z}\";\nconst AMAP_SATELLITE_URL = \"https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}\";\nconst AMAP_LABELS_URL = \"https://webst01.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}\";\n\nconst tencentUrl = (styleId: number) =>\n `https://rt0.map.gtimg.com/tile?z={z}&x={x}&y={y}&styleid=${styleId}&scene=0`;\n\n/**\n * Basemaps served from inside mainland China.\n *\n * Zoom limits are each source's probed maximum, so MapLibre overzooms (blurs)\n * rather than painting a placeholder: Amap's imagery returns a \"no imagery\"\n * tile rather than a 404 past zoom 18, and Tencent 400s past 19 with zoom 19\n * already blank.\n */\nexport const CHINA_BASEMAPS: readonly RegionalBasemap[] = [\n {\n id: \"amap-street\",\n region: \"china\",\n name: \"高德地图\",\n styleUrl: sentinel(\"amap-street\"),\n tileUrl: AMAP_STREET_URL,\n maxZoom: 19,\n attribution: AMAP_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"amap-satellite\",\n region: \"china\",\n name: \"高德卫星\",\n styleUrl: sentinel(\"amap-satellite\"),\n tileUrl: AMAP_SATELLITE_URL,\n maxZoom: 18,\n attribution: AMAP_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"amap-hybrid\",\n region: \"china\",\n name: \"高德混合\",\n styleUrl: sentinel(\"amap-hybrid\"),\n tileUrl: AMAP_SATELLITE_URL,\n overlayTileUrl: AMAP_LABELS_URL,\n maxZoom: 18,\n attribution: AMAP_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"tencent-street\",\n region: \"china\",\n name: \"腾讯地图\",\n styleUrl: sentinel(\"tencent-street\"),\n tileUrl: tencentUrl(1),\n scheme: \"tms\",\n maxZoom: 18,\n attribution: TENCENT_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"tencent-dark\",\n region: \"china\",\n name: \"腾讯深色\",\n styleUrl: sentinel(\"tencent-dark\"),\n tileUrl: tencentUrl(4),\n scheme: \"tms\",\n maxZoom: 18,\n attribution: TENCENT_ATTRIBUTION,\n gcj02: true,\n },\n];\n\n/** Every regional basemap, across all regions. */\nexport const REGIONAL_BASEMAPS: readonly RegionalBasemap[] = CHINA_BASEMAPS;\n\n/**\n * The regional basemaps grouped for display, one entry per region. Both the New\n * Project and Change Basemap panels render this inside a single collapsible\n * \"Regional\" section, so adding a region is a change here rather than in two\n * dialogs.\n */\nexport const REGIONAL_BASEMAP_GROUPS: readonly {\n id: RegionalBasemapRegionId;\n basemaps: readonly RegionalBasemap[];\n}[] = [{ id: \"china\", basemaps: CHINA_BASEMAPS }];\n\n/** Look up a regional basemap by its `geolibre://regional-basemap/<id>` sentinel. */\nexport function getRegionalBasemapByStyleUrl(\n styleUrl: string | undefined,\n): RegionalBasemap | undefined {\n if (!styleUrl) return undefined;\n return REGIONAL_BASEMAPS.find((basemap) => basemap.styleUrl === styleUrl);\n}\n\n/** Look up a regional basemap by id. */\nexport function getRegionalBasemapById(id: string | undefined): RegionalBasemap | undefined {\n if (!id) return undefined;\n return REGIONAL_BASEMAPS.find((basemap) => basemap.id === id);\n}\n\n/** Whether a style URL is a regional-basemap sentinel, resolvable or not. */\nexport function isRegionalBasemapSentinel(styleUrl: string | undefined): boolean {\n return Boolean(styleUrl?.startsWith(REGIONAL_BASEMAP_SENTINEL_PREFIX));\n}\n","/**\n * Maps the project's basemap onto imagery the Cesium globe can render.\n *\n * The 2D panes render `basemapStyleUrl` through MapLibre, which understands\n * vector GL styles and GeoLibre's `geolibre://` sentinels. Cesium understands\n * neither: it draws imagery from tile providers only. So a globe pane that\n * simply mounted the store basemap would show nothing, which is why the pane\n * used to hardcode Ion World Imagery (with a token) or OpenStreetMap (without)\n * and ignore the user's choice entirely — leaving a bright globe beside dark 2D\n * panes and breaking the \"same map, different projection\" promise the multi-map\n * grid makes.\n *\n * This module is the translation. It is deliberately **engine-free** — it\n * returns a plain descriptor and never imports Cesium — so it lives in\n * `@geolibre/core` beside the basemap catalogs it reads, the 2D and 3D\n * renderers share one source of truth, and the Cesium chunk boundary in\n * `vite.config.ts` stays intact.\n *\n * Three outcomes are possible, and the caller has to handle all three (see\n * {@link CesiumBasemapImagery}): tiles to draw, nothing to draw, or \"no raster\n * equivalent exists — pick your own default\".\n */\n\nimport { getPlanetaryBasemapByStyleUrl } from \"./ellipsoids\";\nimport { getRegionalBasemapByStyleUrl } from \"./regional-basemaps\";\nimport { BLANK_BASEMAP, DEFAULT_BASEMAP } from \"./types\";\n\n/** Stable project IDs and the corresponding Cesium bundled thumbnails / ion assets. */\nexport const CESIUM_BASEMAPS = [\n { id: \"project\", name: \"Project basemap\", icon: \"naturalEarthII.png\" },\n { id: \"natural-earth\", name: \"Natural Earth II\", icon: \"naturalEarthII.png\" },\n { id: \"bing-aerial\", name: \"Bing Maps Aerial\", icon: \"bingAerial.png\", assetId: 2 },\n {\n id: \"bing-labels\",\n name: \"Bing Maps Aerial with Labels\",\n icon: \"bingAerialLabels.png\",\n assetId: 3,\n },\n { id: \"bing-roads\", name: \"Bing Maps Roads\", icon: \"bingRoads.png\", assetId: 4 },\n { id: \"sentinel-2\", name: \"Sentinel-2\", icon: \"sentinel-2.png\", assetId: 3954 },\n { id: \"blue-marble\", name: \"Blue Marble\", icon: \"blueMarble.png\", assetId: 3845 },\n { id: \"earth-at-night\", name: \"Earth at night\", icon: \"earthAtNight.png\", assetId: 3812 },\n {\n id: \"google-satellite\",\n name: \"Google Maps Satellite\",\n icon: \"googleSatellite.png\",\n assetId: 3830182,\n },\n {\n id: \"google-labels\",\n name: \"Google Maps Satellite with Labels\",\n icon: \"googleSatelliteLabels.png\",\n assetId: 3830183,\n },\n { id: \"google-roads\", name: \"Google Maps Roadmap\", icon: \"googleRoadmap.png\", assetId: 3830184 },\n {\n id: \"google-contour\",\n name: \"Google Maps Contour\",\n icon: \"googleContour.png\",\n assetId: 3830186,\n },\n { id: \"azure-aerial\", name: \"Azure Maps Aerial\", icon: \"azureAerial.png\", assetId: 3891168 },\n { id: \"azure-roads\", name: \"Azure Maps Roads\", icon: \"azureRoads.png\", assetId: 3891169 },\n {\n id: \"esri-imagery\",\n name: \"ArcGIS World Imagery\",\n icon: \"ArcGisMapServiceWorldImagery.png\",\n category: \"Other\",\n service: \"World_Imagery\",\n },\n {\n id: \"esri-hillshade\",\n name: \"ArcGIS World Hillshade\",\n icon: \"ArcGisMapServiceWorldHillshade.png\",\n category: \"Other\",\n service: \"Elevation/World_Hillshade\",\n },\n {\n id: \"esri-ocean\",\n name: \"Esri World Ocean\",\n icon: \"ArcGisMapServiceWorldOcean.png\",\n category: \"Other\",\n service: \"Ocean/World_Ocean_Base\",\n },\n { id: \"osm\", name: \"OpenStreetMap\", icon: \"openStreetMap.png\", category: \"Other\" },\n] as const;\n\nexport type CesiumBasemapId = (typeof CESIUM_BASEMAPS)[number][\"id\"];\n\nexport function normalizeCesiumBasemap(value: unknown): CesiumBasemapId {\n return CESIUM_BASEMAPS.find((entry) => entry.id === value)?.id ?? \"project\";\n}\n\n/**\n * Missing credentials fall back to keyless imagery without changing the saved\n * choice.\n *\n * The fallback is ArcGIS World Imagery rather than the project background: a\n * globe wants to look like the Earth, and the project's own basemap is usually\n * a vector street style whose raster analogue is mostly empty ocean from\n * orbit. It needs no key, so the globe looks the same with or without an Ion\n * token — only terrain and the Ion-only basemaps are lost.\n */\nexport function availableCesiumBasemap(value: unknown, hasIonToken: boolean): CesiumBasemapId {\n const keyless = \"esri-imagery\";\n const requested = value ?? (hasIonToken ? \"bing-aerial\" : keyless);\n const entry = CESIUM_BASEMAPS.find((entry) => entry.id === requested);\n return entry && (!(\"assetId\" in entry) || hasIonToken) ? entry.id : keyless;\n}\n\n/**\n * What the globe should draw underneath the project's data layers.\n *\n * - `xyz` — a tile template to render, with the credit and zoom bounds that go\n * with it.\n * - `none` — the user chose the blank basemap; draw a bare ellipsoid.\n * - `default` — this basemap has no raster equivalent (a custom style URL, a\n * provider style, an offline archive). The renderer falls back to whatever it\n * can offer, which is Ion World Imagery when a token is configured and\n * OpenStreetMap otherwise.\n */\nexport type CesiumBasemapImagery =\n | { kind: \"none\" }\n | { kind: \"default\" }\n | { kind: \"ion\"; assetId: number }\n | { kind: \"natural-earth\" }\n | { kind: \"arcgis\"; url: string }\n | {\n kind: \"xyz\";\n /** Tile template with `{z}`/`{x}`/`{y}` placeholders. */\n template: string;\n /** Credit to show on the globe, as the HTML the 2D map already uses. */\n attribution: string;\n /** Max native zoom of the source, so the globe overzooms rather than 404s. */\n maximumLevel?: number;\n /**\n * Tile row ordering. `\"tms\"` numbers rows from the bottom; absent means\n * standard XYZ. Cesium has no `scheme` option, so a TMS template needs\n * `{y}` swapped for its `{reverseY}` placeholder at render time.\n */\n scheme?: \"tms\";\n /**\n * A transparent overlay drawn directly above {@link template} — the\n * roads-and-labels tiles a hybrid basemap (Amap Hybrid) bakes in, so one\n * basemap selection still yields a labelled satellite globe.\n */\n overlayTemplate?: string;\n };\n\n/**\n * A keyless raster basemap standing in for a vector style of the same tone.\n *\n * \"Keyless\" is the binding constraint, and it rules out most of the obvious\n * candidates: CARTO's raster endpoints (which the 2D basemap control still\n * lists) now stamp every tile with an \"API KEY REQUIRED\" watermark, and\n * Stadia/Stamen and Thunderforest have required keys for years. What is left\n * and still permissively licensed is OpenStreetMap's own tiles and EOX Maps'\n * WMTS layers (CC BY-SA), which between them cover the three tones GeoLibre's\n * vector basemaps span.\n */\ninterface RasterAnalogue {\n template: string;\n attribution: string;\n /** Max native zoom of the source; the globe overzooms past it rather than 404s. */\n maximumLevel: number;\n}\n\nconst OSM_CREDIT =\n '© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors';\n/** EOX Maps serves these layers keyless under CC BY-SA 4.0. */\nconst EOX_CREDIT = '<a href=\"https://maps.eox.at\">EOX Maps</a> (CC BY-SA 4.0)';\n\nconst eoxLayer = (layer: string, extension: string): string =>\n `https://tiles.maps.eox.at/wmts/1.0.0/${layer}/default/g/{z}/{y}/{x}.${extension}`;\n\n/** Colourful streets — the tone of Liberty and Bright. */\nconst STREETS: RasterAnalogue = {\n template: \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\",\n attribution: OSM_CREDIT,\n maximumLevel: 19,\n};\n\n/** Light and muted — the tone of Positron and the pale Protomaps flavors. */\nconst LIGHT: RasterAnalogue = {\n template: eoxLayer(\"terrain-light_3857\", \"jpg\"),\n attribution: `Terrain Light · ${OSM_CREDIT} · ${EOX_CREDIT}`,\n maximumLevel: 16,\n};\n\n/**\n * Dark — the tone of Dark, Fiord, and the dark Protomaps flavors. NASA's Black\n * Marble night lights is the one keyless dark raster basemap left, and it suits\n * a globe: the point of matching here is that a dark project does not end up\n * with a glaring bright globe in the corner.\n */\nconst DARK: RasterAnalogue = {\n template: eoxLayer(\"blackmarble_3857\", \"jpg\"),\n attribution: `Black Marble © NASA Earth Observatory · ${EOX_CREDIT}`,\n // A low-resolution global product; past this the globe blurs rather than 404s.\n maximumLevel: 8,\n};\n\n/** Raster analogue per OpenFreeMap style name (the last path segment). */\nconst OPENFREEMAP_ANALOGUES: Record<string, RasterAnalogue> = {\n liberty: STREETS,\n bright: STREETS,\n positron: LIGHT,\n dark: DARK,\n fiord: DARK,\n};\n\n/** Raster analogue per Protomaps v5 flavor. */\nconst PROTOMAPS_ANALOGUES: Record<string, RasterAnalogue> = {\n light: LIGHT,\n white: LIGHT,\n grayscale: LIGHT,\n dark: DARK,\n black: DARK,\n};\n\nfunction toImagery(analogue: RasterAnalogue): CesiumBasemapImagery {\n return {\n kind: \"xyz\",\n template: analogue.template,\n attribution: analogue.attribution,\n maximumLevel: analogue.maximumLevel,\n };\n}\n\n/**\n * Whether two descriptors would draw the same thing.\n *\n * Descriptors are built fresh on every call, so two basemaps resolving to the\n * same analogue — OpenFreeMap `liberty` and `bright` are both the streets\n * tone — yield equal but distinct objects. A renderer that remembers the\n * descriptor it drew (to skip redundant work) must compare by value: a\n * reference check would miss that case and tear down and re-add an identical\n * tile provider, costing a flash and a round of re-requested tiles for no\n * visible change.\n */\nexport function sameCesiumImagery(a: CesiumBasemapImagery, b: CesiumBasemapImagery): boolean {\n if (a.kind !== b.kind) return false;\n if (a.kind === \"arcgis\" && b.kind === \"arcgis\") return a.url === b.url;\n if (a.kind === \"ion\" && b.kind === \"ion\") return a.assetId === b.assetId;\n // `none` and `default` carry no fields, so matching kinds is the whole test.\n if (a.kind !== \"xyz\" || b.kind !== \"xyz\") return true;\n return (\n a.template === b.template &&\n a.attribution === b.attribution &&\n a.maximumLevel === b.maximumLevel &&\n a.scheme === b.scheme &&\n a.overlayTemplate === b.overlayTemplate\n );\n}\n\n/**\n * `decodeURIComponent` that reports failure instead of throwing. A URL keeps a\n * malformed escape (`%E0%A4`) in its pathname quite happily, but decoding one\n * throws `URIError` — and this runs inside a React render, so an unhandled throw\n * would take the globe pane down instead of falling through to the default.\n */\nfunction safeDecode(value: string): string | undefined {\n try {\n return decodeURIComponent(value);\n } catch {\n return undefined;\n }\n}\n\n/**\n * The raster analogue for a vector GL style URL, or undefined when the style is\n * not one GeoLibre ships (a provider style, a user's own URL, a Mapbox style).\n * Matching on host + path rather than searching the URL for \"dark\"/\"light\"\n * keeps an unrelated style whose *key* happens to contain one of those words\n * from being mapped to the wrong tone.\n */\nfunction vectorStyleAnalogue(styleUrl: string): RasterAnalogue | undefined {\n let parsed: URL;\n try {\n parsed = new URL(styleUrl);\n } catch {\n return undefined;\n }\n if (parsed.hostname === \"tiles.openfreemap.org\") {\n // https://tiles.openfreemap.org/styles/<name>\n const name = parsed.pathname.split(\"/\").filter(Boolean).at(-1);\n return name ? OPENFREEMAP_ANALOGUES[name] : undefined;\n }\n if (parsed.hostname === \"api.protomaps.com\") {\n // https://api.protomaps.com/styles/v5/<flavor>/<lang>.json?key=…\n const segments = parsed.pathname.split(\"/\").filter(Boolean);\n const flavor = segments[0] === \"styles\" ? segments[2] : undefined;\n const decoded = flavor ? safeDecode(flavor) : undefined;\n return decoded ? PROTOMAPS_ANALOGUES[decoded] : undefined;\n }\n return undefined;\n}\n\n/**\n * Translate a basemap style URL into imagery for the Cesium globe.\n *\n * Planetary and regional basemaps are already raster tile sets, so they carry\n * straight over — the globe shows the same Mars/Moon/Amap tiles the 2D panes\n * do. GeoLibre's own vector styles map to a keyless raster basemap of matching\n * tone, so switching the project to a dark basemap darkens the globe too.\n * Everything else reports `default`, leaving the choice of fallback imagery to\n * the renderer (which knows whether an Ion token is configured).\n *\n * A `geolibre://` sentinel that no longer resolves is treated as the default\n * basemap, mirroring what `resolveMapStyle` does for the 2D map rather than\n * handing the sentinel onward as if it were fetchable.\n *\n * @param styleUrl - The project's `basemapStyleUrl`.\n * @returns What the globe should draw beneath the data layers.\n */\nexport function basemapToCesiumImagery(\n styleUrl: string | undefined,\n cesiumBasemap: CesiumBasemapId = \"project\",\n): CesiumBasemapImagery {\n const entry = CESIUM_BASEMAPS.find((entry) => entry.id === cesiumBasemap);\n if (entry && \"service\" in entry) {\n return {\n kind: \"arcgis\",\n url: `https://services.arcgisonline.com/ArcGIS/rest/services/${entry.service}/MapServer`,\n };\n }\n if (entry && \"assetId\" in entry) return { kind: \"ion\", assetId: entry.assetId };\n if (cesiumBasemap === \"natural-earth\") return { kind: \"natural-earth\" };\n if (cesiumBasemap === \"osm\") return toImagery(STREETS);\n if (styleUrl === BLANK_BASEMAP) return { kind: \"none\" };\n const url = styleUrl ?? DEFAULT_BASEMAP;\n\n const planetary = getPlanetaryBasemapByStyleUrl(url);\n if (planetary) {\n return {\n kind: \"xyz\",\n template: planetary.tileUrl,\n attribution: planetary.attribution,\n maximumLevel: planetary.maxZoom,\n ...(planetary.scheme ? { scheme: planetary.scheme } : {}),\n };\n }\n\n const regional = getRegionalBasemapByStyleUrl(url);\n if (regional) {\n return {\n kind: \"xyz\",\n template: regional.tileUrl,\n attribution: regional.attribution,\n maximumLevel: regional.maxZoom,\n ...(regional.scheme ? { scheme: regional.scheme } : {}),\n ...(regional.overlayTileUrl ? { overlayTemplate: regional.overlayTileUrl } : {}),\n };\n }\n\n // Every remaining sentinel kind — an offline PMTiles archive, a planetary or\n // regional id that has since been renamed — has no raster form to show, so\n // fall back the way the 2D map does instead of leaving the globe blank.\n // Reading the fallback back out of DEFAULT_BASEMAP keeps the two in step if\n // the default ever changes.\n const analogue = vectorStyleAnalogue(url.startsWith(\"geolibre://\") ? DEFAULT_BASEMAP : url);\n return analogue ? toImagery(analogue) : { kind: \"default\" };\n}\n","/**\n * Color-ramp definitions and classification-break helpers shared by the\n * vector graduated-styling panel and the raster symbology panel. Kept in\n * `@geolibre/core` (dependency-light, map-agnostic) so both UIs derive class\n * colors and breaks from the same source. The ramp `value`s intentionally\n * match the named colormaps shipped by `@developmentseed/deck.gl-raster`\n * (`COLORMAP_INDEX` keys) so a ramp choice maps 1:1 onto the raster render\n * path.\n */\n\nexport type ColorRamp = {\n value: string;\n label: string;\n colors: readonly string[];\n};\n\n/** The built-in color ramps offered by the style panels. */\nexport const VECTOR_COLOR_RAMPS: readonly ColorRamp[] = [\n {\n value: \"viridis\",\n label: \"Viridis\",\n colors: [\"#440154\", \"#31688e\", \"#35b779\", \"#fde725\"],\n },\n {\n value: \"plasma\",\n label: \"Plasma\",\n colors: [\"#0d0887\", \"#9c179e\", \"#ed7953\", \"#f0f921\"],\n },\n {\n value: \"inferno\",\n label: \"Inferno\",\n colors: [\"#000004\", \"#781c6d\", \"#ed6925\", \"#fcffa4\"],\n },\n {\n value: \"magma\",\n label: \"Magma\",\n colors: [\"#000004\", \"#721f81\", \"#f1605d\", \"#fcfdbf\"],\n },\n {\n value: \"cividis\",\n label: \"Cividis\",\n colors: [\"#00204d\", \"#575d6d\", \"#a59c74\", \"#ffea46\"],\n },\n {\n value: \"turbo\",\n label: \"Turbo\",\n colors: [\"#30123b\", \"#4777ef\", \"#1ccfd0\", \"#b9e642\", \"#fb8022\", \"#7a0403\"],\n },\n {\n value: \"spectral\",\n label: \"Spectral\",\n colors: [\"#9e0142\", \"#f46d43\", \"#ffffbf\", \"#66c2a5\", \"#5e4fa2\"],\n },\n {\n value: \"blues\",\n label: \"Blues\",\n colors: [\"#eff6ff\", \"#93c5fd\", \"#2563eb\", \"#1e3a8a\"],\n },\n {\n value: \"greens\",\n label: \"Greens\",\n colors: [\"#f0fdf4\", \"#86efac\", \"#16a34a\", \"#14532d\"],\n },\n {\n value: \"oranges\",\n label: \"Oranges\",\n colors: [\"#fff7ed\", \"#fdba74\", \"#f97316\", \"#7c2d12\"],\n },\n {\n value: \"reds\",\n label: \"Reds\",\n colors: [\"#fff5f0\", \"#fcae91\", \"#fb6a4a\", \"#cb181d\", \"#67000d\"],\n },\n {\n value: \"purples\",\n label: \"Purples\",\n colors: [\"#fcfbfd\", \"#bcbddc\", \"#807dba\", \"#54278f\", \"#3f007d\"],\n },\n {\n value: \"terrain\",\n label: \"Terrain\",\n colors: [\"#333399\", \"#21bcb3\", \"#79d05a\", \"#e8e85a\", \"#a87b54\", \"#ffffff\"],\n },\n {\n value: \"rdylgn\",\n label: \"Red-Yellow-Green\",\n colors: [\"#a50026\", \"#f46d43\", \"#ffffbf\", \"#66bd63\", \"#006837\"],\n },\n {\n value: \"rdylbu\",\n label: \"Red-Yellow-Blue\",\n colors: [\"#a50026\", \"#f46d43\", \"#ffffbf\", \"#74add1\", \"#313695\"],\n },\n {\n value: \"rdbu\",\n label: \"Red-Blue\",\n colors: [\"#b2182b\", \"#ef8a62\", \"#f7f7f7\", \"#67a9cf\", \"#2166ac\"],\n },\n {\n value: \"coolwarm\",\n label: \"Cool-Warm\",\n colors: [\"#3b4cc0\", \"#7b9ff9\", \"#dddcdc\", \"#f49a7b\", \"#b40426\"],\n },\n {\n value: \"jet\",\n label: \"Jet\",\n colors: [\"#000080\", \"#0000ff\", \"#00ffff\", \"#ffff00\", \"#ff0000\", \"#800000\"],\n },\n {\n value: \"greys\",\n label: \"Greys\",\n colors: [\"#ffffff\", \"#bdbdbd\", \"#636363\", \"#000000\"],\n },\n {\n value: \"gray\",\n label: \"Grayscale\",\n colors: [\"#000000\", \"#ffffff\"],\n },\n] as const;\n\n/**\n * Resolves a ramp definition by name, falling back to the first ramp when the\n * name is unknown.\n *\n * @param value - The ramp `value` (e.g. \"viridis\").\n * @returns The matching ramp, or the first ramp.\n */\nexport function getVectorColorRamp(value: string): ColorRamp {\n return VECTOR_COLOR_RAMPS.find((colorRamp) => colorRamp.value === value) ?? VECTOR_COLOR_RAMPS[0];\n}\n\n/**\n * Samples a ramp into `count` evenly spaced hex colors by linearly\n * interpolating between the ramp's anchor colors.\n *\n * @param colorRamp - The ramp `value`.\n * @param count - Number of colors to produce.\n * @returns An array of `count` hex colors (a single end color when count <= 1).\n */\nexport function interpolateRampColors(colorRamp: string, count: number): string[] {\n return interpolateColors(getVectorColorRamp(colorRamp).colors, count);\n}\n\n/**\n * Samples an explicit list of anchor colors into `count` evenly spaced hex\n * colors by linear interpolation. Backs both the named ramps and user-defined\n * custom ramps, so a list of hex codes is sampled identically to a built-in.\n *\n * @param colors - The anchor colors (at least one).\n * @param count - Number of colors to produce.\n * @returns An array of `count` hex colors (a single end color when count <= 1).\n */\nexport function interpolateColors(colors: readonly string[], count: number): string[] {\n const anchors = colors.length > 0 ? colors : [\"#000000\"];\n if (count <= 0) return [];\n if (count === 1) return [anchors[anchors.length - 1]];\n if (anchors.length === 1) {\n return Array.from({ length: count }, () => anchors[0]);\n }\n return Array.from({ length: count }, (_, index) => {\n const scaled = (index / (count - 1)) * (anchors.length - 1);\n const lowerIndex = Math.floor(scaled);\n const upperIndex = Math.min(anchors.length - 1, Math.ceil(scaled));\n const ratio = scaled - lowerIndex;\n return interpolateHexColor(anchors[lowerIndex], anchors[upperIndex], ratio);\n });\n}\n\n/**\n * Normalizes a single user-entered color token into a canonical `#rrggbb`\n * lowercase hex string, or null when it is not a valid 3- or 6-digit hex\n * color. Accepts values with or without a leading `#` and expands shorthand\n * (`#abc` → `#aabbcc`).\n *\n * @param token - A raw color token (e.g. \"FF0000\", \"#f00\", \" #AaBbCc \").\n * @returns The canonical `#rrggbb` color, or null when invalid.\n */\nexport function normalizeHexColor(token: string): string | null {\n let value = token.trim().toLowerCase();\n if (value === \"\") return null;\n if (!value.startsWith(\"#\")) value = `#${value}`;\n if (/^#[0-9a-f]{3}$/.test(value)) {\n value = `#${value\n .slice(1)\n .split(\"\")\n .map((channel) => channel + channel)\n .join(\"\")}`;\n }\n return /^#[0-9a-f]{6}$/.test(value) ? value : null;\n}\n\n/**\n * Parses a free-text list of hex color codes into canonical `#rrggbb` colors,\n * for the custom color-ramp inputs. Tokens may be separated by commas,\n * semicolons, or any whitespace (including newlines); invalid tokens are\n * dropped so a partial paste still yields the colors it can.\n *\n * @param input - The raw text the user entered.\n * @returns The valid, normalized colors in input order.\n */\nexport function parseHexColorList(input: string): string[] {\n return input\n .split(/[\\s,;]+/)\n .map(normalizeHexColor)\n .filter((color): color is string => color !== null);\n}\n\n/**\n * Linearly interpolates between two hex colors.\n *\n * @param from - Start hex color (e.g. \"#440154\").\n * @param to - End hex color.\n * @param ratio - Blend factor in [0, 1].\n * @returns The interpolated hex color.\n */\nexport function interpolateHexColor(from: string, to: string, ratio: number): string {\n const start = parseHexColor(from);\n const end = parseHexColor(to);\n return rgbToHex({\n r: Math.round(start.r + (end.r - start.r) * ratio),\n g: Math.round(start.g + (end.g - start.g) * ratio),\n b: Math.round(start.b + (end.b - start.b) * ratio),\n });\n}\n\n/**\n * Parses a `#rrggbb` hex color into its RGB channels.\n *\n * @param value - A `#rrggbb` hex color.\n * @returns The red, green, and blue channels (0-255).\n */\nexport function parseHexColor(value: string): { b: number; g: number; r: number } {\n // Caller must pass a well-formed \"#rrggbb\" string; malformed input parses to\n // NaN and the bitwise ops below coerce it to black ({ r: 0, g: 0, b: 0 }).\n const numeric = Number.parseInt(value.slice(1), 16);\n return {\n r: (numeric >> 16) & 255,\n g: (numeric >> 8) & 255,\n b: numeric & 255,\n };\n}\n\n/**\n * Formats RGB channels as a `#rrggbb` hex color.\n *\n * @param color - The red, green, and blue channels (0-255).\n * @returns A `#rrggbb` hex color.\n */\nexport function rgbToHex(color: { b: number; g: number; r: number }): string {\n return `#${[color.r, color.g, color.b]\n .map((channel) => channel.toString(16).padStart(2, \"0\"))\n .join(\"\")}`;\n}\n\n/**\n * Builds `count` evenly spaced break values across [min, max].\n *\n * @param min - The minimum value.\n * @param max - The maximum value.\n * @param count - Number of breaks to produce.\n * @returns The break values (min..max inclusive when count > 1).\n */\nexport function createEqualIntervalBreaks(min: number, max: number, count: number): number[] {\n return Array.from({ length: count }, (_, index) => {\n const ratio = count === 1 ? 0 : index / (count - 1);\n return min + (max - min) * ratio;\n });\n}\n\n/**\n * Builds `count` quantile break values from a sample of numeric values.\n *\n * These are ramp *edges* spanning the whole sample (the first break is the\n * minimum, the last the maximum), matching {@link createEqualIntervalBreaks}.\n * Graduated vector classes want class *lower bounds* instead; use\n * {@link createGraduatedClassBreaks}.\n *\n * @param values - The sample values.\n * @param count - Number of breaks to produce.\n * @returns The quantile break values.\n */\nexport function createQuantileBreaks(values: number[], count: number): number[] {\n if (count <= 0) return [];\n const sorted = [...values].sort((a, b) => a - b);\n // An empty sample would read `undefined` through the index math below and\n // yield NaN breaks; callers that have no values get an empty result instead.\n if (sorted.length === 0) return [];\n return Array.from({ length: count }, (_, index) => {\n const position = count === 1 ? 0 : (index / (count - 1)) * (sorted.length - 1);\n return quantileAtPosition(sorted, position);\n });\n}\n\n/** Linearly interpolated order statistic at a fractional index into `sorted`. */\nfunction quantileAtPosition(sorted: number[], position: number): number {\n const lowerIndex = Math.floor(position);\n const upperIndex = Math.min(sorted.length - 1, Math.ceil(position));\n const ratio = position - lowerIndex;\n return sorted[lowerIndex] + (sorted[upperIndex] - sorted[lowerIndex]) * ratio;\n}\n\n/** The classification schemes the graduated vector renderer offers. */\nexport type GraduatedClassificationScheme = \"equal-interval\" | \"quantile\" | \"natural-breaks\";\n\n/**\n * Builds the class *lower bounds* for a graduated vector renderer.\n *\n * A graduated renderer's stops are lower bounds, not ramp edges: with N stops\n * sorted ascending, class `i` covers `[stops[i], stops[i + 1])` and the last\n * class is open-ended above. The legend (`≥ value`) and the QML/SLD exporters\n * already read them that way, so the breaks must follow the same convention:\n * `count` classes yield `count` breaks whose first entry is the sample minimum\n * and whose last entry opens the top class. Producing edges instead (ending at\n * the maximum) leaves the top class holding only the single largest feature and\n * puts the interior breaks at the wrong percentiles.\n *\n * The result is strictly ascending: duplicate breaks (a skewed sample can push\n * several quantiles onto the same value) are collapsed, so the caller may get\n * fewer than `count` breaks and must size its colors off `breaks.length`.\n * MapLibre rejects a `step` expression whose inputs are not strictly ascending,\n * so this de-duplication is load-bearing, not cosmetic.\n *\n * @param values - The finite numeric values of the classified property.\n * @param count - Number of classes requested.\n * @param scheme - The classification scheme.\n * @returns Up to `count` strictly ascending class lower bounds.\n */\nexport function createGraduatedClassBreaks(\n values: number[],\n count: number,\n scheme: GraduatedClassificationScheme,\n): number[] {\n if (count <= 0 || values.length === 0) return [];\n if (scheme === \"natural-breaks\") return ascendingUnique(naturalClassBreaks(values, count));\n const sorted = [...values].sort((a, b) => a - b);\n if (scheme === \"quantile\") {\n return ascendingUnique(\n Array.from({ length: count }, (_, index) =>\n quantileAtPosition(sorted, (index / count) * (sorted.length - 1)),\n ),\n );\n }\n const min = sorted[0];\n const max = sorted[sorted.length - 1];\n return ascendingUnique(\n Array.from({ length: count }, (_, index) => min + ((max - min) * index) / count),\n );\n}\n\n/** Drops non-finite breaks and collapses repeats, keeping ascending order. */\nfunction ascendingUnique(breaks: number[]): number[] {\n const out: number[] = [];\n for (const value of breaks) {\n if (!Number.isFinite(value)) continue;\n if (out.length > 0 && value <= out[out.length - 1]) continue;\n out.push(value);\n }\n return out;\n}\n\n/** Cap on the Jenks input size; the DP below is roughly O(n^2 * k). */\nconst MAX_NATURAL_BREAK_SAMPLES = 1000;\n\n/** Evenly thins a sorted array down to at most `maxSamples` entries. */\nfunction downsampleSortedValues(values: number[], maxSamples: number): number[] {\n if (values.length <= maxSamples) return values;\n const result: number[] = [];\n const step = (values.length - 1) / (maxSamples - 1);\n for (let index = 0; index < maxSamples; index += 1) {\n result.push(values[Math.round(index * step)]);\n }\n return result;\n}\n\n/**\n * Jenks natural breaks: the class lower bounds that minimize the summed\n * within-class variance. Returns the sample minimum first, then the first value\n * of each subsequent class.\n */\nfunction naturalClassBreaks(values: number[], count: number): number[] {\n const unique = Array.from(new Set(values)).sort((a, b) => a - b);\n // Cap the input so a large layer does not freeze the Style panel's UI thread.\n const sorted = downsampleSortedValues(unique, MAX_NATURAL_BREAK_SAMPLES);\n if (sorted.length <= count) return sorted;\n\n const lowerClassLimits = Array.from({ length: sorted.length + 1 }, () =>\n Array(count + 1).fill(0),\n );\n const varianceCombinations = Array.from({ length: sorted.length + 1 }, () =>\n Array(count + 1).fill(Number.POSITIVE_INFINITY),\n );\n\n for (let classIndex = 1; classIndex <= count; classIndex += 1) {\n lowerClassLimits[1][classIndex] = 1;\n varianceCombinations[1][classIndex] = 0;\n }\n\n for (let valueIndex = 2; valueIndex <= sorted.length; valueIndex += 1) {\n let sum = 0;\n let sumSquares = 0;\n let weight = 0;\n\n for (let lowerIndex = 1; lowerIndex <= valueIndex; lowerIndex += 1) {\n const currentIndex = valueIndex - lowerIndex + 1;\n const value = sorted[currentIndex - 1];\n weight += 1;\n sum += value;\n sumSquares += value * value;\n const variance = sumSquares - (sum * sum) / weight;\n const previousIndex = currentIndex - 1;\n if (previousIndex === 0) continue;\n\n for (let classIndex = 2; classIndex <= count; classIndex += 1) {\n const candidate = variance + varianceCombinations[previousIndex][classIndex - 1];\n if (varianceCombinations[valueIndex][classIndex] >= candidate) {\n lowerClassLimits[valueIndex][classIndex] = currentIndex;\n varianceCombinations[valueIndex][classIndex] = candidate;\n }\n }\n }\n\n lowerClassLimits[valueIndex][1] = 1;\n varianceCombinations[valueIndex][1] = sumSquares - (sum * sum) / Math.max(1, weight);\n }\n\n // Walk the DP table back from the top class: `lowerClassLimits[i][k]` is the\n // 1-based index in `sorted` where class `k` starts, which is exactly that\n // class's lower bound. Class 1 always starts at the sample minimum.\n const breaks = Array(count).fill(sorted[0]) as number[];\n let valueIndex = sorted.length;\n for (let classIndex = count; classIndex >= 2; classIndex -= 1) {\n const lowerClassLimit = Math.max(1, lowerClassLimits[valueIndex][classIndex]);\n breaks[classIndex - 1] = sorted[lowerClassLimit - 1];\n valueIndex = lowerClassLimit - 1;\n }\n breaks[0] = sorted[0];\n return breaks;\n}\n","// Matches a `..` directory-traversal segment (bounded by separators or the\n// string ends), without rejecting a `..` inside a filename like `v1..2.gpkg`.\nconst PATH_TRAVERSAL = /(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/;\n\n/**\n * Whether a path contains a `..` directory-traversal segment. Used to reject a\n * (possibly hand-edited) project's `sourcePath` before re-reading it off disk.\n *\n * @param path - The path to check.\n * @returns True when the path contains a traversal segment.\n */\nexport function hasPathTraversal(path: string): boolean {\n return PATH_TRAVERSAL.test(path);\n}\n\n/**\n * Whether a path is an absolute local filesystem path a host could read.\n *\n * Matches the raw path (not a trimmed copy): a whitespace-padded value would\n * pass a trimmed check but reach the file read unchanged and fail there, so it\n * is rejected up front. Accepts POSIX paths and Windows drive-letter paths only.\n * UNC paths (`\\\\server\\share`) are deliberately rejected: reading one can make\n * Windows auto-authenticate against a remote host (NTLM hash capture), and a\n * remote share is not a supported local data source.\n *\n * Lives in core so validation of a persisted path is available wherever a\n * record is normalized — the desktop app's `isAbsoluteLocalPath` re-exports the\n * same rule for its Tauri call sites.\n *\n * @param path - The path to check.\n * @returns True when the path is an absolute POSIX or Windows path.\n */\nexport function isAbsoluteFilesystemPath(path: string): boolean {\n return path.startsWith(\"/\") || /^[a-z]:[\\\\/]/i.test(path);\n}\n","/**\n * Resolves runtime environment variables shared by the external-service clients\n * (geocoding, routing). Allowlisted build-time vars (`__GEOLIBRE_BUILD_ENV__`,\n * injected by `vite.config.ts`) are overlaid\n * with project-supplied runtime vars (`window.__GEOLIBRE_RUNTIME_ENV__`, set\n * from project preferences) so a self-hosted endpoint can be configured without\n * a rebuild. Carries no React/MapLibre dependency so callers stay unit-testable.\n */\n\n// Injected by `vite.config.ts`'s `define` from an explicit allowlist\n// (BUILD_ENV_KEYS), with credential-bearing names withheld from redistributable\n// builds such as the Jupyter wheel.\n//\n// This deliberately does NOT read `import.meta.env`. That read is a whole-object\n// one — nothing here names a static key — so Vite could not replace it per-key\n// and instead inlined the entire env record into every chunk importing this\n// module. Combined with the bare-to-prefixed shell bridge in `vite.config.ts`,\n// every chunk importing this module would carry whatever the build machine had\n// set. Keep this indirection: reverting to `import.meta.env` silently restores\n// that behaviour, and no test would catch it.\ndeclare const __GEOLIBRE_BUILD_ENV__: Record<string, string> | undefined;\n\nconst buildEnv: Record<string, string | undefined> =\n typeof __GEOLIBRE_BUILD_ENV__ === \"undefined\" ? {} : __GEOLIBRE_BUILD_ENV__;\n\n/**\n * The allowlisted build-time environment, with no runtime overlay.\n *\n * For settings that must be fixed by the build and NOT overridable by a\n * `.geolibre.json` a user opened — the auth gate, the deployment profile, the\n * embed origin allowlist. Routing those through {@link getRuntimeEnvironment}\n * would let project-supplied `__GEOLIBRE_RUNTIME_ENV__` values relax them.\n *\n * Callers should prefer this over reading `import.meta.env` directly: a\n * whole-object read defeats Vite's per-key replacement and inlines every\n * `VITE_` var on the build machine into the chunk.\n *\n * @returns The build-time environment record.\n */\nexport function getBuildEnvironment(): Record<string, string | undefined> {\n return buildEnv;\n}\n\n/**\n * Merges build-time env with project runtime env (the latter wins). Falls back\n * to build-time env alone outside a browser (e.g. in tests).\n *\n * @returns The resolved environment variables.\n */\nexport function getRuntimeEnvironment(): Record<string, string | undefined> {\n if (typeof window === \"undefined\") return buildEnv;\n\n // __GEOLIBRE_RUNTIME_ENV__ is declared globally in ./types.\n return {\n ...buildEnv,\n ...(window.__GEOLIBRE_RUNTIME_ENV__ ?? {}),\n };\n}\n\n/**\n * Resolves a local DuckDB spatial extension path from the runtime environment.\n *\n * When `VITE_DUCKDB_SPATIAL_EXTENSION_PATH` is set, DuckDB consumers (the\n * desktop app's own loader and the Add Vector panel's maplibre-gl-vector\n * control) load the spatial extension from this path with `LOAD '<path>'`\n * instead of installing it from the remote repository, which hangs in\n * sandboxed or firewalled environments. Lives in `@geolibre/core` so every\n * consumer shares one implementation.\n *\n * @param env - Environment record (defaults to the runtime environment);\n * injectable for testing.\n * @returns The trimmed extension path, or undefined when unset.\n */\nexport function getSpatialExtensionPath(\n env?: Record<string, string | undefined>,\n): string | undefined {\n const runtimeEnv = env ?? getRuntimeEnvironment();\n const trimmed = runtimeEnv.VITE_DUCKDB_SPATIAL_EXTENSION_PATH?.trim();\n return trimmed || undefined;\n}\n\n/**\n * Resolves the Protomaps API key from the runtime environment.\n *\n * Protomaps' hosted styles require an API key embedded in the style URL. The\n * key is supplied via `VITE_PROTOMAPS_API_KEY` (baked in at build time for the\n * web demo; see the deploy workflow). When unset, the Protomaps basemaps are\n * unavailable and should be hidden from the UI.\n *\n * @param env - Environment record (defaults to the runtime environment);\n * injectable for testing.\n * @returns The trimmed API key, or undefined when unset.\n */\nexport function getProtomapsApiKey(env?: Record<string, string | undefined>): string | undefined {\n const runtimeEnv = env ?? getRuntimeEnvironment();\n const trimmed = runtimeEnv.VITE_PROTOMAPS_API_KEY?.trim();\n return trimmed || undefined;\n}\n\n/**\n * Resolves the Google Maps API key from the runtime environment.\n *\n * GeoLibre's browser-facing builds normally use `VITE_GOOGLE_MAPS_API_KEY`.\n * The bare `GOOGLE_MAPS_API_KEY` fallback is reached two ways: (1) the desktop\n * Vite config copies it to `VITE_GOOGLE_MAPS_API_KEY` at build time for local\n * shell testing (`vite.config.ts`'s `envPrefix` does not include the bare\n * name), and (2) a project's own runtime environment variables\n * (`window.__GEOLIBRE_RUNTIME_ENV__`), which are not subject to Vite's\n * envPrefix allowlist at all.\n *\n * @param env - Environment record (defaults to the runtime environment);\n * injectable for testing.\n * @returns The trimmed API key, or undefined when unset.\n */\nexport function getGoogleMapsApiKey(env?: Record<string, string | undefined>): string | undefined {\n const runtimeEnv = env ?? getRuntimeEnvironment();\n const trimmed =\n runtimeEnv.VITE_GOOGLE_MAPS_API_KEY?.trim() || runtimeEnv.GOOGLE_MAPS_API_KEY?.trim();\n return trimmed || undefined;\n}\n\n/**\n * Resolves the Mapbox access token from the runtime environment.\n *\n * The basemap control's Mapbox styles authenticate with the user's own token.\n * It is supplied via `VITE_MAPBOX_ACCESS_TOKEN` (baked in at build time from the\n * bare `MAPBOX_TOKEN` env var, which `vite.config.ts` copies into the prefixed\n * name — the spelling Mapbox's own tooling uses) or set at runtime through\n * Settings → Environment variables (`window.__GEOLIBRE_RUNTIME_ENV__`, which\n * bypasses Vite's envPrefix allowlist, so a bare `MAPBOX_TOKEN` entry works\n * there too). When unset, the Mapbox basemaps prompt for a token in the basemap\n * panel's API keys view instead.\n *\n * Note the precedence, shared with {@link getGoogleMapsApiKey} and\n * {@link getCesiumIonToken}: the prefixed name always wins over the bare one,\n * and `getRuntimeEnvironment` merges build-time and runtime vars into one\n * record. So a build that baked in `VITE_MAPBOX_ACCESS_TOKEN` is overridden at\n * runtime by a Settings entry under that *same* prefixed name; a bare\n * `MAPBOX_TOKEN` entry is a fallback for when nothing was baked in, not a way\n * to override a baked token.\n *\n * @param env - Environment record (defaults to the runtime environment);\n * injectable for testing.\n * @returns The trimmed token, or undefined when unset.\n */\nexport function getMapboxAccessToken(env?: Record<string, string | undefined>): string | undefined {\n const runtimeEnv = env ?? getRuntimeEnvironment();\n const trimmed = runtimeEnv.VITE_MAPBOX_ACCESS_TOKEN?.trim() || runtimeEnv.MAPBOX_TOKEN?.trim();\n return trimmed || undefined;\n}\n\n/**\n * Resolves the ArcGIS API key from the runtime environment.\n *\n * The ArcGIS renderer draws the translated project basemap and non-Esri layers\n * without a key; Esri's basemap styles and location services require one\n * (an ArcGIS Location Platform or ArcGIS Online API key credential). It is\n * supplied via `VITE_ARCGIS_API_KEY` (baked in at build time from the bare\n * `ARCGIS_API_KEY` env var, which `vite.config.ts` copies into the prefixed\n * name) or set at runtime through Settings → Environment variables\n * (`window.__GEOLIBRE_RUNTIME_ENV__`, so a bare `ARCGIS_API_KEY` entry works\n * there too). Same precedence as {@link getMapboxAccessToken}.\n *\n * @param env - Environment record (defaults to the runtime environment);\n * injectable for testing.\n * @returns The trimmed key, or undefined when unset.\n */\nexport function getArcgisApiKey(env?: Record<string, string | undefined>): string | undefined {\n const runtimeEnv = env ?? getRuntimeEnvironment();\n const trimmed = runtimeEnv.VITE_ARCGIS_API_KEY?.trim() || runtimeEnv.ARCGIS_API_KEY?.trim();\n return trimmed || undefined;\n}\n\n/**\n * Resolves the Cesium Ion access token from the runtime environment.\n *\n * Cesium World Terrain and Ion World Imagery need a Cesium Ion token. It is\n * supplied via `VITE_CESIUM_TOKEN` (baked in at build time from the bare\n * `CESIUM_TOKEN` env var, which `vite.config.ts` copies into the prefixed name)\n * or set at runtime through Settings → Environment variables\n * (`window.__GEOLIBRE_RUNTIME_ENV__`, which bypasses Vite's envPrefix allowlist,\n * so a bare `CESIUM_TOKEN` entry works there too).\n *\n * The 3D-globe view itself does **not** need one: it draws the project basemap\n * as its base imagery whenever that basemap has a raster form (see\n * {@link basemapToCesiumImagery}), and falls back to Ion World Imagery — or, with\n * no token, to OpenStreetMap — for the ones that do not. So an absent token\n * costs terrain and the quality of that fallback, not the view.\n *\n * @param env - Environment record (defaults to the runtime environment);\n * injectable for testing.\n * @returns The trimmed token, or undefined when unset.\n */\nexport function getCesiumIonToken(env?: Record<string, string | undefined>): string | undefined {\n const runtimeEnv = env ?? getRuntimeEnvironment();\n const trimmed = runtimeEnv.VITE_CESIUM_TOKEN?.trim() || runtimeEnv.CESIUM_TOKEN?.trim();\n return trimmed || undefined;\n}\n\n/**\n * Builds a full Protomaps v5 style URL for a flavor, injecting the API key.\n *\n * @param flavor - The Protomaps flavor name (e.g. `light`, `dark`, `white`,\n * `grayscale`, `black`).\n * @param env - Environment record (defaults to the runtime environment);\n * injectable for testing.\n * @returns The resolved style URL, or undefined when no API key is configured.\n */\nexport function getProtomapsStyleUrl(\n flavor: string,\n env?: Record<string, string | undefined>,\n): string | undefined {\n const key = getProtomapsApiKey(env);\n if (!key) return undefined;\n return `https://api.protomaps.com/styles/v5/${encodeURIComponent(\n flavor,\n )}/en.json?key=${encodeURIComponent(key)}`;\n}\n","import type { Feature, FeatureCollection, LineString, MultiPolygon, Polygon } from \"geojson\";\nimport { decodePolyline } from \"./polyline\";\nimport { getRuntimeEnvironment } from \"./runtime-env\";\n\n/**\n * Routing / network-analysis client and pure helpers backing the Processing →\n * Network tools (isochrones / service areas, OD cost matrices).\n *\n * Provider: Valhalla. OSRM (the Directions backend) has no isochrone endpoint,\n * so isochrones require Valhalla, which also serves the OD matrix\n * (`/sources_to_targets`) — one consistent API. The default is the shared\n * public FOSSGIS server, overridable via runtime env (`VITE_ROUTING_ENDPOINT`)\n * so a self-hosted Valhalla can be used instead. The pure request builders and\n * response parsers carry no React/MapLibre dependency so they can be\n * unit-tested without a browser or network.\n */\n\n/** Default public Valhalla server (FOSSGIS). CORS-enabled; usage limits apply. */\nexport const DEFAULT_ROUTING_ENDPOINT = \"https://valhalla1.openstreetmap.de\";\n\n/** Valhalla costing models exposed in the UI. */\nexport type RoutingMode = \"auto\" | \"pedestrian\" | \"bicycle\";\n\n/** Isochrone contour metric: travel time (minutes) or distance (km). */\nexport type RoutingMetric = \"time\" | \"distance\";\n\nexport interface RoutingConfig {\n /** Base URL of the Valhalla server (no trailing slash). */\n endpoint: string;\n}\n\n/** A point with a stable id, used as an isochrone origin or matrix source/target. */\nexport interface RoutingPoint {\n id: string | number;\n lon: number;\n lat: number;\n}\n\n/**\n * Resolves the routing configuration from runtime env, defaulting to the public\n * Valhalla server. `VITE_ROUTING_ENDPOINT` overrides the endpoint.\n *\n * @returns The resolved routing configuration.\n */\nexport function getRoutingConfig(): RoutingConfig {\n const env = getRuntimeEnvironment();\n return {\n endpoint: stripTrailingSlash(env.VITE_ROUTING_ENDPOINT?.trim()) || DEFAULT_ROUTING_ENDPOINT,\n };\n}\n\nfunction stripTrailingSlash(value: string | undefined): string | undefined {\n return value ? value.replace(/\\/+$/, \"\") : value;\n}\n\n/**\n * Parses a comma/space-separated contour string (\"5, 10, 15\") into a sorted,\n * de-duplicated list of positive numbers.\n *\n * @param value - The raw contour string.\n * @returns Ascending unique positive contour values.\n */\nexport function parseContours(value: string): number[] {\n const seen = new Set<number>();\n for (const token of value.split(/[\\s,]+/)) {\n if (!token) continue;\n const n = Number(token);\n if (Number.isFinite(n) && n > 0) seen.add(n);\n }\n return [...seen].sort((a, b) => a - b);\n}\n\nexport interface IsochroneRequestBody {\n locations: { lon: number; lat: number }[];\n costing: RoutingMode;\n contours: ({ time: number } | { distance: number })[];\n polygons: true;\n}\n\n/**\n * Builds a Valhalla `/isochrone` request body for a single origin.\n *\n * @param point - The origin `[lon, lat]`.\n * @param opts - Travel mode, metric, and contour values (minutes for time, km for distance).\n * @returns The request body.\n */\nexport function buildIsochroneRequest(\n point: [number, number],\n opts: { mode: RoutingMode; metric: RoutingMetric; contours: number[] },\n): IsochroneRequestBody {\n const [lon, lat] = point;\n return {\n locations: [{ lon, lat }],\n costing: opts.mode,\n contours: opts.contours.map((value) =>\n opts.metric === \"time\" ? { time: value } : { distance: value },\n ),\n polygons: true,\n };\n}\n\ntype IsochroneFeatureProps = {\n source_id: string | number;\n mode: RoutingMode;\n metric: RoutingMetric;\n contour: number | null;\n};\n\n/**\n * Converts a Valhalla isochrone GeoJSON response into polygon features tagged\n * with their origin, mode, metric, and contour value. Non-polygon features\n * (Valhalla can also emit contour LineStrings) are dropped.\n *\n * @param response - The Valhalla `/isochrone` GeoJSON FeatureCollection.\n * @param ctx - Origin id, travel mode, and metric to tag onto each polygon.\n * @returns The tagged polygon features.\n */\nexport function isochroneResponseToFeatures(\n response: unknown,\n ctx: { sourceId: string | number; mode: RoutingMode; metric: RoutingMetric },\n): Feature<Polygon | MultiPolygon, IsochroneFeatureProps>[] {\n const features = (response as FeatureCollection | null)?.features;\n if (!Array.isArray(features)) return [];\n const out: Feature<Polygon | MultiPolygon, IsochroneFeatureProps>[] = [];\n for (const feature of features) {\n const type = feature?.geometry?.type;\n if (type !== \"Polygon\" && type !== \"MultiPolygon\") continue;\n const contour = (feature.properties as { contour?: unknown } | null)?.contour;\n out.push({\n type: \"Feature\",\n geometry: feature.geometry as Polygon | MultiPolygon,\n properties: {\n source_id: ctx.sourceId,\n mode: ctx.mode,\n metric: ctx.metric,\n contour: typeof contour === \"number\" ? contour : null,\n },\n });\n }\n return out;\n}\n\nexport interface MatrixRequestBody {\n sources: { lon: number; lat: number }[];\n targets: { lon: number; lat: number }[];\n costing: RoutingMode;\n}\n\n/**\n * Builds a Valhalla `/sources_to_targets` request body.\n *\n * @param origins - The source points.\n * @param targets - The target points.\n * @param mode - The travel mode.\n * @returns The request body.\n */\nexport function buildMatrixRequest(\n origins: RoutingPoint[],\n targets: RoutingPoint[],\n mode: RoutingMode,\n): MatrixRequestBody {\n return {\n sources: origins.map((p) => ({ lon: p.lon, lat: p.lat })),\n targets: targets.map((p) => ({ lon: p.lon, lat: p.lat })),\n costing: mode,\n };\n}\n\ntype MatrixCell = {\n from_index: number;\n to_index: number;\n /** Travel time in seconds, or null when unreachable. */\n time: number | null;\n /** Travel distance in km, or null when unreachable. */\n distance: number | null;\n};\n\ntype MatrixFeatureProps = {\n origin_id: string | number;\n dest_id: string | number;\n time_s: number;\n distance_km: number;\n mode: RoutingMode;\n};\n\n/**\n * Converts a Valhalla `/sources_to_targets` response into one LineString per\n * reachable origin→destination pair, carrying the travel time and distance.\n * Unreachable pairs (null time) are dropped.\n *\n * @param response - The Valhalla matrix response.\n * @param origins - The origin points (indexed by `from_index`).\n * @param targets - The target points (indexed by `to_index`).\n * @param ctx - The travel mode to tag onto each pair.\n * @returns The OD-pair LineString features.\n */\nexport function matrixResponseToFeatures(\n response: unknown,\n origins: RoutingPoint[],\n targets: RoutingPoint[],\n ctx: { mode: RoutingMode },\n): Feature<LineString, MatrixFeatureProps>[] {\n const rows = (response as { sources_to_targets?: MatrixCell[][] } | null)?.sources_to_targets;\n if (!Array.isArray(rows)) return [];\n const out: Feature<LineString, MatrixFeatureProps>[] = [];\n for (const row of rows) {\n if (!Array.isArray(row)) continue;\n for (const cell of row) {\n if (cell?.time == null || cell.distance == null) continue;\n const origin = origins[cell.from_index];\n const target = targets[cell.to_index];\n if (!origin || !target) continue;\n out.push({\n type: \"Feature\",\n geometry: {\n type: \"LineString\",\n coordinates: [\n [origin.lon, origin.lat],\n [target.lon, target.lat],\n ],\n },\n properties: {\n origin_id: origin.id,\n dest_id: target.id,\n time_s: cell.time,\n distance_km: cell.distance,\n mode: ctx.mode,\n },\n });\n }\n }\n return out;\n}\n\nexport interface RouteRequestBody {\n locations: { lon: number; lat: number }[];\n costing: RoutingMode;\n directions_options: { units: \"kilometers\" };\n}\n\n/**\n * Builds a Valhalla `/route` request body that visits the points in the given\n * order. Valhalla snaps each location to the nearest routable edge, so points\n * that are off the road network (cell sites, sensors) still route cleanly.\n *\n * @param points - The waypoints in visiting order (at least two).\n * @param mode - The travel mode (costing model).\n * @returns The request body.\n */\nexport function buildRouteRequest(points: RoutingPoint[], mode: RoutingMode): RouteRequestBody {\n return {\n locations: points.map((p) => ({ lon: p.lon, lat: p.lat })),\n costing: mode,\n directions_options: { units: \"kilometers\" },\n };\n}\n\ntype RouteLeg = {\n shape?: string;\n summary?: { time?: number; length?: number };\n};\n\ntype RouteFeatureProps = {\n leg_index: number;\n from_id: string | number;\n to_id: string | number;\n time_s: number;\n distance_km: number;\n mode: RoutingMode;\n};\n\n/**\n * Converts a Valhalla `/route` response into one LineString per leg (the road\n * path between two consecutive waypoints), carrying the leg's travel time and\n * distance. Legs without a decodable shape are dropped.\n *\n * @param response - The Valhalla `/route` response.\n * @param points - The waypoints passed to the request, indexing the legs.\n * @param ctx - The travel mode to tag onto each leg.\n * @returns One LineString feature per routed leg.\n */\nexport function routeResponseToFeatures(\n response: unknown,\n points: RoutingPoint[],\n ctx: { mode: RoutingMode },\n): Feature<LineString, RouteFeatureProps>[] {\n const legs = (response as { trip?: { legs?: RouteLeg[] } } | null)?.trip?.legs;\n if (!Array.isArray(legs)) return [];\n const out: Feature<LineString, RouteFeatureProps>[] = [];\n legs.forEach((leg, index) => {\n if (typeof leg?.shape !== \"string\") return;\n const coordinates = decodePolyline(leg.shape, 6);\n if (coordinates.length < 2) return;\n const from = points[index];\n const to = points[index + 1];\n out.push({\n type: \"Feature\",\n geometry: { type: \"LineString\", coordinates },\n properties: {\n leg_index: index,\n from_id: from?.id ?? index,\n to_id: to?.id ?? index + 1,\n time_s: typeof leg.summary?.time === \"number\" ? leg.summary.time : 0,\n distance_km: typeof leg.summary?.length === \"number\" ? leg.summary.length : 0,\n mode: ctx.mode,\n },\n });\n });\n return out;\n}\n\n/**\n * Comparator for ordering route waypoints by a chosen attribute value. Numeric\n * strings and timestamps (anything `Date.parse` understands, e.g. ISO 8601)\n * sort chronologically/numerically; values that parse this way sort before\n * free-form text, which falls back to a locale string comparison. Empty/missing\n * values sort last so unlabeled points trail the ordered ones.\n *\n * Numeric parsing is tried before date parsing, so a single column is expected\n * to use one convention: a column mixing plain integers with ISO timestamps\n * would compare them on the same numeric axis (epoch millis dwarf small\n * integers), which is not a meaningful ordering. Real sequence/time columns use\n * one convention, so this is acceptable in practice.\n *\n * @param a - The first value.\n * @param b - The second value.\n * @returns A negative, zero, or positive number for ascending order.\n */\nexport function compareSequenceValues(a: unknown, b: unknown): number {\n const aMissing = isMissingValue(a);\n const bMissing = isMissingValue(b);\n if (aMissing && bMissing) return 0;\n if (aMissing) return 1;\n if (bMissing) return -1;\n const an = toComparableNumber(a);\n const bn = toComparableNumber(b);\n if (an !== null && bn !== null) return an - bn;\n if (an !== null) return -1;\n if (bn !== null) return 1;\n return String(a).localeCompare(String(b));\n}\n\nfunction isMissingValue(value: unknown): boolean {\n return value == null || (typeof value === \"string\" && value.trim() === \"\");\n}\n\nfunction toComparableNumber(value: unknown): number | null {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\") return null;\n // Only treat plain decimal/scientific notation as numeric, so hex (\"0x1A\")\n // and other Number()-coercible forms sort as text rather than by their\n // surprising numeric value.\n if (/^[+-]?\\d+(\\.\\d+)?([eE][+-]?\\d+)?$/.test(trimmed)) {\n return Number(trimmed);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isFinite(parsed)) return parsed;\n }\n return null;\n}\n\n/**\n * Error thrown when a Valhalla request returns a non-2xx response. Carries the\n * HTTP status so callers can branch on it (e.g. a 4xx rejection) without\n * pattern-matching the message string.\n */\nexport class RoutingRequestError extends Error {\n readonly status: number;\n\n constructor(status: number, statusText: string) {\n super(`Routing request failed (${status} ${statusText})`);\n this.name = \"RoutingRequestError\";\n this.status = status;\n }\n}\n\nasync function postJson(url: string, body: unknown, signal?: AbortSignal): Promise<unknown> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new RoutingRequestError(response.status, response.statusText);\n }\n return response.json();\n}\n\n/**\n * Requests an isochrone from the Valhalla server.\n *\n * @param endpoint - The Valhalla base URL.\n * @param body - The request body from {@link buildIsochroneRequest}.\n * @param signal - Optional abort signal.\n * @returns The Valhalla GeoJSON response.\n */\nexport function requestIsochrone(\n endpoint: string,\n body: IsochroneRequestBody,\n signal?: AbortSignal,\n): Promise<unknown> {\n return postJson(`${stripTrailingSlash(endpoint)}/isochrone`, body, signal);\n}\n\n/**\n * Requests an OD cost matrix from the Valhalla server.\n *\n * @param endpoint - The Valhalla base URL.\n * @param body - The request body from {@link buildMatrixRequest}.\n * @param signal - Optional abort signal.\n * @returns The Valhalla matrix response.\n */\nexport function requestMatrix(\n endpoint: string,\n body: MatrixRequestBody,\n signal?: AbortSignal,\n): Promise<unknown> {\n return postJson(`${stripTrailingSlash(endpoint)}/sources_to_targets`, body, signal);\n}\n\n/**\n * Requests a route through ordered waypoints from the Valhalla server.\n *\n * @param endpoint - The Valhalla base URL.\n * @param body - The request body from {@link buildRouteRequest}.\n * @param signal - Optional abort signal.\n * @returns The Valhalla `/route` response.\n */\nexport function requestRoute(\n endpoint: string,\n body: RouteRequestBody,\n signal?: AbortSignal,\n): Promise<unknown> {\n return postJson(`${stripTrailingSlash(endpoint)}/route`, body, signal);\n}\n","import { DEFAULT_LAYER_STYLE, styleValue, type LayerStyle, type VectorRule } from \"./types\";\nimport { getVectorColorRamp } from \"./color-ramp\";\nimport { getActiveSemiMajorAxisMeters } from \"./ellipsoids\";\nimport { removeTrailingJsonCommas } from \"./expressions\";\n\n/**\n * A data-driven color value for a vector paint property: either a plain CSS\n * color string, or a MapLibre expression array (e.g. a categorized `match` or\n * graduated `interpolate`). Typed maplibre-agnostically so `@geolibre/core`\n * stays free of a maplibre-gl dependency; consumers cast to the concrete\n * `PropertyValueSpecification<string>` where the MapLibre types are in scope.\n */\nexport type VectorColorValue = string | unknown[];\n\n/** Whether a color value is a data-driven expression rather than a flat color. */\nexport function isVectorColorExpression(value: VectorColorValue): value is unknown[] {\n return Array.isArray(value);\n}\n\n/**\n * Whether a string is a 6-digit hex color (`#rrggbb`), the form the vector\n * renderer treats as valid. Exported so the symbology exporters can apply the\n * same validity check the live `match`/`case` expressions use rather than\n * re-deriving the regex.\n */\nexport function isHexColor(value: unknown): boolean {\n // Guard the type: `stop.color`/`rule.color` are typed `string`, but the data\n // can come from a hand-edited or imported .geolibre.json, so a missing/null\n // value must return false rather than throw on `.trim()`.\n return typeof value === \"string\" && /^#[0-9a-f]{6}$/i.test(value.trim());\n}\n\n/** A 3- or 6-digit hex color, as emitted by the simplestyle spec. */\nfunction isSimpleStyleColor(value: string): boolean {\n return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim());\n}\n\n/**\n * simplestyle-spec color and numeric property names. Color keys carry CSS hex\n * colors; numeric keys carry plain numbers. See\n * https://github.com/mapbox/simplestyle-spec.\n */\nconst SIMPLE_STYLE_COLOR_KEYS = [\"fill\", \"stroke\", \"marker-color\"] as const;\nconst SIMPLE_STYLE_NUMBER_KEYS = [\n \"fill-opacity\",\n \"stroke-width\",\n \"stroke-opacity\",\n // Non-standard: alpha from a KML IconStyle color, wired into circle-opacity.\n \"marker-opacity\",\n] as const;\n\nfunction isSimpleStyleEnabled(style: LayerStyle): boolean {\n return styleValue(style, \"simpleStyleEnabled\") === true;\n}\n\n/**\n * Wrap a resolved color value so a per-feature simplestyle property takes\n * precedence when {@link LayerStyle.simpleStyleEnabled} is set. Returns the base\n * value unchanged when the feature lacks the property or the mode is off.\n *\n * @param style - The layer style.\n * @param property - The simplestyle property name (e.g. `fill`, `stroke`).\n * @param base - The flat color or expression to fall back to.\n * @returns A `coalesce` expression, or the base value when disabled.\n */\nfunction withSimpleStyleColor(\n style: LayerStyle,\n property: (typeof SIMPLE_STYLE_COLOR_KEYS)[number],\n base: VectorColorValue,\n): VectorColorValue {\n if (!isSimpleStyleEnabled(style)) return base;\n // A zoom-stepped base (per-rule scale ranges) must keep its step outermost:\n // MapLibre only allows [\"zoom\"] as the input of a top-level step/interpolate,\n // so the per-feature coalesce is applied inside each step output instead of\n // around the whole expression.\n return mapZoomStepOutputs(base, (value) => [\n \"coalesce\",\n [\"get\", property],\n value,\n ]) as VectorColorValue;\n}\n\n/**\n * Apply a per-feature transform to a value, descending into the outputs of a\n * top-level `[\"step\", [\"zoom\"], …]` expression (as produced by the rule-based\n * per-rule zoom compilation) so the zoom step stays outermost. MapLibre\n * rejects any expression where `[\"zoom\"]` is not the input of a top-level\n * `step`/`interpolate`, so wrappers (simplestyle coalesce, opacity folds)\n * must be pushed inside the step's outputs rather than wrapped around it.\n * Non-step values are transformed directly.\n */\nexport function mapZoomStepOutputs(\n value: unknown,\n transform: (output: unknown) => unknown,\n): unknown {\n if (\n Array.isArray(value) &&\n value[0] === \"step\" &&\n Array.isArray(value[1]) &&\n value[1][0] === \"zoom\"\n ) {\n const out: unknown[] = [\"step\", value[1], transform(value[2])];\n for (let index = 3; index < value.length; index += 2) {\n out.push(value[index], transform(value[index + 1]));\n }\n return out;\n }\n return transform(value);\n}\n\n/**\n * Resolve a numeric paint value, letting a per-feature simplestyle property\n * override the layer value when {@link LayerStyle.simpleStyleEnabled} is set.\n *\n * The lookup is wrapped in a `coalesce` rather than leaning on `to-number`'s\n * own fallback argument, because that fallback only fires when a value fails\n * to convert — and a *missing* property does not fail, it converts to 0. Since\n * simplestyle is enabled for the whole layer as soon as any feature carries any\n * one of these keys, features that carry only some of them (an ArcGIS KML\n * export writes `fill`/`fill-opacity` but no `marker-opacity`, for instance)\n * would otherwise be painted at opacity 0 or width 0 — present in the layer,\n * invisible on the map (#1552).\n *\n * @param style - The layer style.\n * @param property - The simplestyle property name (e.g. `stroke-width`).\n * @param base - The layer-level fallback value.\n * @returns A `to-number` expression, or `base` when disabled.\n */\nexport function simpleStyleNumberValue(\n style: LayerStyle,\n property: (typeof SIMPLE_STYLE_NUMBER_KEYS)[number],\n base: number,\n): number | unknown[] {\n if (!isSimpleStyleEnabled(style)) return base;\n return [\"to-number\", [\"coalesce\", [\"get\", property], base], base];\n}\n\n/**\n * Ground resolution (meters per pixel) at MapLibre zoom 0 on the equator, for\n * the Web Mercator projection: earth circumference (2*pi*6378137) over the\n * 512px world at zoom 0. Resolution halves with every zoom level. Exported so\n * the Mapbox-style importer can reverse {@link metersWidthExpression} without a\n * second copy of the magic number.\n */\nexport const MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 = (2 * Math.PI * 6378137) / 512;\n\n/**\n * Same ground resolution as {@link MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0} but for\n * the project's active body: its circumference over the 512px zoom-0 world. On\n * Earth this equals the constant; on the Moon/Mars it uses that body's radius so\n * a stroke width given in ground meters renders at the correct on-screen size.\n * Exported so the Mapbox-style importer can reverse {@link metersWidthExpression}\n * with the same ellipsoid-aware factor the exporter used (keeping the round trip\n * correct on non-Earth projects), rather than the Earth-only constant.\n */\nexport function mercatorMetersPerPixelAtZoom0(): number {\n return (2 * Math.PI * getActiveSemiMajorAxisMeters()) / 512;\n}\n\n// Largest zoom MapLibre renders; used as the upper interpolation stop.\nconst MAX_MERCATOR_ZOOM = 24;\n\n/**\n * Build a zoom-driven width expression that keeps a stroke proportional to the\n * map scale, so a width given in ground meters renders thicker when zoomed in\n * and thinner when zoomed out (QGIS \"map units\" behavior).\n *\n * In Web Mercator the pixels-per-meter ratio doubles with each zoom level, so\n * an `[\"exponential\", 2]` interpolation between two stops one zoom apart is\n * exact across the whole range. The conversion is referenced to the equator;\n * because Mercator stretches distances toward the poles, the on-screen width at\n * higher latitudes is correspondingly larger, matching how the underlying map\n * is itself stretched.\n *\n * Typed maplibre-agnostically (`unknown[]`); consumers cast to the concrete\n * `PropertyValueSpecification<number>` where the MapLibre types are in scope.\n *\n * @param meters - The stroke width in ground meters.\n * @returns A MapLibre `interpolate` expression array.\n */\nexport function metersWidthExpression(meters: number): unknown[] {\n const widthAtZoom0 = meters / mercatorMetersPerPixelAtZoom0();\n return [\n \"interpolate\",\n [\"exponential\", 2],\n [\"zoom\"],\n 0,\n widthAtZoom0,\n MAX_MERCATOR_ZOOM,\n widthAtZoom0 * 2 ** MAX_MERCATOR_ZOOM,\n ];\n}\n\n/**\n * Resolve the `line-width` paint value for a layer style, honoring the\n * {@link LayerStyle.strokeWidthUnit}:\n *\n * - `\"meters\"`: a zoom-driven {@link metersWidthExpression} from the flat\n * `strokeWidth`, so the stroke scales with the map. A per-feature pixel\n * `stroke-width` override no longer applies in this mode.\n * - `\"pixels\"` (default): the constant pixel width, still honoring any\n * per-feature simplestyle `stroke-width`.\n *\n * Shared by the map style-mapper and the geo-editor plugin so the Sketches\n * store layer and Geoman's interaction display layers render an identical\n * width.\n *\n * @param style - The layer style.\n * @returns A number (constant pixels) or a MapLibre expression array.\n */\nexport function lineWidthValue(style: LayerStyle): number | unknown[] {\n // Proportional (graduated) sizing takes precedence: width is driven by a\n // numeric field, reusing the circle-radius output range as the width range.\n const proportionalWidth = proportionalRadiusExpression(style);\n if (proportionalWidth) {\n // Per-rule widths still override the proportional base for matched\n // features (the property-driven interpolate nests legally inside a\n // case), mirroring circleRadiusValue.\n return vectorStrokeWidthValue(style, proportionalWidth);\n }\n if (styleValue(style, \"strokeWidthUnit\") === \"meters\") {\n // The meters width is itself a zoom-driven interpolation; embedding it\n // inside a per-rule case would nest [\"zoom\"] below the top level, which\n // MapLibre rejects. Like the per-feature simplestyle width, per-rule pixel\n // widths do not apply in meters mode.\n return metersWidthExpression(styleValue(style, \"strokeWidth\"));\n }\n // Per-rule symbol widths (rule-based mode) override the layer width for\n // features a rule matches; unmatched features keep the base value.\n return vectorStrokeWidthValue(\n style,\n simpleStyleNumberValue(style, \"stroke-width\", styleValue(style, \"strokeWidth\")),\n );\n}\n\n/**\n * Whether a FeatureCollection carries per-feature simplestyle-spec properties\n * worth honoring: at least one feature with a valid hex color in a color key\n * (`fill`/`stroke`/`marker-color`) or a finite number in a numeric key\n * (`fill-opacity`/`stroke-width`/`stroke-opacity`). The scan is capped so very\n * large collections do not pay a full pass.\n *\n * @param geojson - The collection to inspect (may be undefined).\n * @returns `true` when simplestyle rendering should be enabled for the layer.\n */\nexport function hasSimpleStyleProperties(\n geojson: { features?: { properties?: Record<string, unknown> | null }[] } | undefined,\n): boolean {\n const features = geojson?.features;\n if (!features?.length) return false;\n const limit = Math.min(features.length, 1000);\n for (let index = 0; index < limit; index += 1) {\n const properties = features[index]?.properties;\n if (!properties) continue;\n for (const key of SIMPLE_STYLE_COLOR_KEYS) {\n const value = properties[key];\n if (typeof value === \"string\" && isSimpleStyleColor(value)) return true;\n }\n for (const key of SIMPLE_STYLE_NUMBER_KEYS) {\n const value = properties[key];\n if (typeof value === \"number\" && Number.isFinite(value)) return true;\n }\n }\n return false;\n}\n\n/**\n * Parses a user-entered MapLibre expression string into an expression array,\n * tolerating trailing commas. Returns null when the text is empty or not a\n * JSON array.\n */\nexport function parseJsonExpression(expression: string): unknown[] | null {\n const trimmed = expression.trim();\n if (!trimmed) return null;\n\n try {\n const parsed = JSON.parse(removeTrailingJsonCommas(trimmed));\n if (!Array.isArray(parsed)) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\n/**\n * Builds the data-driven color value for a vector layer's current style mode.\n * `single` (or any mode that cannot produce a valid expression) returns the\n * flat fallback color; `categorized` returns a `match` expression, `graduated`\n * a `step` expression, and `expression` the parsed user expression.\n *\n * Graduated stops are class lower bounds (see\n * {@link createGraduatedClassBreaks}), so the classes are discrete: the map\n * paints exactly one color per class, which is what the legend (`≥ value`) and\n * the QML/SLD exporters already describe. Rendering them as a continuous\n * `interpolate` instead blends a shade per feature, so a 4-class layer draws in\n * many more than 4 colors and stops matching its own legend (#1384).\n *\n * @param style - The layer style.\n * @param fallbackColor - The flat color used for `single` mode and as the\n * expression fallback.\n * @returns A flat color string or a MapLibre color expression.\n */\nexport function vectorColorExpression(style: LayerStyle, fallbackColor: string): VectorColorValue {\n const mode = styleValue(style, \"vectorStyleMode\");\n if (mode === \"single\") return fallbackColor;\n\n if (mode === \"expression\") {\n return parseJsonExpression(styleValue(style, \"vectorStyleExpression\")) ?? fallbackColor;\n }\n\n if (mode === \"rule-based\") {\n return ruleBasedColorExpression(style, fallbackColor);\n }\n\n const property = styleValue(style, \"vectorStyleProperty\").trim();\n if (!property) return fallbackColor;\n\n if (mode === \"categorized\") {\n const stops = styleValue(style, \"vectorStyleStops\").filter(\n (stop) => String(stop.value).trim().length > 0 && isHexColor(stop.color),\n );\n if (stops.length === 0) return fallbackColor;\n\n return [\n \"match\",\n [\"to-string\", [\"get\", property]],\n ...stops.flatMap((stop) => [String(stop.value).trim(), stop.color]),\n fallbackColor,\n ];\n }\n\n const stops = graduatedStops(style);\n if (stops.length < 2) return fallbackColor;\n\n // `step` outputs the base color below the first break, then switches at each\n // subsequent break, so every feature lands in exactly one class and features\n // under the lowest break keep the first class's color (matching how the SLD\n // exporter writes its leading `< first` rule).\n return [\n \"step\",\n [\"to-number\", [\"get\", property], stops[0].value],\n stops[0].color,\n ...stops.slice(1).flatMap((stop) => [stop.value, stop.color]),\n ];\n}\n\n/**\n * The graduated stops of a style, cleaned for rendering: non-numeric values and\n * invalid colors dropped, sorted ascending, and de-duplicated. MapLibre rejects\n * a `step` expression whose inputs are not strictly ascending, and stops reach\n * here from hand-edited projects and style imports as well as the Style panel,\n * so repeats are dropped rather than trusted away.\n */\nfunction graduatedStops(style: LayerStyle): { color: string; value: number }[] {\n const sorted = styleValue(style, \"vectorStyleStops\")\n .map((stop) => ({\n color: stop.color,\n value: typeof stop.value === \"number\" ? stop.value : Number.parseFloat(stop.value),\n }))\n .filter((stop) => Number.isFinite(stop.value) && isHexColor(stop.color))\n .sort((a, b) => a.value - b.value);\n return sorted.filter((stop, index) => index === 0 || stop.value > sorted[index - 1].value);\n}\n\n/**\n * A rule-based renderer rule resolved for rendering: the rule tree walked down\n * to a drawable leaf, with the ancestors' filters ANDed in and zoom ranges\n * intersected. Only enabled leaves with a valid\n * color and filter appear; group rules (rules other rules name as\n * {@link VectorRule.parentId}) contribute constraints but are not themselves\n * emitted. Shared by the paint compilers, the legend, and the symbology\n * exporters so all of them agree on which rules actually draw.\n */\nexport interface EffectiveVectorRule {\n id: string;\n /** The leaf rule's label (for the legend / exported rule titles). */\n label: string;\n /** The symbol fill/circle color (valid 6-digit hex). */\n color: string;\n /** The parsed MapLibre filter, ANDed with every ancestor's filter. */\n filter: unknown[];\n /** Intersected lower zoom bound (inclusive), when any rule in the chain has one. */\n minZoom?: number;\n /** Intersected upper zoom bound (exclusive), when any rule in the chain has one. */\n maxZoom?: number;\n strokeColor?: string;\n strokeWidth?: number;\n fillOpacity?: number;\n circleRadius?: number;\n}\n\n/** A finite per-rule zoom bound, or undefined when unset/invalid. */\nfunction ruleZoom(value: number | undefined): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Resolve the layer's rule tree into the drawable leaf rules (see\n * {@link EffectiveVectorRule}) plus the catch-all else rule. Disabled rules\n * (and every descendant of a disabled group), rules with an invalid color or\n * filter, subtrees under a group whose non-blank filter does not parse, and\n * rules whose intersected zoom range is empty are all dropped. Cycles in\n * `parentId` are treated as broken chains and dropped rather than looping.\n *\n * @param style - The layer style (reads {@link LayerStyle.vectorRules}).\n * @returns The ordered drawable rules and the enabled else rule, if any.\n */\nexport function effectiveVectorRules(style: LayerStyle): {\n rules: EffectiveVectorRule[];\n elseRule: VectorRule | null;\n} {\n const all = styleValue(style, \"vectorRules\");\n const elseRule = all.find((rule) => rule.isElse && rule.enabled !== false) ?? null;\n const byId = new Map<string, VectorRule>();\n for (const rule of all) {\n if (!rule.isElse) byId.set(rule.id, rule);\n }\n // A rule other rules point at is a group; only leaves draw. A rule naming\n // itself as its parent is treated as top-level (mirroring the editor and\n // the QML exporter), not as its own group.\n const groupIds = new Set<string>();\n for (const rule of byId.values()) {\n if (rule.parentId && rule.parentId !== rule.id && byId.has(rule.parentId)) {\n groupIds.add(rule.parentId);\n }\n }\n\n const rules: EffectiveVectorRule[] = [];\n for (const rule of all) {\n if (rule.isElse || groupIds.has(rule.id)) continue;\n if (rule.enabled === false || !isHexColor(rule.color)) continue;\n const own = parseJsonExpression(rule.filter);\n // A MapLibre filter is an expression that must start with a string operator;\n // skip non-operator arrays (e.g. a bare value) so the compiled `case` never\n // carries a non-boolean condition that MapLibre would reject at runtime.\n if (!own || typeof own[0] !== \"string\") continue;\n\n const filters: unknown[][] = [own];\n let minZoom = ruleZoom(rule.minZoom);\n let maxZoom = ruleZoom(rule.maxZoom);\n let dropped = false;\n const seen = new Set([rule.id]);\n // A self-referencing parentId means \"no parent\" (a normal top-level\n // rule), matching the editor's tree walk and the QML exporter.\n let parent = rule.parentId && rule.parentId !== rule.id ? byId.get(rule.parentId) : undefined;\n while (parent) {\n if (seen.has(parent.id) || parent.enabled === false) {\n dropped = true;\n break;\n }\n seen.add(parent.id);\n if (parent.filter.trim()) {\n const parentFilter = parseJsonExpression(parent.filter);\n // A group whose filter is written but unreadable can match nothing;\n // dropping the subtree mirrors how an invalid leaf filter is skipped.\n if (!parentFilter || typeof parentFilter[0] !== \"string\") {\n dropped = true;\n break;\n }\n filters.unshift(parentFilter);\n }\n const parentMin = ruleZoom(parent.minZoom);\n const parentMax = ruleZoom(parent.maxZoom);\n if (parentMin !== undefined) {\n minZoom = minZoom === undefined ? parentMin : Math.max(minZoom, parentMin);\n }\n if (parentMax !== undefined) {\n maxZoom = maxZoom === undefined ? parentMax : Math.min(maxZoom, parentMax);\n }\n // An ancestor with a self-referencing parentId is a top-level rule (the\n // same convention as the initial parent resolution above); advancing to\n // itself would trip the cycle guard and wrongly drop the whole subtree.\n parent =\n parent.parentId && parent.parentId !== parent.id ? byId.get(parent.parentId) : undefined;\n }\n if (dropped) continue;\n if (minZoom !== undefined && maxZoom !== undefined && minZoom >= maxZoom) {\n continue;\n }\n\n rules.push({\n id: rule.id,\n label: rule.label,\n color: rule.color,\n filter: filters.length === 1 ? filters[0] : [\"all\", ...filters],\n ...(minZoom !== undefined ? { minZoom } : {}),\n ...(maxZoom !== undefined ? { maxZoom } : {}),\n ...(isHexColor(rule.strokeColor) ? { strokeColor: rule.strokeColor } : {}),\n ...(typeof rule.strokeWidth === \"number\" && Number.isFinite(rule.strokeWidth)\n ? { strokeWidth: rule.strokeWidth }\n : {}),\n ...(typeof rule.fillOpacity === \"number\" && Number.isFinite(rule.fillOpacity)\n ? { fillOpacity: rule.fillOpacity }\n : {}),\n ...(typeof rule.circleRadius === \"number\" && Number.isFinite(rule.circleRadius)\n ? { circleRadius: rule.circleRadius }\n : {}),\n });\n }\n return { rules, elseRule };\n}\n\n/**\n * A rule's match condition as a MapLibre filter: its (ancestor-combined)\n * attribute filter, with any scale range folded in as `[\"zoom\"]` comparisons.\n * The zoom window is half-open (`min <= zoom < max`), matching how\n * {@link zoomWrappedRuleValue} activates rules per zoom segment. MapLibre only\n * re-evaluates `[\"zoom\"]` in filters at integer zoom levels, so a scale-ranged\n * rule's features appear/disappear on whole zooms — the paint expressions keep\n * the fractional-zoom precision.\n */\nfunction ruleMatchFilter(rule: EffectiveVectorRule): unknown[] {\n const conditions: unknown[] = [rule.filter];\n if (rule.minZoom !== undefined) {\n conditions.push([\">=\", [\"zoom\"], rule.minZoom]);\n }\n if (rule.maxZoom !== undefined) {\n conditions.push([\"<\", [\"zoom\"], rule.maxZoom]);\n }\n return conditions.length === 1 ? rule.filter : [\"all\", ...conditions];\n}\n\n// The layer sync recomputes the visibility filter for every render sub-layer\n// (fill, line, point, labels, …) on every sync tick, and compiling it re-walks\n// and re-parses the whole rule tree. The store replaces the vectorRules array\n// on every edit, so the array's identity is a correct cache key; memoizing on\n// it makes the repeated per-sub-layer calls O(1), mirroring the WeakMap caches\n// in the map package (e.g. the text-marker scan).\nconst visibilityFilterCache = new WeakMap<VectorRule[], unknown[] | null>();\n\n/**\n * The MapLibre filter that hides features matching no rule, for a rule-based\n * layer whose catch-all else rule has been switched off. Returns `null` — and\n * callers then leave their geometry filters untouched — unless the layer is in\n * `\"rule-based\"` mode AND carries an else record explicitly disabled\n * (`enabled: false`). An absent else record keeps the historical fallback\n * rendering, so projects saved before the toggle existed are unaffected.\n *\n * With the else rule off, the returned `[\"any\", filter1, filter2, …]` keeps\n * only features matched by a drawable rule (see {@link effectiveVectorRules});\n * with no drawable rules it returns the vacuous `[\"any\"]`, which matches\n * nothing, so every feature hides. Sub-layers combine it with their geometry\n * filters, which also drops unmatched features from labels and hit-testing —\n * mirroring QGIS, where features matching no rule are simply not drawn.\n *\n * @param style - The layer style (reads {@link LayerStyle.vectorRules}).\n * @returns The filter to `[\"all\", …]`-combine into each sub-layer, or `null`.\n */\nexport function ruleBasedVisibilityFilter(style: LayerStyle): unknown[] | null {\n if (styleValue(style, \"vectorStyleMode\") !== \"rule-based\") return null;\n const all = styleValue(style, \"vectorRules\");\n const cached = visibilityFilterCache.get(all);\n if (cached !== undefined) return cached;\n const elseRecord = all.find((rule) => rule.isElse);\n const filter =\n !elseRecord || elseRecord.enabled !== false\n ? null\n : [\"any\", ...effectiveVectorRules(style).rules.map(ruleMatchFilter)];\n visibilityFilterCache.set(all, filter);\n return filter;\n}\n\n/**\n * Wrap a per-zoom-segment value builder in a `[\"step\", [\"zoom\"], …]` expression\n * when any rule carries a zoom bound. MapLibre only allows `[\"zoom\"]` as the\n * top-level input of a `step`/`interpolate` in paint properties (not inside a\n * `case` condition), so per-rule scale ranges compile by splitting the zoom\n * axis at every rule bound and rebuilding the `case` expression per segment\n * with only the rules active in that segment. With no zoom bounds the builder\n * runs once and its plain value is returned unchanged, keeping the historical\n * `case` shape (which the Mapbox-style importer recognizes).\n */\nfunction zoomWrappedRuleValue(\n rules: EffectiveVectorRule[],\n build: (active: EffectiveVectorRule[]) => unknown,\n): unknown {\n const bounds = new Set<number>();\n for (const rule of rules) {\n if (rule.minZoom !== undefined) bounds.add(rule.minZoom);\n if (rule.maxZoom !== undefined) bounds.add(rule.maxZoom);\n }\n if (bounds.size === 0) return build(rules);\n const breaks = [...bounds].sort((a, b) => a - b);\n const activeAt = (segmentStart: number) =>\n rules.filter(\n (rule) =>\n (rule.minZoom ?? -Infinity) <= segmentStart && segmentStart < (rule.maxZoom ?? Infinity),\n );\n const expression: unknown[] = [\"step\", [\"zoom\"], build(activeAt(-Infinity))];\n for (const zoom of breaks) {\n expression.push(zoom, build(activeAt(zoom)));\n }\n return expression;\n}\n\n/**\n * Compiles the `\"rule-based\"` renderer's ordered rules into a MapLibre `case`\n * color expression: `[\"case\", filter1, color1, filter2, color2, …, elseColor]`.\n * Rules are evaluated top to bottom; the first matching filter wins. Disabled\n * rules and rules with an invalid filter JSON or a non-hex color are skipped;\n * nested rules AND their ancestors' filters (see {@link effectiveVectorRules}).\n * When any rule carries a zoom range the `case` is rebuilt per zoom segment\n * inside a `[\"step\", [\"zoom\"], …]` wrapper. The catch-all (`isElse`) rule\n * supplies the trailing fallback; when absent or invalid the layer\n * `fallbackColor` is used. With no usable rules the flat fallback color is\n * returned.\n *\n * @param style - The layer style (reads {@link LayerStyle.vectorRules}).\n * @param fallbackColor - The color used when no else rule defines one.\n * @returns A MapLibre expression, or a flat color when no rule applies.\n */\nexport function ruleBasedColorExpression(\n style: LayerStyle,\n fallbackColor: string,\n): VectorColorValue {\n const { rules, elseRule } = effectiveVectorRules(style);\n const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : fallbackColor;\n if (rules.length === 0) return elseColor;\n return zoomWrappedRuleValue(rules, (active) => {\n if (active.length === 0) return elseColor;\n return [\"case\", ...active.flatMap((rule) => [rule.filter, rule.color]), elseColor];\n }) as VectorColorValue;\n}\n\n/** The per-rule override fields a paint channel can read. */\ntype VectorRuleOverrideKey = \"strokeColor\" | \"strokeWidth\" | \"fillOpacity\" | \"circleRadius\";\n\n/**\n * Build a paint value honoring per-rule symbol overrides for one field: a\n * `case` expression (zoom-wrapped when rules carry scale ranges) where each\n * drawable rule's branch yields its override, or the layer fallback when that\n * rule does not override the field. Returns the fallback unchanged when the\n * layer is not in rule-based mode or no rule (including the else rule)\n * overrides the field, so simple styles keep their flat paint values.\n *\n * First-match semantics are preserved: every drawable rule contributes a\n * branch (even without an override) so a feature matched by an earlier rule\n * never picks up a later rule's override.\n */\nfunction ruleOverrideValue(\n style: LayerStyle,\n key: VectorRuleOverrideKey,\n fallback: unknown,\n): unknown {\n if (styleValue(style, \"vectorStyleMode\") !== \"rule-based\") return fallback;\n const { rules, elseRule } = effectiveVectorRules(style);\n const elseOverride = readRuleOverride(elseRule ?? undefined, key);\n if (elseOverride === undefined && !rules.some((rule) => rule[key] !== undefined)) {\n return fallback;\n }\n const elseValue = elseOverride ?? fallback;\n return zoomWrappedRuleValue(rules, (active) => {\n if (active.length === 0) return elseValue;\n return [\"case\", ...active.flatMap((rule) => [rule.filter, rule[key] ?? fallback]), elseValue];\n });\n}\n\n/** A validated override read straight off a (possibly else) {@link VectorRule}. */\nfunction readRuleOverride(\n rule: VectorRule | undefined,\n key: VectorRuleOverrideKey,\n): string | number | undefined {\n const value = rule?.[key];\n if (key === \"strokeColor\") {\n return isHexColor(value) ? (value as string) : undefined;\n }\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * The stroke/outline color for polygon outlines and circle strokes, honoring\n * per-rule {@link VectorRule.strokeColor} overrides in rule-based mode.\n * Falls back to the flat layer stroke color.\n */\nexport function vectorOutlineColorValue(style: LayerStyle): VectorColorValue {\n return ruleOverrideValue(\n style,\n \"strokeColor\",\n styleValue(style, \"strokeColor\"),\n ) as VectorColorValue;\n}\n\n/**\n * A stroke-width paint value honoring per-rule {@link VectorRule.strokeWidth}\n * overrides in rule-based mode; `fallback` is the layer-level width (a number\n * or an already-built expression such as the meters-based interpolation).\n */\nexport function vectorStrokeWidthValue(\n style: LayerStyle,\n fallback: number | unknown[],\n): number | unknown[] {\n return ruleOverrideValue(style, \"strokeWidth\", fallback) as number | unknown[];\n}\n\n/**\n * A fill/circle opacity paint value honoring per-rule\n * {@link VectorRule.fillOpacity} overrides in rule-based mode; `fallback` is\n * the layer-level opacity value (number or expression).\n */\nexport function vectorFillOpacityValue(\n style: LayerStyle,\n fallback: number | unknown[],\n): number | unknown[] {\n return ruleOverrideValue(style, \"fillOpacity\", fallback) as number | unknown[];\n}\n\n/**\n * The color-ramp colors a density heatmap paints with.\n *\n * {@link getVectorColorRamp} falls back to the *first* ramp (\"viridis\") for an\n * unknown name, which is not the heatmap default (\"turbo\"). Both the map paint\n * (`heatmapPaint` in `@geolibre/map`) and the auto-legend gradient resolve the\n * ramp here so a stale or hand-edited `heatmapColorRamp` cannot make the legend\n * and the rendered heatmap disagree about which ramp is in use.\n *\n * @param style - The layer style.\n * @returns The ramp's colors, or the default ramp's colors for an unknown name.\n */\nexport function heatmapRampColors(style: LayerStyle): readonly string[] {\n const value = styleValue(style, \"heatmapColorRamp\");\n const ramp = getVectorColorRamp(value);\n return (ramp.value === value ? ramp : getVectorColorRamp(DEFAULT_LAYER_STYLE.heatmapColorRamp))\n .colors;\n}\n\n/** The validated proportional (graduated) size configuration of a layer. */\nexport interface ProportionalSizeRange {\n property: string;\n minValue: number;\n maxValue: number;\n minRadius: number;\n maxRadius: number;\n}\n\n/**\n * The proportional-size configuration, or `null` when proportional sizing\n * does not apply: disabled, no field chosen, non-finite or degenerate\n * (`maxValue <= minValue`) value range, or non-finite radii. This is the\n * single validation chain for every proportional consumer — circle radius,\n * line width, and marker icon-size (`@geolibre/map`) — so their notion of\n * \"active\" can never drift apart.\n *\n * @param style - The layer style.\n * @returns The validated range, or `null` when proportional sizing is off.\n */\nexport function proportionalSizeRange(style: LayerStyle): ProportionalSizeRange | null {\n if (!styleValue(style, \"proportionalSizeEnabled\")) return null;\n return validatedSizeRange(\n styleValue(style, \"proportionalSizeProperty\"),\n styleValue(style, \"proportionalSizeMinValue\"),\n styleValue(style, \"proportionalSizeMaxValue\"),\n styleValue(style, \"proportionalSizeMinRadius\"),\n styleValue(style, \"proportionalSizeMaxRadius\"),\n );\n}\n\n/**\n * The validation every field-driven size range shares: a chosen field, a\n * finite non-degenerate value span, and finite radii. Kept in one place so a\n * future tightening (rejecting negative radii, say) cannot land on the\n * layer-wide proportional renderer without also landing on the geometry\n * generator's centroids.\n */\nfunction validatedSizeRange(\n rawProperty: string,\n minValue: number,\n maxValue: number,\n minRadius: number,\n maxRadius: number,\n): ProportionalSizeRange | null {\n const property = rawProperty.trim();\n if (!property) return null;\n if (!(Number.isFinite(minValue) && Number.isFinite(maxValue))) return null;\n if (maxValue <= minValue) return null;\n if (!(Number.isFinite(minRadius) && Number.isFinite(maxRadius))) return null;\n return { property, minValue, maxValue, minRadius, maxRadius };\n}\n\n/**\n * The proportional `interpolate` expression mapping\n * `proportionalSizeMinValue..proportionalSizeMaxValue` onto\n * `proportionalSizeMinRadius..proportionalSizeMaxRadius`, or `null` when\n * proportional sizing does not apply (see {@link proportionalSizeRange}).\n *\n * @param style - The layer style.\n * @returns The `interpolate` expression, or `null`.\n */\nexport function proportionalRadiusExpression(style: LayerStyle): unknown[] | null {\n const range = proportionalSizeRange(style);\n return range ? radiusInterpolateExpression(range) : null;\n}\n\n/**\n * The `interpolate` that maps a validated range's value span onto its radius\n * span. Shared by the layer-wide proportional renderer and the geometry\n * generator's centroid sizing so both scale identically (linear, with the\n * value clamped to the range's ends by `interpolate`'s own stop behavior).\n */\nfunction radiusInterpolateExpression(range: ProportionalSizeRange): unknown[] {\n return [\n \"interpolate\",\n [\"linear\"],\n [\"to-number\", [\"get\", range.property], range.minValue],\n range.minValue,\n Math.max(0, range.minRadius),\n range.maxValue,\n Math.max(0, range.maxRadius),\n ];\n}\n\n/**\n * The geometry generator's centroid-size configuration, or `null` when the\n * generated points should all take the flat\n * {@link LayerStyle.geometryGeneratorCircleRadius}: no field chosen, non-finite\n * or degenerate (`maxValue <= minValue`) value range, or non-finite radii.\n * Mirrors {@link proportionalSizeRange} against the generator's own keys.\n *\n * @param style - The layer style.\n * @returns The validated range, or `null` when field-driven sizing is off.\n */\nexport function generatorSizeRange(style: LayerStyle): ProportionalSizeRange | null {\n return validatedSizeRange(\n styleValue(style, \"geometryGeneratorSizeProperty\"),\n styleValue(style, \"geometryGeneratorSizeMinValue\"),\n styleValue(style, \"geometryGeneratorSizeMaxValue\"),\n styleValue(style, \"geometryGeneratorSizeMinRadius\"),\n styleValue(style, \"geometryGeneratorSizeMaxRadius\"),\n );\n}\n\n/**\n * Builds the `circle-radius` paint value for the geometry generator's derived\n * centroid points: an `interpolate` over\n * {@link LayerStyle.geometryGeneratorSizeProperty} when field-driven sizing\n * applies (see {@link generatorSizeRange}), otherwise the flat\n * {@link LayerStyle.geometryGeneratorCircleRadius}.\n *\n * The flat fallback keeps the 1px floor the generator has always applied; the\n * interpolate is left as authored, so a 0px min radius still hides the\n * smallest symbols on purpose.\n *\n * @param style - The layer style.\n * @returns A constant radius (pixels) or a MapLibre `interpolate` expression.\n */\nexport function generatorCircleRadiusValue(style: LayerStyle): number | unknown[] {\n const range = generatorSizeRange(style);\n if (range) return radiusInterpolateExpression(range);\n return Math.max(1, styleValue(style, \"geometryGeneratorCircleRadius\"));\n}\n\n/**\n * Builds the `circle-radius` paint value, honoring proportional (graduated)\n * symbol sizing. When {@link LayerStyle.proportionalSizeEnabled} is set with a\n * chosen numeric field and a valid value range, returns an `interpolate` that\n * maps `proportionalSizeMinValue..proportionalSizeMaxValue` onto\n * `proportionalSizeMinRadius..proportionalSizeMaxRadius`; otherwise the constant\n * {@link LayerStyle.circleRadius}.\n *\n * @param style - The layer style.\n * @returns A constant radius (pixels) or a MapLibre `interpolate` expression.\n */\nexport function circleRadiusValue(style: LayerStyle): number | unknown[] {\n // Per-rule symbol sizes (rule-based mode) override the base radius for\n // features a rule matches; unmatched features keep the base value.\n return ruleOverrideValue(style, \"circleRadius\", baseCircleRadiusValue(style)) as\n | number\n | unknown[];\n}\n\n/** The layer-level circle radius before per-rule overrides. */\nfunction baseCircleRadiusValue(style: LayerStyle): number | unknown[] {\n return proportionalRadiusExpression(style) ?? styleValue(style, \"circleRadius\");\n}\n\n/** Fill color value for a polygon layer (fallback: the layer fill color). */\nexport function vectorFillColorValue(style: LayerStyle): VectorColorValue {\n return withSimpleStyleColor(\n style,\n \"fill\",\n vectorColorExpression(style, styleValue(style, \"fillColor\")),\n );\n}\n\n/**\n * Circle color value for a point layer. Intentionally identical to\n * `vectorFillColorValue`: GeoLibre has no separate point-fill color, so point\n * circles share the polygon fill color (matching `circlePaint` in the map\n * package). Kept as its own function so the per-geometry callers read in\n * parallel and a future dedicated circle color stays a one-line change here.\n */\nexport function vectorCircleColorValue(style: LayerStyle): VectorColorValue {\n return withSimpleStyleColor(\n style,\n \"marker-color\",\n vectorColorExpression(style, styleValue(style, \"fillColor\")),\n );\n}\n\n/**\n * Line color value for line geometry and polygon outlines (fallback: the\n * layer stroke color). For non-`expression` modes the data-driven color is\n * applied to line geometry only, while polygon outlines keep the flat stroke\n * color, matching the polygon-fill-only behavior of categorized/graduated\n * styling.\n */\nexport function vectorLineColorValue(style: LayerStyle): VectorColorValue {\n const strokeColor = styleValue(style, \"strokeColor\");\n if (styleValue(style, \"vectorStyleMode\") === \"rule-based\") {\n return withSimpleStyleColor(style, \"stroke\", ruleBasedLineColorExpression(style));\n }\n const vectorColor = vectorColorExpression(style, strokeColor);\n const resolved =\n vectorColor === strokeColor\n ? strokeColor\n : styleValue(style, \"vectorStyleMode\") === \"expression\"\n ? vectorColor\n : [\"case\", [\"==\", [\"geometry-type\"], \"Polygon\"], strokeColor, vectorColor];\n return withSimpleStyleColor(style, \"stroke\", resolved);\n}\n\n/**\n * The line color for the rule-based renderer: line geometry takes the rule\n * color, polygon outlines take the per-rule stroke color override (falling\n * back to the flat layer stroke). Both channels are built inside one shared\n * zoom wrapper, since composing two independently zoom-stepped expressions in\n * a single `case` would nest `[\"zoom\"]` below the top level, which MapLibre\n * rejects.\n */\nfunction ruleBasedLineColorExpression(style: LayerStyle): VectorColorValue {\n const strokeColor = styleValue(style, \"strokeColor\");\n const { rules, elseRule } = effectiveVectorRules(style);\n const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : strokeColor;\n const elseOutline =\n (readRuleOverride(elseRule ?? undefined, \"strokeColor\") as string | undefined) ?? strokeColor;\n const hasOutlineOverride =\n rules.some((rule) => rule.strokeColor !== undefined) || elseOutline !== strokeColor;\n if (rules.length === 0 && elseColor === strokeColor && !hasOutlineOverride) {\n return strokeColor;\n }\n return zoomWrappedRuleValue(rules, (active) => {\n const colorValue: unknown =\n active.length === 0\n ? elseColor\n : [\"case\", ...active.flatMap((rule) => [rule.filter, rule.color]), elseColor];\n const outlineValue: unknown = !hasOutlineOverride\n ? strokeColor\n : active.length === 0\n ? elseOutline\n : [\n \"case\",\n ...active.flatMap((rule) => [rule.filter, rule.strokeColor ?? strokeColor]),\n elseOutline,\n ];\n if (colorValue === strokeColor && outlineValue === strokeColor) {\n return strokeColor;\n }\n return [\"case\", [\"==\", [\"geometry-type\"], \"Polygon\"], outlineValue, colorValue];\n }) as VectorColorValue;\n}\n\n/**\n * Resolves the 3D-extrusion height for a layer style into a MapLibre value: a\n * plain meters number, or a data-driven expression. In advanced mode a valid\n * `extrusionHeightExpression` wins; otherwise the height is the chosen property\n * scaled by `extrusionHeightScale` (`[\"*\", [\"to-number\", [\"get\", prop], 0],\n * scale]`), or a flat `0` when no property is set (so the layer renders flat\n * rather than erroring). Shared by the map's fill-extrusion paint and the\n * Add Vector Layer control's extrusion mapping so both extrude identically.\n *\n * @param style - The layer style.\n * @returns The extrusion height as a number or a MapLibre expression array.\n */\nexport function extrusionHeightValue(style: LayerStyle): number | unknown[] {\n const advancedExpression = parseJsonExpression(\n styleValue(style, \"extrusionAdvancedStyleEnabled\")\n ? styleValue(style, \"extrusionHeightExpression\")\n : \"\",\n );\n if (advancedExpression) return advancedExpression;\n const property = styleValue(style, \"extrusionHeightProperty\").trim();\n if (!property) return 0;\n const scale = styleValue(style, \"extrusionHeightScale\");\n return [\"*\", [\"to-number\", [\"get\", property], 0], scale];\n}\n\n/**\n * Resolves the 3D-extrusion color for a layer style: a data-driven expression\n * when the layer's symbology mode produces one (categorized/graduated/rule/\n * expression) or an advanced `extrusionColorExpression` is set, otherwise the\n * flat `extrusionColor`. Mirrors the fill-color contract so an extruded layer\n * honors the same attribute-driven styling.\n *\n * @param style - The layer style.\n * @returns A flat color string or a MapLibre color expression array.\n */\nexport function extrusionColorValue(style: LayerStyle): VectorColorValue {\n const flat = styleValue(style, \"extrusionColor\");\n const vectorExpression = vectorColorExpression(style, flat);\n if (vectorExpression !== flat) return vectorExpression;\n const advancedExpression = parseJsonExpression(\n styleValue(style, \"extrusionAdvancedStyleEnabled\")\n ? styleValue(style, \"extrusionColorExpression\")\n : \"\",\n );\n return (advancedExpression as VectorColorValue | null) ?? flat;\n}\n","import {\n Color,\n featureFilter,\n latest,\n normalizePropertyExpression,\n} from \"@maplibre/maplibre-gl-style-spec\";\n\n/** The geometry kinds a MapLibre style expression tells apart (`[\"geometry-type\"]`). */\nexport type StyleGeometryKind = \"Point\" | \"LineString\" | \"Polygon\";\n\n/** A feature as a style layer's expressions read it. */\nexport interface StyleFeatureInput {\n id?: string | number;\n properties: Record<string, unknown> | null | undefined;\n kind: StyleGeometryKind;\n}\n\n/** A style layer's filter and property values, evaluated as MapLibre would. */\nexport interface StyleLayerEvaluator {\n /** Whether the layer's `filter` keeps the feature at `zoom`. */\n passes: (zoom: number, feature: StyleFeatureInput) => boolean;\n /**\n * A paint or layout property's value for a feature at `zoom`: the layer's\n * value (a literal, a legacy function or an expression) or the spec's\n * default. A color comes back as `[r, g, b, a]` with channels 0-255 and\n * alpha 0-1; formatted text as its plain string. `undefined` for a property\n * the spec does not know or an expression that does not compile.\n */\n value: (\n group: \"paint\" | \"layout\",\n name: string,\n zoom: number,\n feature: StyleFeatureInput,\n ) => unknown;\n}\n\ninterface StyleLayerInput {\n id?: string;\n type: string;\n filter?: unknown;\n paint?: Record<string, unknown>;\n layout?: Record<string, unknown>;\n}\n\nconst GEOMETRY_TYPE: Record<StyleGeometryKind, number> = { Point: 1, LineString: 2, Polygon: 3 };\n\ntype Evaluate = (globals: { zoom: number }, feature: unknown) => unknown;\n\n/**\n * Compile a MapLibre style layer's filter and properties for evaluation\n * outside MapLibre, for a renderer that draws the layer itself (the ArcGIS\n * renderer drawing a plugin's own GeoJSON overlay, say). Uses the style spec's\n * own evaluator, so data-driven and zoom expressions, legacy functions and\n * defaults behave as they do on MapLibre. Each property compiles once.\n *\n * @param layer - The style layer (its `type`, `filter`, `paint`, `layout`).\n * @returns The layer's evaluator.\n */\nexport function createStyleLayerEvaluator(layer: StyleLayerInput): StyleLayerEvaluator {\n const root = `layers[${layer.id ?? \"\"}]`;\n let filter: ((zoom: number, feature: unknown) => boolean) | null;\n try {\n const compiled = featureFilter(layer.filter as never, `${root}.filter`);\n filter = (zoom, feature) => compiled.filter({ zoom } as never, feature as never);\n } catch {\n // MapLibre refuses the layer outright; draw nothing rather than everything.\n filter = () => false;\n }\n const compiled = new Map<string, Evaluate | null>();\n const compile = (group: \"paint\" | \"layout\", name: string): Evaluate | null => {\n const key = `${group}/${name}`;\n if (compiled.has(key)) return compiled.get(key)!;\n const spec = (latest as unknown as Record<string, Record<string, unknown>>)[\n `${group}_${layer.type}`\n ]?.[name] as { default?: unknown } | undefined;\n let evaluate: Evaluate | null = null;\n if (spec) {\n const raw = layer[group]?.[name];\n try {\n const expression = normalizePropertyExpression(\n (raw ?? spec.default) as never,\n `${root}.${group}.${name}`,\n spec as never,\n );\n evaluate = (globals, feature) => expression.evaluate(globals as never, feature as never);\n } catch {\n evaluate = null;\n }\n }\n compiled.set(key, evaluate);\n return evaluate;\n };\n const input = (feature: StyleFeatureInput) => ({\n type: GEOMETRY_TYPE[feature.kind],\n id: feature.id,\n properties: feature.properties ?? {},\n });\n return {\n passes: (zoom, feature) => {\n try {\n return filter(zoom, input(feature));\n } catch {\n return false;\n }\n },\n value: (group, name, zoom, feature) => {\n const evaluate = compile(group, name);\n if (!evaluate) return undefined;\n let result: unknown;\n try {\n result = evaluate({ zoom }, input(feature));\n } catch {\n return undefined;\n }\n if (result instanceof Color) {\n const [r, g, b, a] = result.rgb;\n return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a];\n }\n if (result && typeof result === \"object\" && \"sections\" in result) return String(result);\n return result;\n },\n };\n}\n","/**\n * The document language, so formatted numbers and dates follow the UI locale.\n *\n * Shared by the Identify popup's value formatting and by label number\n * formatting, which both fall back to it when the author has not pinned an\n * explicit locale. Returns `undefined` outside a browser (tests, the headless\n * project authoring paths), which leaves `Intl` on its own default.\n */\nexport function documentLocale(): string | undefined {\n const lang = typeof document !== \"undefined\" ? document.documentElement.lang.trim() : \"\";\n return lang || undefined;\n}\n","import type { LabelStyle } from \"./types\";\n\n/**\n * Locale-aware number formatting for map labels (issue #2336).\n *\n * A numeric attribute rendered straight into a label reads as `1234567.5`,\n * which is hard to compare at a glance. QGIS solves this with\n * `format_number(field, places, language)`; the equivalent here is three\n * {@link LabelStyle} fields — {@link LabelStyle.numberFormatEnabled},\n * {@link LabelStyle.numberDecimals} and {@link LabelStyle.numberLocale} —\n * whose effect this module renders two ways:\n *\n * - {@link labelFieldTextField} builds the MapLibre `text-field` expression the\n * 2D map and the Mapbox/MapLibre style export both use, so the map and the\n * exported style agree.\n * - {@link formatLabelNumber} does the same in JavaScript for the paths that\n * resolve label text themselves: the Cesium globe and the duplicate-label\n * collapsing modes.\n *\n * Formatting applies to {@link LabelStyle.field} only. A label\n * {@link LabelStyle.expression} formats its own output (with `number-format`,\n * offered in the Expression Builder), so wrapping it here would fight the\n * author.\n */\n\n/**\n * Locales offered for {@link LabelStyle.numberLocale}, alongside the default\n * empty value that follows the app's own language.\n *\n * Deliberately short, and every entry groups with a character the map's glyph\n * stack can draw: ASCII `,`/`.` or U+00A0. Locales whose CLDR grouping uses\n * the narrow no-break space U+202F (`fr-FR`) or non-Latin digits (`ar-EG`)\n * are left out because a missing glyph renders as a blank box on the map\n * rather than as a separator. An author who needs one of those can still\n * write `[\"number-format\", …]` in the label expression.\n */\nexport const LABEL_NUMBER_LOCALES = [\"en-US\", \"de-DE\", \"ru-RU\", \"hi-IN\"] as const;\n\n/**\n * Magnitude at which the text-field stops treating a value as a finite number.\n *\n * JSON carries no `Infinity`, so the expression cannot test against it\n * directly; this is the largest power of ten a double holds, which doubles as\n * the `to-number` fallback so an unconvertible value fails the same test. A\n * real attribute at or above it labels as plain text instead of a formatted\n * number -- a trade worth making for values already past 1e308.\n */\nconst NON_FINITE_SENTINEL = 1e308;\n\n/** Sample value used to preview a locale's separators in the Style panel. */\nexport const LABEL_NUMBER_SAMPLE = 1234567.5;\n\n/**\n * Cached `Intl.NumberFormat`s, keyed by locale + decimals. `formatLabelNumber`\n * runs once per labeled feature (per Cesium entity, per deduplicated point), so\n * constructing a formatter on every call is the wrong order of magnitude.\n *\n * The key space is small: {@link LABEL_NUMBER_LOCALES} plus the app's own\n * language, times eleven decimal settings, times whatever a hand-edited project\n * pins.\n */\nconst formatterCache = new Map<string, Intl.NumberFormat>();\n\n/** Locale tags already accepted or rejected, so each is tested once. */\nconst localeSupport = new Map<string, boolean>();\n\n/**\n * A formatter for this locale/decimals pair. Grouping is always on: it is the\n * whole point of the setting. The locale must already have been vetted by\n * {@link resolveLabelNumberLocale}, so this never throws.\n */\nfunction labelNumberFormatter(locale: string | undefined, decimals: number): Intl.NumberFormat {\n const digits = clampLabelDecimals(decimals);\n const key = `${locale ?? \"\"}\\u0000${digits}`;\n const cached = formatterCache.get(key);\n if (cached) return cached;\n const formatter = new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: digits,\n maximumFractionDigits: digits,\n });\n formatterCache.set(key, formatter);\n return formatter;\n}\n\n/**\n * Whether every character a locale's grouping produces is one the map's glyph\n * stack can draw: ASCII, or the no-break space U+00A0.\n *\n * {@link LABEL_NUMBER_LOCALES} is curated on exactly this basis, but the picker\n * is not the only way a locale reaches the renderer. A hand-edited project can\n * pin any tag, and the default (`numberLocale: \"\"`) follows the app's own\n * language, so a French or Arabic UI would otherwise put U+202F or Arabic-Indic\n * digits into the `text-field` and label the map with blank boxes. Testing the\n * rendered sample rather than the tag covers both routes and needs no list to\n * keep in sync.\n */\nfunction glyphSafeLocale(tag: string): boolean {\n try {\n const format = new Intl.NumberFormat(tag, {\n useGrouping: true,\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n // Both signs: sv-SE, fi-FI and nb-NO group with U+00A0 (safe) but write\n // negatives with U+2212 MINUS SIGN, so a positive-only check would pass\n // them and then blank-box every negative label.\n for (const sample of [\n format.format(LABEL_NUMBER_SAMPLE),\n format.format(-LABEL_NUMBER_SAMPLE),\n ]) {\n for (const char of sample) {\n const code = char.codePointAt(0) ?? 0;\n if (code >= 0x80 && code !== 0x00a0) return false;\n }\n }\n return true;\n } catch {\n // A malformed BCP 47 tag (`\"en_US\"`, `\"not a locale\"`) makes Intl throw a\n // RangeError. It would propagate out of the Cesium labeler and the dedup\n // builder and take down the layer's labels, and MapLibre evaluates its own\n // Intl.NumberFormat per feature, so the expression path is exposed too.\n return false;\n }\n}\n\n/** Clamp a decimal-places setting to the range the UI and `Intl` both accept. */\nexport function clampLabelDecimals(decimals: number): number {\n if (!Number.isFinite(decimals)) return 0;\n return Math.max(0, Math.min(10, Math.trunc(decimals)));\n}\n\n/**\n * The first locale tag that both `Intl` accepts and the map can draw, or\n * `undefined` to leave `Intl` and MapLibre on the runtime default.\n *\n * Every path resolves its locale through here -- the JavaScript formatter, the\n * MapLibre expression, and the dedup cache key -- so all three agree on which\n * locale is actually in effect.\n */\nexport function resolveLabelNumberLocale(...tags: (string | undefined)[]): string | undefined {\n for (const tag of tags) {\n if (!tag) continue;\n let usable = localeSupport.get(tag);\n if (usable === undefined) {\n usable = glyphSafeLocale(tag);\n localeSupport.set(tag, usable);\n }\n if (usable) return tag;\n }\n return undefined;\n}\n\n/**\n * Format one label value as a number, or `null` when the setting is off or the\n * value is not a finite number (a text attribute keeps its own rendering).\n *\n * @param value - The raw attribute value.\n * @param labels - The layer's label configuration.\n * @param fallbackLocale - Locale used when {@link LabelStyle.numberLocale} is\n * empty; the caller passes the app's language. Omitted means the runtime\n * default, matching how popups format their numbers.\n */\nexport function formatLabelNumber(\n value: unknown,\n labels: Pick<LabelStyle, \"numberFormatEnabled\" | \"numberDecimals\" | \"numberLocale\">,\n fallbackLocale?: string,\n): string | null {\n if (!labels.numberFormatEnabled) return null;\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n const locale = resolveLabelNumberLocale(labels.numberLocale, fallbackLocale);\n return labelNumberFormatter(locale, labels.numberDecimals).format(value);\n}\n\n/**\n * The MapLibre expression that renders {@link LabelStyle.field} as text, with\n * number formatting applied when it is on. Returns `\"\"` when no field is set,\n * which callers treat as \"no label text\".\n *\n * Number formatting is guarded by a `typeof` test rather than applied blindly\n * so a mixed or text column still labels normally; only real numbers take the\n * `number-format` branch.\n */\nexport function labelFieldTextField(\n labels: Pick<LabelStyle, \"field\" | \"numberFormatEnabled\" | \"numberDecimals\" | \"numberLocale\">,\n fallbackLocale?: string,\n): unknown[] | \"\" {\n const field = labels.field;\n if (!field) return \"\";\n const asText = [\"to-string\", [\"coalesce\", [\"get\", field], \"\"]];\n if (!labels.numberFormatEnabled) return asText;\n const locale = resolveLabelNumberLocale(labels.numberLocale, fallbackLocale);\n const digits = clampLabelDecimals(labels.numberDecimals);\n const options: Record<string, unknown> = {};\n if (locale) options.locale = locale;\n // MapLibre's `number-format` ignores a falsy option, so zero fraction digits\n // cannot be requested that way — it would silently fall back to the spec\n // default of up to three. Rounding first gives an integer, which then prints\n // with no fraction digits at all.\n // `to-number` carries a fallback so it can never throw: MapLibre hands a NaN\n // property through as null, and an unguarded conversion raised \"Could not\n // convert null to number\" from inside the render. The fallback is the same\n // sentinel the finiteness test rejects, so a value that could not convert\n // takes the plain-text branch rather than labelling as the fallback itself.\n const numeric = [\"to-number\", [\"get\", field], NON_FINITE_SENTINEL];\n const number = digits > 0 ? numeric : [\"round\", numeric];\n if (digits > 0) {\n options[\"min-fraction-digits\"] = digits;\n options[\"max-fraction-digits\"] = digits;\n }\n return [\n \"case\",\n [\n \"all\",\n [\"==\", [\"typeof\", [\"get\", field]], \"number\"],\n // Finite only, matching formatLabelNumber's Number.isFinite guard.\n // Without it the map rendered Infinity as U+221E -- a glyph the label\n // font need not carry, so a tofu box -- while the JavaScript path fell\n // back to the plain string, which is the disagreement the parity tests\n // exist to prevent. NaN was worse: it reached the render as null and\n // threw out of the conversion.\n [\"<\", [\"abs\", numeric], NON_FINITE_SENTINEL],\n ],\n [\"number-format\", number, options],\n asText,\n ];\n}\n\n/**\n * Render {@link LABEL_NUMBER_SAMPLE} the way a locale/decimals pair would, so\n * the Style panel can name each choice by the separators it actually produces\n * (\"1,234,567.50\") instead of by an opaque language tag.\n *\n * A tag `Intl` rejects (or one the map cannot draw) falls back to the runtime\n * locale's formatting rather than to an unformatted number, matching what\n * {@link formatLabelNumber} and the emitted expression do with the same tag.\n */\nexport function formatLabelNumberSample(locale: string, decimals: number): string {\n return labelNumberFormatter(resolveLabelNumberLocale(locale), decimals).format(\n LABEL_NUMBER_SAMPLE,\n );\n}\n","/**\n * Paint ownership for plugin-registered native layers.\n *\n * A plugin can register a MapLibre `CustomLayerInterface` (a WebGL layer that\n * draws itself, e.g. a Zarr renderer that reprojects chunks on the GPU) through\n * `registerExternalNativeLayer`. Such a layer has **no MapLibre paint\n * properties**, so GeoLibre's Style panel editors (raster opacity/brightness/\n * saturation/contrast/hue, or the vector paint editors) cannot reach it: every\n * `setPaintProperty` call is dropped and the control looks functional while\n * doing nothing (opengeos/GeoLibre#1445).\n *\n * The registration therefore declares `paintMode: \"plugin\"`, which lands in the\n * store layer as `metadata.paintMode` so the UI can hide the editors it cannot\n * apply, and may additionally supply a {@link ExternalNativePaintBridge} so the\n * generic controls GeoLibre keeps (opacity, visibility) are forwarded to the\n * custom layer's own API.\n *\n * The bridge holds live functions, so it cannot live in `metadata` (that is\n * serialized into `.geolibre.json`). It is kept in this module-level registry\n * instead, keyed by layer id, and the layer record only carries the\n * serializable `paintMode` flag.\n */\nexport interface ExternalNativePaintBridge {\n /**\n * Apply an opacity in [0, 1] to the plugin's layer (e.g.\n * `zarrLayer.setOpacity(value)`). When supplied, GeoLibre keeps its Opacity\n * sliders live for the layer and calls this whenever the value changes.\n */\n setOpacity?: (opacity: number) => void;\n /**\n * Apply the panel's show/hide state. MapLibre already honors `visibility` on\n * a custom layer, so this is only needed by layers that also want to release\n * resources or stop fetching while hidden.\n */\n setVisibility?: (visible: boolean) => void;\n}\n\n/** Serializable paint-ownership marker stored in `GeoLibreLayer.metadata`. */\nexport type ExternalNativePaintMode = \"geolibre\" | \"plugin\";\n\n/**\n * Whether a layer is drawn by a **custom render layer** its control created — a\n * deck.gl overlay, 3D Tiles, or Add Vector Layer's own MapLibre layers — rather\n * than by GeoLibre's layer sync. `metadata.customLayerType` is what marks one,\n * and it is the flag `syncExternalNativeLayer` branches on: for these layers the\n * sync only *moves and filters* native layers that already exist, so nothing\n * appears on the map unless the owning control (re)creates them.\n *\n * Lives here, in core, so the map's dispatch and the consumers that need to\n * reason about it — the Layer Library's \"can this be re-added and rendered?\"\n * gate (issue #1520) — share one definition and cannot drift.\n *\n * Distinct from {@link pluginOwnsPaint}: that asks who applies *paint* to an\n * existing layer, this asks who creates the layer at all.\n *\n * @param layer - A store layer (metadata only).\n * @returns True when only the owning control can create the layer's map output.\n */\nexport function controlRendersLayer(layer: { metadata: Record<string, unknown> }): boolean {\n return typeof layer.metadata.customLayerType === \"string\";\n}\n\n/**\n * True when the plugin that registered this layer paints it itself, so GeoLibre\n * must not offer (or apply) MapLibre paint properties for it.\n */\nexport function pluginOwnsPaint(layer: { metadata: Record<string, unknown> }): boolean {\n return layer.metadata.paintMode === \"plugin\";\n}\n\nconst paintBridges = new Map<string, ExternalNativePaintBridge>();\n\n/** Register (or replace) the paint bridge a plugin supplied for a layer. */\nexport function setExternalNativePaintBridge(\n layerId: string,\n bridge: ExternalNativePaintBridge | undefined,\n): void {\n if (bridge && (bridge.setOpacity || bridge.setVisibility)) {\n paintBridges.set(layerId, bridge);\n } else {\n paintBridges.delete(layerId);\n }\n}\n\n/** Drop a layer's paint bridge (on unregister, or when the layer is removed). */\nexport function clearExternalNativePaintBridge(layerId: string): void {\n paintBridges.delete(layerId);\n}\n\n/** The paint bridge for a layer, if its plugin supplied one. */\nexport function getExternalNativePaintBridge(\n layerId: string,\n): ExternalNativePaintBridge | undefined {\n return paintBridges.get(layerId);\n}\n\n/**\n * True when a plugin-painted layer bridged `setOpacity`, so an Opacity slider\n * would actually reach the renderer. It says nothing about GeoLibre-painted\n * layers, which have no bridge: the Style and Layers panels combine it with\n * `!pluginOwnsPaint(layer)` to decide whether to show the slider at all.\n */\nexport function supportsBridgedOpacity(layerId: string): boolean {\n return typeof paintBridges.get(layerId)?.setOpacity === \"function\";\n}\n\n/**\n * True when the primary renderer draws this plugin-painted layer itself, from\n * the store record, and so applies `layer.opacity` without any plugin bridge.\n *\n * A Zarr layer is a MapLibre custom layer owned by the Zarr control on the 2D\n * map, but the ArcGIS view and the Cesium globe render it natively (an ArcGIS\n * layer's `opacity`, a Cesium `ImageryLayer.alpha`) and mount no control to\n * register a bridge. Without this the Opacity slider would be hidden there\n * although it works (opengeos/GeoLibre#2261).\n *\n * @param layer - A store layer.\n * @param renderer - The primary renderer (`useAppStore().primaryRenderer`).\n * @returns Whether an Opacity slider reaches the layer through the renderer.\n */\nexport function rendererAppliesOpacity(\n layer: { type: string },\n renderer: string | undefined,\n): boolean {\n // @geolibre/core sits below @geolibre/map and cannot read its capabilities;\n // these are the renderers whose `nativeZarr` is set, which a test pins.\n // eslint-disable-next-line local/no-renderer-kind-checks -- see above\n return (renderer === \"arcgis\" || renderer === \"cesium\") && layer.type === \"zarr\";\n}\n\n/**\n * Whether `layer` is an Add Vector Layer layer GeoLibre adopted\n * (`maplibre-gl-vector-adopted`, ADOPTED_VECTOR_SOURCE_KIND in\n * @geolibre/plugins) that has no features yet. A project saves a URL- or\n * path-backed adopted layer without them, and the vector control reads them\n * again on reopen. Until then its `source.url` may name GeoParquet or\n * GeoPackage, so a renderer must wait rather than load that URL as GeoJSON.\n *\n * @param layer - A store layer.\n * @returns True while the adopted layer awaits its features.\n */\nexport function isAdoptedVectorAwaitingFeatures(layer: {\n metadata: Record<string, unknown>;\n geojson?: unknown;\n}): boolean {\n return layer.metadata.sourceKind === \"maplibre-gl-vector-adopted\" && !layer.geojson;\n}\n","/**\n * Pure helpers for the Attribute Form designer (#1322): coercing raw form\n * strings into widget-typed values, validating values against per-field rules\n * (required, numeric bounds, value maps, boolean constraint expressions), and\n * evaluating conditional field visibility.\n *\n * The designer UI (layer properties → Attributes Form) authors an\n * {@link AttributeFormConfig} stored on the layer; the attribute editing\n * surfaces (attribute table inline editor, Field Collection capture form) call\n * these helpers so both enforce the same rules. Everything here is side-effect\n * free so it can be unit tested without a DOM or the app store.\n *\n * Constraint and visibility expressions are MapLibre expressions compiled and\n * evaluated through `expressions.ts` (the same engine as the Expression\n * Builder), always with `expectedType: \"boolean\"`.\n */\nimport type { Feature } from \"geojson\";\nimport { evaluateMapExpression } from \"./expressions\";\nimport type {\n AttributeFormConfig,\n AttributeFormFieldConfig,\n AttributeFormValueMapEntry,\n} from \"./types\";\n\n/** Options threaded to expression evaluation (constraints, visibility). */\nexport interface AttributeFormEvalOptions {\n /** Map zoom for `[\"zoom\"]` in expressions; defaults to 0. */\n zoom?: number;\n /**\n * Real feature backing the properties, when the caller has one — its\n * geometry feeds `[\"geometry-type\"]` and geometry-aware operators. Its\n * `properties` are ignored in favor of the candidate record being\n * validated.\n */\n feature?: Feature;\n}\n\n/** Why a field's value was rejected; codes map to localized messages. */\nexport interface AttributeFormFieldError {\n code: \"required\" | \"number\" | \"range\" | \"valueMap\" | \"constraint\";\n /**\n * For `constraint`: the author's `constraintDescription`, or the\n * expression's own error text when it failed to evaluate.\n */\n message?: string;\n /** Bounds echoed back for `range` error messages. */\n min?: number;\n max?: number;\n}\n\n/** Outcome of validating a whole properties record against a form config. */\nexport interface AttributeFormValidation {\n ok: boolean;\n /** Field name → error for every failing, visible, configured field. */\n errors: Record<string, AttributeFormFieldError>;\n}\n\n/** Look up the configuration for a field, if any. */\nexport function getAttributeFormField(\n form: AttributeFormConfig | undefined,\n field: string,\n): AttributeFormFieldConfig | undefined {\n return form?.fields.find((entry) => entry.field === field);\n}\n\n/** The label attribute forms should show for a configured field. */\nexport function attributeFormFieldLabel(config: AttributeFormFieldConfig): string {\n return config.alias?.trim() || config.field;\n}\n\n/**\n * Parse the designer's value-map text — one entry per line, `value=label` or\n * just `value` — into entries. Blank lines and duplicate values are dropped.\n */\nexport function parseValueMapText(text: string): AttributeFormValueMapEntry[] {\n const seen = new Set<string>();\n const entries: AttributeFormValueMapEntry[] = [];\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const eq = trimmed.indexOf(\"=\");\n const value = (eq >= 0 ? trimmed.slice(0, eq) : trimmed).trim();\n const label = eq >= 0 ? trimmed.slice(eq + 1).trim() : \"\";\n if (!value || seen.has(value)) continue;\n seen.add(value);\n entries.push(label && label !== value ? { value, label } : { value });\n }\n return entries;\n}\n\n/** Serialize value-map entries back into the designer's editable text form. */\nexport function valueMapToText(entries: readonly AttributeFormValueMapEntry[] | undefined): string {\n return (entries ?? [])\n .map((entry) =>\n entry.label && entry.label !== entry.value ? `${entry.value}=${entry.label}` : entry.value,\n )\n .join(\"\\n\");\n}\n\n/** Display label for a stored value under a value-map widget. */\nexport function valueMapLabelFor(config: AttributeFormFieldConfig, value: unknown): string {\n const text = value == null ? \"\" : String(value);\n const entry = config.valueMap?.find((candidate) => candidate.value === text);\n return entry?.label ?? text;\n}\n\n/**\n * Coerce a raw form string into the typed value the widget stores on the\n * feature. An empty/blank string means \"no value\" (`null`). A `number`/`range`\n * string that does not parse is returned verbatim so validation can flag it\n * instead of silently dropping the input.\n */\nexport function coerceAttributeFormValue(config: AttributeFormFieldConfig, raw: string): unknown {\n const trimmed = raw.trim();\n if (trimmed === \"\") return null;\n if (config.widget === \"number\" || config.widget === \"range\") {\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : raw;\n }\n if (config.widget === \"checkbox\") {\n const normalized = trimmed.toLowerCase();\n if (normalized === \"true\") return true;\n if (normalized === \"false\") return false;\n return raw;\n }\n // A value map whose entries are all canonical numeric codes stores numbers,\n // so an edited row keeps the same property type as untouched rows (strict-\n // equality style/filter expressions would otherwise silently miss edited\n // features). Canonical means the string round-trips through Number (\"1\",\n // \"-2\", \"3.5\") — zero-padded or exotic forms like \"01\" or \"1e3\" are\n // identifiers, not numbers, and stay strings so the membership check in\n // validateAttributeFormField keeps matching the entry verbatim.\n if (\n config.widget === \"valueMap\" &&\n config.valueMap?.length &&\n config.valueMap.every((entry) => String(Number(entry.value)) === entry.value)\n ) {\n const parsed = Number(trimmed);\n if (Number.isFinite(parsed)) return parsed;\n }\n // text, date (ISO yyyy-mm-dd string), and valueMap store the string verbatim.\n return trimmed;\n}\n\n/**\n * Whether a configured field is currently shown, given the form's candidate\n * properties. An empty, invalid, or erroring visibility expression fails open\n * (visible) so a typo cannot hide data entry.\n */\nexport function isAttributeFormFieldVisible(\n config: AttributeFormFieldConfig,\n properties: Record<string, unknown>,\n options: AttributeFormEvalOptions = {},\n): boolean {\n const source = config.visibilityExpression?.trim();\n if (!source) return true;\n const result = evaluateMapExpression(source, {\n feature: featureForEvaluation(properties, options.feature),\n zoom: options.zoom,\n expectedType: \"boolean\",\n });\n return result.kind === \"value\" ? result.value === true : true;\n}\n\n/**\n * Validate one configured field against the candidate properties record (the\n * field's own candidate value lives at `properties[config.field]`). Returns\n * `null` when the value passes. Checks run in order: required, widget typing,\n * numeric bounds, value-map membership, then the constraint expression — the\n * constraint sees the whole record, so cross-field rules work.\n */\nexport function validateAttributeFormField(\n config: AttributeFormFieldConfig,\n properties: Record<string, unknown>,\n options: AttributeFormEvalOptions = {},\n): AttributeFormFieldError | null {\n const value = properties[config.field];\n const empty = value == null || value === \"\";\n\n if (empty) {\n // An unchecked checkbox is a valid false-like state, not a missing value —\n // requiring it would block every save with a confusing \"value required\".\n if (config.required && config.widget !== \"checkbox\") return { code: \"required\" };\n } else {\n if (config.widget === \"number\" || config.widget === \"range\") {\n const numeric = typeof value === \"number\" ? value : Number(value);\n if (!Number.isFinite(numeric)) return { code: \"number\" };\n if (\n (config.min != null && numeric < config.min) ||\n (config.max != null && numeric > config.max)\n ) {\n return { code: \"range\", min: config.min, max: config.max };\n }\n }\n if (\n config.widget === \"valueMap\" &&\n config.valueMap?.length &&\n !config.valueMap.some((entry) => entry.value === String(value))\n ) {\n return { code: \"valueMap\" };\n }\n }\n\n const source = config.constraintExpression?.trim();\n if (source) {\n const result = evaluateMapExpression(source, {\n feature: featureForEvaluation(properties, options.feature),\n zoom: options.zoom,\n expectedType: \"boolean\",\n });\n if (result.kind === \"error\") {\n // An unevaluable constraint cannot be verified, so the value is\n // rejected — surfacing the compile/runtime message rather than\n // silently accepting data the author meant to gate.\n return { code: \"constraint\", message: (result.errors ?? []).join(\"; \") };\n }\n if (result.kind === \"value\" && result.value !== true) {\n return {\n code: \"constraint\",\n ...(config.constraintDescription ? { message: config.constraintDescription } : {}),\n };\n }\n }\n\n return null;\n}\n\n/**\n * Validate a whole candidate properties record against a form config. Hidden\n * fields (visibility expression currently false) are skipped entirely — a\n * field the form does not show cannot block a save.\n */\nexport function validateAttributeFormValues(\n form: AttributeFormConfig | undefined,\n properties: Record<string, unknown>,\n options: AttributeFormEvalOptions = {},\n): AttributeFormValidation {\n const errors: Record<string, AttributeFormFieldError> = {};\n for (const config of form?.fields ?? []) {\n if (!isAttributeFormFieldVisible(config, properties, options)) continue;\n const error = validateAttributeFormField(config, properties, options);\n if (error) errors[config.field] = error;\n }\n return { ok: Object.keys(errors).length === 0, errors };\n}\n\n/**\n * Build the feature handed to expression evaluation: the caller's real\n * feature geometry when available (so `[\"geometry-type\"]` works), always with\n * the candidate properties being validated.\n */\nfunction featureForEvaluation(\n properties: Record<string, unknown>,\n feature: Feature | undefined,\n): Feature {\n return {\n type: \"Feature\",\n geometry: feature?.geometry ?? { type: \"Point\", coordinates: [0, 0] },\n ...(feature?.id !== undefined ? { id: feature.id } : {}),\n properties: properties as Feature[\"properties\"],\n };\n}\n","/**\n * Helpers for a layer's user-authored catalog metadata\n * ({@link LayerDescriptiveMetadata}, issue #2858): normalization for the\n * project file, field validation for the Metadata dialog, and the STAC 1.0\n * Item export.\n */\nimport type {\n GeoLibreLayer,\n LayerDescriptiveMetadata,\n LayerMetadataContact,\n LayerMetadataLink,\n LayerMetadataTemporalExtent,\n} from \"./types\";\n\n/** STAC specification version the Item export targets. */\nexport const STAC_VERSION = \"1.0.0\";\n\n/** Schema URL of the STAC processing extension, declared for `processing:lineage`. */\nexport const STAC_PROCESSING_EXTENSION =\n \"https://stac-extensions.github.io/processing/v1.1.0/schema.json\";\n\n/** URL schemes a metadata link (or asset) may use. */\nconst ALLOWED_LINK_PROTOCOLS = new Set([\"http:\", \"https:\", \"ftp:\", \"s3:\", \"gs:\", \"mailto:\"]);\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst DATE_TIME_PATTERN =\n /^(\\d{4})-(\\d{2})-(\\d{2})[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?$/i;\n/** The STAC 1.0 `license` pattern (an SPDX identifier, `various` or `proprietary`). */\nconst STAC_LICENSE_PATTERN = /^[\\w\\-.+]+$/;\n\n/**\n * Trim a value to a non-empty string, or `undefined`.\n *\n * @param value - Any value.\n * @returns The trimmed string, or `undefined` for a non-string or blank value.\n */\nfunction cleanString(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Split comma-separated keyword text into a clean keyword list.\n *\n * @param text - Text such as `\"roads, transport, osm\"`.\n * @returns The trimmed, non-empty keywords with case-insensitive duplicates removed.\n */\nexport function parseMetadataKeywords(text: string): string[] {\n return normalizeKeywords(text.split(\",\"));\n}\n\nfunction normalizeKeywords(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const seen = new Set<string>();\n const keywords: string[] = [];\n for (const entry of value) {\n const keyword = cleanString(entry);\n if (!keyword) continue;\n const key = keyword.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n keywords.push(keyword);\n }\n return keywords;\n}\n\nfunction normalizeContact(value: unknown): LayerMetadataContact | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const contact: LayerMetadataContact = {};\n const name = cleanString(raw.name);\n const email = cleanString(raw.email);\n const organization = cleanString(raw.organization);\n if (name) contact.name = name;\n if (email) contact.email = email;\n if (organization) contact.organization = organization;\n return Object.keys(contact).length > 0 ? contact : undefined;\n}\n\nfunction normalizeTemporalExtent(value: unknown): LayerMetadataTemporalExtent | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const extent: LayerMetadataTemporalExtent = {};\n const start = cleanString(raw.start);\n const end = cleanString(raw.end);\n if (start) extent.start = start;\n if (end) extent.end = end;\n return Object.keys(extent).length > 0 ? extent : undefined;\n}\n\nfunction normalizeLinks(value: unknown): LayerMetadataLink[] {\n if (!Array.isArray(value)) return [];\n const links: LayerMetadataLink[] = [];\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) continue;\n const raw = entry as Record<string, unknown>;\n const href = cleanString(raw.href);\n // A link is its address: a row with only a label is an unfinished edit.\n if (!href) continue;\n const link: LayerMetadataLink = { href };\n const rel = cleanString(raw.rel);\n const title = cleanString(raw.title);\n if (rel) link.rel = rel;\n if (title) link.title = title;\n links.push(link);\n }\n return links;\n}\n\n/**\n * Normalize a raw descriptive-metadata value (from a project file, a plugin or\n * the Metadata dialog): keep only string fields, trim them, and drop empty\n * fields, empty keywords and links without an address.\n *\n * Values are not validated here — a malformed email or date in a hand-edited\n * file is kept so the dialog can show and fix it rather than silently lose it.\n *\n * @param value - Any value.\n * @returns The cleaned record, or `undefined` when nothing is left, so an\n * empty record is never written to a project.\n */\nexport function normalizeLayerDescriptiveMetadata(\n value: unknown,\n): LayerDescriptiveMetadata | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const metadata: LayerDescriptiveMetadata = {};\n for (const key of [\"title\", \"abstract\", \"license\", \"attribution\", \"lineage\"] as const) {\n const text = cleanString(raw[key]);\n if (text) metadata[key] = text;\n }\n const keywords = normalizeKeywords(raw.keywords);\n if (keywords.length > 0) metadata.keywords = keywords;\n const contact = normalizeContact(raw.contact);\n if (contact) metadata.contact = contact;\n const temporalExtent = normalizeTemporalExtent(raw.temporalExtent);\n if (temporalExtent) metadata.temporalExtent = temporalExtent;\n const links = normalizeLinks(raw.links);\n if (links.length > 0) metadata.links = links;\n return Object.keys(metadata).length > 0 ? metadata : undefined;\n}\n\n/**\n * Whether a string is a plausible email address (`local@domain.tld`).\n *\n * @param value - The candidate address.\n * @returns `true` when it has the shape of an email address.\n */\nexport function isValidMetadataEmail(value: string): boolean {\n return EMAIL_PATTERN.test(value.trim());\n}\n\n/**\n * Whether a string is an absolute URL with a scheme a metadata link may use\n * (http, https, ftp, s3, gs, mailto). Script and data URLs are rejected.\n *\n * @param value - The candidate URL.\n * @returns `true` when it parses as an allowed absolute URL.\n */\nexport function isValidMetadataUrl(value: string): boolean {\n try {\n const url = new URL(value.trim());\n return ALLOWED_LINK_PROTOCOLS.has(url.protocol);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether the calendar fields name a real day (rejecting e.g. February 30).\n *\n * @param year - Four-digit year.\n * @param month - Month, 1-12.\n * @param day - Day of month.\n * @returns `true` when the date exists.\n */\nfunction isRealDate(year: number, month: number, day: number): boolean {\n // setUTCFullYear, not Date.UTC: the latter maps years 0-99 to 1900-1999.\n const date = new Date(0);\n date.setUTCFullYear(year, month - 1, day);\n return (\n date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day\n );\n}\n\n/**\n * Whether a string is an ISO 8601 date (`YYYY-MM-DD`) or date-time\n * (`YYYY-MM-DDTHH:MM[:SS[.fff]][Z|±HH:MM]`) naming a real instant.\n *\n * @param value - The candidate date.\n * @returns `true` when it is a valid ISO 8601 date or date-time.\n */\nexport function isValidMetadataDate(value: string): boolean {\n const text = value.trim();\n const dateOnly = DATE_ONLY_PATTERN.exec(text);\n if (dateOnly) return isRealDate(Number(dateOnly[1]), Number(dateOnly[2]), Number(dateOnly[3]));\n const dateTime = DATE_TIME_PATTERN.exec(text);\n if (!dateTime) return false;\n if (!isRealDate(Number(dateTime[1]), Number(dateTime[2]), Number(dateTime[3]))) return false;\n const hours = Number(dateTime[4]);\n const minutes = Number(dateTime[5]);\n const seconds = dateTime[6] === undefined ? 0 : Number(dateTime[6]);\n return hours < 24 && minutes < 60 && seconds < 60;\n}\n\n/**\n * Convert a validated metadata date to an RFC 3339 UTC date-time, as STAC\n * requires. A date-only bound covers its whole day: a start becomes the first\n * second of the day, an end the last. A date-time without an offset is read\n * as UTC so the export does not depend on the exporting machine's time zone.\n *\n * @param value - A date accepted by {@link isValidMetadataDate}.\n * @param bound - Whether the value starts or ends an interval.\n * @returns The RFC 3339 date-time, or `null` for an invalid date.\n */\nexport function metadataDateToRfc3339(value: string, bound: \"start\" | \"end\"): string | null {\n const text = value.trim();\n if (!isValidMetadataDate(text)) return null;\n if (DATE_ONLY_PATTERN.test(text)) {\n return `${text}T${bound === \"start\" ? \"00:00:00\" : \"23:59:59\"}Z`;\n }\n // A compact `+HHMM` offset passes validation but is not an ECMAScript\n // date-time format, so spell it `+HH:MM` before parsing. The pattern is\n // case-insensitive, but ECMAScript only promises to read an upper-case `T`\n // and `Z`, so upper-case the text first.\n const normalized = text\n .toUpperCase()\n .replace(\" \", \"T\")\n .replace(/([+-]\\d{2})(\\d{2})$/, \"$1:$2\");\n const hasOffset = /(Z|[+-]\\d{2}:\\d{2})$/.test(normalized);\n const parsed = new Date(hasOffset ? normalized : `${normalized}Z`);\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();\n}\n\n/** Why a metadata field failed {@link validateLayerDescriptiveMetadata}. */\nexport type LayerMetadataIssueCode = \"email\" | \"url\" | \"date\" | \"dateOrder\";\n\n/** One validation failure, addressed by a dotted field path. */\nexport interface LayerMetadataIssue {\n /**\n * The failing field: `contact.email`, `temporalExtent.start`,\n * `temporalExtent.end`, or `links.<index>.href`.\n */\n field: string;\n code: LayerMetadataIssueCode;\n}\n\n/**\n * Validate the fields of a descriptive-metadata record that have a format:\n * the contact email, the temporal-extent dates (and their order), and the\n * link URLs. Free-text fields are not checked.\n *\n * @param metadata - The record to check (normalized or a dialog draft).\n * @returns Every failure found; empty when the record is valid.\n */\nexport function validateLayerDescriptiveMetadata(\n metadata: LayerDescriptiveMetadata | undefined,\n): LayerMetadataIssue[] {\n const issues: LayerMetadataIssue[] = [];\n if (!metadata) return issues;\n const email = cleanString(metadata.contact?.email);\n if (email && !isValidMetadataEmail(email)) issues.push({ field: \"contact.email\", code: \"email\" });\n const start = cleanString(metadata.temporalExtent?.start);\n const end = cleanString(metadata.temporalExtent?.end);\n const startValid = start ? isValidMetadataDate(start) : true;\n const endValid = end ? isValidMetadataDate(end) : true;\n if (!startValid) issues.push({ field: \"temporalExtent.start\", code: \"date\" });\n if (!endValid) issues.push({ field: \"temporalExtent.end\", code: \"date\" });\n if (start && end && startValid && endValid) {\n const startTime = Date.parse(metadataDateToRfc3339(start, \"start\") ?? \"\");\n const endTime = Date.parse(metadataDateToRfc3339(end, \"end\") ?? \"\");\n if (startTime > endTime) issues.push({ field: \"temporalExtent.end\", code: \"dateOrder\" });\n }\n (metadata.links ?? []).forEach((link, index) => {\n const href = cleanString(link?.href);\n if (href && !isValidMetadataUrl(href)) {\n issues.push({ field: `links.${index}.href`, code: \"url\" });\n }\n });\n return issues;\n}\n\n/** A STAC 1.0 link object. */\nexport interface StacLink {\n href: string;\n rel: string;\n type?: string;\n title?: string;\n}\n\n/** A STAC 1.0 asset object. */\nexport interface StacAsset {\n href: string;\n type?: string;\n title?: string;\n roles?: string[];\n}\n\n/** A STAC 1.0 provider object. */\nexport interface StacProvider {\n name: string;\n description?: string;\n roles?: string[];\n url?: string;\n}\n\n/** The STAC 1.0 Item {@link buildLayerStacItem} produces. */\nexport interface StacItem {\n type: \"Feature\";\n stac_version: string;\n stac_extensions: string[];\n id: string;\n geometry: Record<string, unknown> | null;\n bbox?: [number, number, number, number];\n properties: Record<string, unknown> & { datetime: string | null };\n links: StacLink[];\n assets: Record<string, StacAsset>;\n}\n\n/** Options for {@link buildLayerStacItem}. */\nexport interface LayerStacItemOptions {\n /** The layer's extent in EPSG:4326 as `[west, south, east, north]`. */\n bbox?: readonly number[] | null;\n /** Where the layer's data can be fetched from, when it has a source URL. */\n assetHref?: string | null;\n /** Clock used for `datetime` when no temporal extent is recorded. */\n now?: Date;\n}\n\n/**\n * Validate a WGS84 bounding box.\n *\n * @param bbox - Candidate `[west, south, east, north]`.\n * @returns The box, or `null` when it is not four finite in-range numbers.\n */\nfunction validBbox(\n bbox: readonly number[] | null | undefined,\n): [number, number, number, number] | null {\n if (!bbox || bbox.length !== 4) return null;\n if (!bbox.every((value) => typeof value === \"number\" && Number.isFinite(value))) return null;\n const [west, south, east, north] = bbox;\n if (south < -90 || north > 90 || south > north) return null;\n if (west < -180 || west > 180 || east < -180 || east > 180) return null;\n return [west, south, east, north];\n}\n\n/**\n * The GeoJSON geometry covering a bounding box: a Point for a degenerate box,\n * a MultiPolygon split at the antimeridian when `west > east` (the STAC\n * convention for a box crossing it), otherwise a Polygon.\n *\n * @param bbox - A box accepted by {@link validBbox}.\n * @returns The footprint geometry.\n */\nfunction bboxGeometry([west, south, east, north]: [number, number, number, number]): Record<\n string,\n unknown\n> {\n if (west === east && south === north) return { type: \"Point\", coordinates: [west, south] };\n // Degenerate on one axis only (e.g. points sharing one longitude): a line,\n // not a zero-area polygon.\n if (west === east || south === north) {\n return {\n type: \"LineString\",\n coordinates: [\n [west, south],\n [east, north],\n ],\n };\n }\n const ring = (w: number, e: number) => [\n [w, south],\n [e, south],\n [e, north],\n [w, north],\n [w, south],\n ];\n if (west > east) {\n return { type: \"MultiPolygon\", coordinates: [[ring(west, 180)], [ring(-180, east)]] };\n }\n return { type: \"Polygon\", coordinates: [ring(west, east)] };\n}\n\n/** Media types by data-file extension, for the Item's data asset. */\nconst ASSET_MEDIA_TYPES: Record<string, string> = {\n geojson: \"application/geo+json\",\n json: \"application/json\",\n tif: \"image/tiff; application=geotiff\",\n tiff: \"image/tiff; application=geotiff\",\n parquet: \"application/vnd.apache.parquet\",\n geoparquet: \"application/vnd.apache.parquet\",\n fgb: \"application/vnd.flatgeobuf\",\n pmtiles: \"application/vnd.pmtiles\",\n gpkg: \"application/geopackage+sqlite3\",\n kml: \"application/vnd.google-earth.kml+xml\",\n kmz: \"application/vnd.google-earth.kmz\",\n csv: \"text/csv\",\n zip: \"application/zip\",\n las: \"application/vnd.las\",\n laz: \"application/vnd.laszip\",\n zarr: \"application/vnd+zarr\",\n};\n\n/**\n * Guess the media type of a layer's data asset from the URL's extension, or\n * the layer type when the URL has none (a COG is flagged cloud-optimized).\n *\n * @param href - The asset URL.\n * @param layerType - The layer's type.\n * @returns The media type, or `undefined` when it cannot be told.\n */\nexport function stacAssetMediaType(\n href: string,\n layerType: GeoLibreLayer[\"type\"],\n): string | undefined {\n let path = href;\n try {\n path = new URL(href).pathname;\n } catch {\n // Not an absolute URL: read the extension off the raw text.\n }\n const extension = /\\.([a-z0-9]+)$/i.exec(path.replace(/\\/+$/, \"\"))?.[1]?.toLowerCase();\n if (layerType === \"cog\" && (extension === \"tif\" || extension === \"tiff\" || !extension)) {\n return \"image/tiff; application=geotiff; profile=cloud-optimized\";\n }\n if (extension && ASSET_MEDIA_TYPES[extension]) return ASSET_MEDIA_TYPES[extension];\n if (layerType === \"geojson\" && !extension) return \"application/geo+json\";\n return undefined;\n}\n\n/**\n * Build a STAC 1.0 Item describing a layer from its descriptive metadata.\n *\n * Mapping: `id` is the layer id; `geometry`/`bbox` come from the WGS84 extent\n * (a `null` geometry, as STAC allows, when the extent is unknown); `title`\n * falls back to the layer name; `abstract` becomes `description`; the contact\n * becomes a `producer` provider; `lineage` becomes `processing:lineage` (with\n * the processing extension declared); the temporal extent becomes `datetime`\n * or `start_datetime`/`end_datetime` (with `datetime` set to the export time\n * when none is recorded, since STAC requires one); links keep their `rel`\n * (default `related`); and a source URL becomes the `data` asset.\n *\n * A license that is not an SPDX-style identifier (STAC 1.0's pattern) is\n * exported as `proprietary`, with the original text kept in\n * `geolibre:license`; the attribution is kept in `geolibre:attribution`.\n *\n * @param layer - The layer to describe.\n * @param options - Extent, asset URL and clock; see {@link LayerStacItemOptions}.\n * @returns The STAC Item, ready to `JSON.stringify`.\n */\nexport function buildLayerStacItem(\n layer: Pick<GeoLibreLayer, \"id\" | \"name\" | \"type\" | \"descriptiveMetadata\">,\n options: LayerStacItemOptions = {},\n): StacItem {\n const metadata = normalizeLayerDescriptiveMetadata(layer.descriptiveMetadata) ?? {};\n const now = options.now ?? new Date();\n const properties: StacItem[\"properties\"] = { datetime: null };\n properties.title = metadata.title ?? layer.name;\n if (metadata.abstract) properties.description = metadata.abstract;\n if (metadata.keywords?.length) properties.keywords = [...metadata.keywords];\n\n const start = metadata.temporalExtent?.start\n ? metadataDateToRfc3339(metadata.temporalExtent.start, \"start\")\n : null;\n const end = metadata.temporalExtent?.end\n ? metadataDateToRfc3339(metadata.temporalExtent.end, \"end\")\n : null;\n // Compared as instants: the two bounds may be spelled differently\n // (`…T00:00:00Z` from a date, `…T00:00:00.000Z` from a date-time).\n if (start && end && Date.parse(start) !== Date.parse(end)) {\n properties.start_datetime = start;\n properties.end_datetime = end;\n } else {\n // STAC needs a datetime unless both interval bounds are known: an instant\n // or a one-sided interval exports its known bound, and a layer with no\n // temporal extent is stamped with the export time.\n properties.datetime = start ?? end ?? now.toISOString();\n }\n\n if (metadata.license) {\n if (STAC_LICENSE_PATTERN.test(metadata.license)) {\n properties.license = metadata.license;\n } else {\n properties.license = \"proprietary\";\n properties[\"geolibre:license\"] = metadata.license;\n }\n }\n const contact = metadata.contact;\n const providerName = contact?.organization ?? contact?.name ?? contact?.email;\n if (contact && providerName) {\n const person = [\n contact.organization && contact.name ? contact.name : undefined,\n contact.email,\n ].filter(Boolean);\n const provider: StacProvider = { name: providerName, roles: [\"producer\"] };\n if (person.length > 0) provider.description = `Contact: ${person.join(\", \")}`;\n properties.providers = [provider];\n }\n if (metadata.attribution) properties[\"geolibre:attribution\"] = metadata.attribution;\n\n const extensions: string[] = [];\n if (metadata.lineage) {\n properties[\"processing:lineage\"] = metadata.lineage;\n extensions.push(STAC_PROCESSING_EXTENSION);\n }\n\n const links: StacLink[] = (metadata.links ?? [])\n .filter((link) => isValidMetadataUrl(link.href))\n .map((link) => ({\n href: link.href,\n rel: link.rel ?? \"related\",\n ...(link.title ? { title: link.title } : {}),\n }));\n\n const assets: Record<string, StacAsset> = {};\n const assetHref = cleanString(options.assetHref);\n if (assetHref) {\n // A tile template (`…/{z}/{x}/{y}.png`) cannot be fetched as-is, so it\n // is published as a `tiles` asset rather than a dereferenceable `data` one.\n const templated = /\\{[a-z]+\\}/i.test(assetHref);\n const type = templated ? undefined : stacAssetMediaType(assetHref, layer.type);\n assets[templated ? \"tiles\" : \"data\"] = {\n href: assetHref,\n ...(type ? { type } : {}),\n title: properties.title as string,\n roles: [templated ? \"tiles\" : \"data\"],\n };\n }\n\n const bbox = validBbox(options.bbox);\n return {\n type: \"Feature\",\n stac_version: STAC_VERSION,\n stac_extensions: extensions,\n id: layer.id,\n geometry: bbox ? bboxGeometry(bbox) : null,\n ...(bbox ? { bbox } : {}),\n properties,\n links,\n assets,\n };\n}\n","/**\n * Pure helpers for the popup and tooltip designer (#2113): resolving which\n * fields a layer's Identify popup (or hover tooltip) shows and in what order,\n * and formatting each value the way the author asked for.\n *\n * The designer UI (Style panel → Popup) authors a {@link LayerPopupConfig} on\n * the layer; `MapCanvas` renders the popup from these helpers so the click\n * popup and the hover tooltip agree on labels, order and formatting.\n * Everything here is side-effect free so it can be unit tested without a DOM\n * or the app store.\n *\n * Two rules the rest of the app depends on:\n *\n * - {@link GeoLibreLayer.fieldVisibility} is authoritative. A field marked\n * `\"hidden\"` or `\"excluded\"` never reaches a popup, even when a popup config\n * names it explicitly — the config selects from what is visible, it cannot\n * re-expose what the author hid.\n * - A layer with no config behaves exactly as it did before the designer\n * existed: every property, in the feature's own key order, unformatted.\n */\nimport type { Feature } from \"geojson\";\nimport { compileFeatureExpression, type CompiledFeatureExpression } from \"./expressions\";\nimport { PHOTO_FULL_PROPERTY } from \"./photo\";\nimport type {\n FieldVisibility,\n LayerPopupConfig,\n PopupDateFormat,\n PopupFieldConfig,\n PopupFieldFormat,\n PopupFieldKind,\n} from \"./types\";\n\n/** Match an inline base64 raster image (excludes SVG, which can carry scripts). */\nconst INLINE_IMAGE_DATA_URL = /^data:image\\/(?!svg)[\\w.+-]+;base64,/i;\n\n/** One resolved popup row, ready for the renderer. */\nexport interface PopupRow {\n /** Feature property key. */\n field: string;\n /** Label to print in the key column. */\n label: string;\n /** The raw property value, for kinds that need it (image `src`, link `href`). */\n value: unknown;\n /** The formatted value text. */\n text: string;\n /** How the renderer should draw the value. */\n kind: PopupFieldKind;\n /** Link text for a `\"link\"` row; falls back to the value. */\n linkLabel?: string;\n}\n\n/**\n * A trimmed non-empty string, or `undefined` for anything else.\n *\n * Popup configs arrive from untrusted JSON — a hand-edited `.geolibre.json`, an\n * imported layer-library bundle, an MCP-authored project — so a field typed as\n * `string` may hold a number, an object or null. `value.trim()` on one of those\n * throws and takes the whole popup render with it, so every string read here\n * goes through this.\n */\nfunction trimmedString(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/** Whether the Identify popup should open for a layer. Default: yes. */\nexport function isPopupClickEnabled(popup: LayerPopupConfig | undefined): boolean {\n return popup?.click !== false;\n}\n\n/** Whether a hover tooltip should follow the pointer. Default: no. */\nexport function isPopupHoverEnabled(popup: LayerPopupConfig | undefined): boolean {\n return popup?.hover === true;\n}\n\n/**\n * Bounds for {@link LayerPopupConfig.maxWidth}, in CSS pixels. The floor is\n * the popup's own minimum width (`min-w-[18rem]`), below which the setting\n * would be overruled by the layout and read as broken; the ceiling keeps a\n * hand-edited project from authoring a popup that blankets the map.\n */\nexport const POPUP_MAX_WIDTH_RANGE = { min: 288, max: 1200 } as const;\n\n/**\n * Bounds for {@link LayerPopupConfig.imageHeight}, in CSS pixels. Below the\n * floor a thumbnail carries no information; the ceiling matches\n * {@link POPUP_MAX_WIDTH_RANGE}, since a taller picture than that is what the\n * lightbox is for.\n */\nexport const POPUP_IMAGE_HEIGHT_RANGE = { min: 40, max: 1200 } as const;\n\n/**\n * Coerce an author-supplied pixel size to a usable number, or `undefined`.\n *\n * Popup configs arrive from untrusted JSON, so the value may be a string, null\n * or NaN. Anything that is not a finite positive number means \"unset\" (keep\n * the default) rather than an error that would take the popup render down,\n * and a number outside `range` is clamped rather than dropped — a project\n * asking for a 4000px popup wants the widest one it can have.\n */\nfunction pixelSize(\n value: unknown,\n range: { readonly min: number; readonly max: number },\n): number | undefined {\n const size = typeof value === \"number\" ? value : Number(trimmedString(value) ?? NaN);\n if (!Number.isFinite(size) || size <= 0) return undefined;\n return Math.round(Math.max(range.min, Math.min(range.max, size)));\n}\n\n/**\n * The widest the click popup may draw for a layer, in CSS pixels, or\n * `undefined` to keep the renderer's default.\n */\nexport function resolvePopupMaxWidth(popup: LayerPopupConfig | undefined): number | undefined {\n return pixelSize(popup?.maxWidth, POPUP_MAX_WIDTH_RANGE);\n}\n\n/**\n * The tallest an `\"image\"` field may draw inside the popup, in CSS pixels, or\n * `undefined` to keep the stylesheet's default.\n */\nexport function resolvePopupImageHeight(popup: LayerPopupConfig | undefined): number | undefined {\n return pixelSize(popup?.imageHeight, POPUP_IMAGE_HEIGHT_RANGE);\n}\n\n/** The label a popup shows for a configured field. */\nexport function popupFieldLabel(config: PopupFieldConfig): string {\n return trimmedString(config.label) ?? config.field;\n}\n\n/**\n * Property keys that never belong in a popup: GeoLibre's own internal columns\n * and the full-resolution companion of a photo thumbnail (an internal twin of\n * a value already shown, and often multiple megabytes of data URL).\n */\nexport function isInternalPopupField(key: string): boolean {\n return key === PHOTO_FULL_PROPERTY || key.startsWith(\"__geolibre_\");\n}\n\n/**\n * The property keys a popup may show, before any popup config narrows them:\n * the feature's own keys minus the internal ones and anything the author\n * marked `\"hidden\"` or `\"excluded\"`.\n */\nexport function visiblePopupFields(\n properties: Record<string, unknown>,\n fieldVisibility?: Record<string, FieldVisibility>,\n): string[] {\n return Object.keys(properties).filter(\n (key) => !isInternalPopupField(key) && !fieldVisibility?.[key],\n );\n}\n\n/** Options for {@link formatPopupValue} and {@link resolvePopupRows}. */\nexport interface PopupFormatOptions {\n /**\n * BCP 47 tag for number/date formatting. Omitted means the runtime default,\n * which is what the browser uses for every other locale-aware rendering.\n */\n locale?: string;\n}\n\nfunction formatNumber(value: number, format: PopupFieldFormat, locale?: string): string {\n const options: Intl.NumberFormatOptions = {\n useGrouping: format.thousands === true,\n };\n if (format.decimals != null && Number.isFinite(format.decimals)) {\n const digits = Math.max(0, Math.min(20, Math.trunc(format.decimals)));\n options.minimumFractionDigits = digits;\n options.maximumFractionDigits = digits;\n }\n return new Intl.NumberFormat(locale, options).format(value);\n}\n\n/**\n * Coerce a property value to a Date. A number is epoch milliseconds (what a\n * GeoJSON timestamp column normally holds); a string goes through `Date`'s own\n * parser. Anything that does not parse comes back as `null` so the caller can\n * fall back to the raw text rather than printing \"Invalid Date\".\n */\nfunction toDate(value: unknown): Date | null {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;\n if (typeof value === \"number\" && Number.isFinite(value)) {\n const fromEpoch = new Date(value);\n return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch;\n }\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n }\n return null;\n}\n\nfunction formatDate(date: Date, dateFormat: PopupDateFormat, locale?: string): string {\n switch (dateFormat) {\n case \"datetime\":\n return date.toLocaleString(locale);\n case \"time\":\n return date.toLocaleTimeString(locale);\n case \"iso\":\n return date.toISOString();\n case \"year\":\n // Local, like the date/datetime/time cases above — only `iso` is\n // deliberately UTC, because that is what the format itself means. Reading\n // the year in UTC while the neighbouring formats read local would make\n // \"Year\" disagree with \"Date\" for the same timestamp near a year\n // boundary (2026-01-01T02:00:00Z is Dec 31 2025 in UTC-8).\n return String(date.getFullYear());\n case \"date\":\n default:\n return date.toLocaleDateString(locale);\n }\n}\n\n/** Stringify a value the way the untyped popup always has. */\nexport function stringifyPopupValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\n/**\n * Render one value as text under a field's kind and format. `\"auto\"` and\n * `\"image\"` values are returned verbatim (the renderer draws them itself);\n * every other kind is stringified, formatted, then wrapped in the configured\n * prefix/suffix.\n */\nexport function formatPopupValue(\n value: unknown,\n config: Pick<PopupFieldConfig, \"kind\" | \"format\"> = {},\n options: PopupFormatOptions = {},\n): string {\n const kind = config.kind ?? \"auto\";\n const format = config.format ?? {};\n if (kind === \"auto\" || kind === \"image\") return stringifyPopupValue(value);\n\n // A missing value stays missing under every kind. Without this a `number`\n // field would coerce null (and \"\") through `Number(\"\")` to a confident 0,\n // reporting a population of zero for a city that simply has no figure.\n if (value == null || (typeof value === \"string\" && value.trim() === \"\")) return \"\";\n\n let body: string;\n if (kind === \"number\") {\n const numeric = typeof value === \"number\" ? value : Number(stringifyPopupValue(value));\n body = Number.isFinite(numeric)\n ? formatNumber(numeric, format, options.locale)\n : stringifyPopupValue(value);\n } else if (kind === \"date\") {\n const date = toDate(value);\n body = date\n ? formatDate(date, format.dateFormat ?? \"date\", options.locale)\n : stringifyPopupValue(value);\n } else {\n body = stringifyPopupValue(value);\n }\n\n // An empty value gets no affixes: \"$\" alone, or a bare \" km\", reads as data\n // that is present when it is not.\n if (body === \"\") return \"\";\n return `${format.prefix ?? \"\"}${body}${format.suffix ?? \"\"}`;\n}\n\n/** Whether a row would draw as a picture rather than as text. */\nfunction rendersAsImage(kind: PopupFieldKind, value: unknown): boolean {\n return kind === \"image\" || (kind === \"auto\" && isInlineImageValue(value));\n}\n\n/**\n * The properties a popup may read, with the author's hidden and excluded\n * fields and GeoLibre's internal columns removed.\n *\n * Expressions get this rather than the raw record. `[\"get\", \"ssn\"]` in a title\n * or body would otherwise reach a column the author hid, printing it into the\n * popup — and onto the hover tooltip, which needs no click at all. The rest of\n * this module already honors `fieldVisibility` field by field; an expression\n * has no field list to filter, so the filtering has to happen to its input.\n */\nexport function visiblePopupProperties(\n properties: Record<string, unknown>,\n fieldVisibility?: Record<string, FieldVisibility>,\n): Record<string, unknown> {\n const visible: Record<string, unknown> = {};\n for (const key of visiblePopupFields(properties, fieldVisibility)) {\n visible[key] = properties[key];\n }\n return visible;\n}\n\n/** Options for {@link resolvePopupRows}. */\nexport interface ResolvePopupRowsOptions extends PopupFormatOptions {\n popup?: LayerPopupConfig;\n fieldVisibility?: Record<string, FieldVisibility>;\n /** Resolve the hover subset (fields flagged `hover`) instead of the full list. */\n hover?: boolean;\n}\n\n/**\n * The rows a popup (or hover tooltip) should render for one feature's\n * properties.\n *\n * With no `popup.fields`, every visible property is returned in the feature's\n * own key order and left untyped, which is exactly what the popup did before\n * the designer existed. With a list, only the listed fields appear, in the\n * listed order, under their labels and formats — and a listed field the\n * feature does not carry is skipped rather than printed empty, so one config\n * can cover a layer whose features have ragged properties.\n *\n * `hover: true` narrows the list to fields flagged `hover`. A hover request\n * with no flagged field returns nothing: an author who turned the tooltip on\n * without choosing a field gets no tip rather than the whole table following\n * the pointer.\n */\nexport function resolvePopupRows(\n properties: Record<string, unknown>,\n options: ResolvePopupRowsOptions = {},\n): PopupRow[] {\n const { popup, fieldVisibility, hover = false, locale } = options;\n // Untrusted JSON again: `fields` may be absent, a non-array, or hold entries\n // whose `field` is not a string. Anything unusable is dropped here so the\n // loop below can treat every entry as a real field name.\n const configured = (Array.isArray(popup?.fields) ? popup.fields : []).filter(\n (config): config is PopupFieldConfig =>\n Boolean(config) && typeof (config as PopupFieldConfig).field === \"string\",\n );\n\n if (configured.length === 0) {\n if (hover) return [];\n return visiblePopupFields(properties, fieldVisibility).map((field) => ({\n field,\n label: field,\n value: properties[field],\n text: stringifyPopupValue(properties[field]),\n kind: \"auto\" as const,\n }));\n }\n\n const rows: PopupRow[] = [];\n const seen = new Set<string>();\n for (const config of configured) {\n if (hover && config.hover !== true) continue;\n // fieldVisibility wins: a popup config can order and relabel what is\n // visible, it can never re-expose a field the author hid or excluded.\n if (fieldVisibility?.[config.field]) continue;\n if (isInternalPopupField(config.field)) continue;\n if (!(config.field in properties)) continue;\n if (seen.has(config.field)) continue;\n const value = properties[config.field];\n // A hover tip is a glance, and it re-renders on every animation frame the\n // pointer is over the feature. An image row has no useful text form — its\n // value is a multi-kilobyte data URL — so it would print that URL as the\n // tip's body, over and over. Skip it and let the click popup show the\n // picture.\n if (hover && rendersAsImage(config.kind ?? \"auto\", value)) continue;\n seen.add(config.field);\n rows.push({\n field: config.field,\n label: popupFieldLabel(config),\n value,\n text: formatPopupValue(value, config, { locale }),\n kind: config.kind ?? \"auto\",\n ...(trimmedString(config.format?.linkLabel)\n ? { linkLabel: trimmedString(config.format?.linkLabel) }\n : {}),\n });\n }\n return rows;\n}\n\n/**\n * Compiled title/body expressions, keyed by zoom and source.\n *\n * `createHoverTooltipElement` calls {@link resolvePopupTitle} once per\n * animation frame for as long as the pointer sits on a hovered feature, and\n * compiling means a `JSON.parse` plus a full style-spec validation. Without\n * this the same unchanged source string would be re-parsed sixty times a\n * second. Zoom is part of the key because a compiled expression bakes it in\n * for `[\"zoom\"]`; it is constant through a hover, so the cache still hits.\n */\nconst EXPRESSION_CACHE = new Map<string, CompiledFeatureExpression>();\n/** Bound on the cache, so a project full of layers cannot grow it without end. */\nconst EXPRESSION_CACHE_LIMIT = 64;\n\nfunction compiledPopupExpression(source: string, zoom: number): CompiledFeatureExpression {\n const key = `${zoom}\\u0000${source}`;\n const cached = EXPRESSION_CACHE.get(key);\n if (cached) return cached;\n const compiled = compileFeatureExpression(source, { zoom });\n // Map iterates in insertion order, so the first key is the oldest entry.\n if (EXPRESSION_CACHE.size >= EXPRESSION_CACHE_LIMIT) {\n const oldest = EXPRESSION_CACHE.keys().next().value;\n if (oldest !== undefined) EXPRESSION_CACHE.delete(oldest);\n }\n EXPRESSION_CACHE.set(key, compiled);\n return compiled;\n}\n\n/**\n * Evaluate a popup expression against a feature's properties, or return\n * `undefined` when it does not compile or throws. Both failures mean the same\n * thing to every caller here: fall back to what the popup would show without\n * the expression.\n */\nfunction evaluatePopupExpression(\n source: string,\n properties: Record<string, unknown>,\n options: PopupExpressionOptions,\n): unknown {\n const compiled = compiledPopupExpression(source, options.zoom ?? 0);\n if (!compiled.ok || !compiled.evaluate) return undefined;\n try {\n return compiled.evaluate(\n featureFor(visiblePopupProperties(properties, options.fieldVisibility), options.feature),\n );\n } catch {\n return undefined;\n }\n}\n\n/** Build the synthetic feature an expression is evaluated against. */\nfunction featureFor(properties: Record<string, unknown>, feature?: Feature | null): Feature {\n if (feature) return { ...feature, properties };\n return { type: \"Feature\", geometry: null as never, properties };\n}\n\n/** Options for {@link resolvePopupTitle} and {@link resolvePopupBody}. */\nexport interface PopupExpressionOptions {\n /**\n * The author's field visibility, so an expression cannot read a hidden or\n * excluded column. Omitting it means nothing is hidden.\n */\n fieldVisibility?: Record<string, FieldVisibility>;\n /** The real feature, when the caller has one — feeds `[\"geometry-type\"]`. */\n feature?: Feature | null;\n /** Map zoom for `[\"zoom\"]`; defaults to 0. */\n zoom?: number;\n}\n\n/**\n * The heading a popup should show: the title expression's value, else the\n * title field's value, else the layer name. An expression that fails to\n * compile or evaluate, and a title field that is empty or hidden, both fall\n * back to the layer name rather than leaving the popup headless.\n */\nexport function resolvePopupTitle(\n layerName: string,\n properties: Record<string, unknown>,\n popup: LayerPopupConfig | undefined,\n options: PopupExpressionOptions = {},\n): string {\n return resolveConfiguredPopupTitle(properties, popup, options) ?? layerName;\n}\n\n/**\n * The title the author's own configuration produced, or `null` when there is\n * none — no title field or expression, or one that evaluated to nothing, or a\n * field the author hid.\n *\n * {@link resolvePopupTitle} is this plus the layer-name fallback. The hover\n * tooltip needs the two apart: a tip with no fields and no configured title\n * would be a box repeating the layer name, so it is suppressed — but that\n * decision has to rest on whether a title was *configured*, not on whether the\n * resolved text happens to equal the layer name. A city layer named \"Olympia\"\n * whose feature is also called \"Olympia\" is a real title, not a fallback.\n */\nexport function resolveConfiguredPopupTitle(\n properties: Record<string, unknown>,\n popup: LayerPopupConfig | undefined,\n options: PopupExpressionOptions = {},\n): string | null {\n const source = trimmedString(popup?.titleExpression);\n if (source) {\n const text = stringifyPopupValue(evaluatePopupExpression(source, properties, options)).trim();\n if (text) return text;\n }\n const field = trimmedString(popup?.titleField);\n if (field && !options.fieldVisibility?.[field] && !isInternalPopupField(field)) {\n const text = stringifyPopupValue(properties[field]).trim();\n if (text) return text;\n }\n return null;\n}\n\n/**\n * The popup body text when the author supplied a body expression, or `null`\n * when there is none (or it failed) and the field rows should render instead.\n */\nexport function resolvePopupBody(\n properties: Record<string, unknown>,\n popup: LayerPopupConfig | undefined,\n options: PopupExpressionOptions = {},\n): string | null {\n const source = trimmedString(popup?.bodyExpression);\n if (!source) return null;\n const preview = evaluatePopupExpression(source, properties, options);\n if (preview === undefined) return null;\n const text = stringifyPopupValue(preview);\n return text.trim() ? text : null;\n}\n\n/**\n * Whether a value should render as an inline image thumbnail under `\"auto\"`.\n * Kept here so the renderer and the designer's preview agree on what counts.\n */\nexport function isInlineImageValue(value: unknown): value is string {\n return typeof value === \"string\" && INLINE_IMAGE_DATA_URL.test(value);\n}\n\n/**\n * Whether a URL is safe to put in a popup `href`/`src`. Only http(s) and\n * inline raster data URLs: a `javascript:` or `data:text/html` value coming\n * from an untrusted GeoJSON must never become a live link.\n */\nexport function isSafePopupUrl(value: unknown, allowDataImage = false): value is string {\n if (typeof value !== \"string\") return false;\n const trimmed = value.trim();\n if (/^https?:\\/\\//i.test(trimmed)) return true;\n return allowDataImage && INLINE_IMAGE_DATA_URL.test(trimmed);\n}\n","import type { GeoLibreLayer, LayerQuickFilter } from \"./types\";\n\n/**\n * Quick filters (issue #2114): per-layer, data-driven filter controls that\n * compile down to the MapLibre filter a layer already supports.\n *\n * The persisted record is the *control state* (field, kind, chosen values),\n * never the compiled expression. Storing the compiled output would make a\n * filter unreadable and un-editable the moment it is saved, which is exactly\n * the wall this feature exists to remove: a rule filter, Select by Expression,\n * and the attribute table's filter box all already accept a hand-written\n * MapLibre expression.\n *\n * Compilation happens at sync time in `@geolibre/map`'s layer sync, which\n * combines the result with the transient `timeFilter`, the embed API's\n * `embedFilter`, and the rule-based visibility filter under one `[\"all\", …]`,\n * so a host page's filter and a user's filter narrow the layer together\n * instead of clobbering each other.\n *\n * Every clause reads the property in place, so the same expression is correct\n * for a GeoJSON source and for a tile-backed source whose tiles decode later.\n */\n\n/** Day length in milliseconds, used to make a date filter's end day inclusive. */\nconst DAY_MS = 86_400_000;\n\n/** The `YYYY-MM-DD` prefix length every ISO date comparison slices to. */\nconst ISO_DAY_LENGTH = 10;\n\n/** Whether a filter is switched on (the field defaults to enabled). */\nfunction isEnabled(filter: LayerQuickFilter): boolean {\n return filter.enabled !== false;\n}\n\n/** A validated date bound: its canonical `YYYY-MM-DD` text and UTC midnight. */\ninterface IsoDayBound {\n day: string;\n ms: number;\n}\n\n/**\n * Validate a `YYYY-MM-DD` bound, returning its canonical text and UTC midnight,\n * or `null` when the text is blank, malformed, or not a real calendar day.\n *\n * The round-trip check is what rejects a day that does not exist: `Date.parse`\n * happily reads `2026-02-30` as March 2, so shape-checking alone would compile\n * a bound the user never chose. Rejecting here means a half-typed or impossible\n * bound compiles to no clause at all, leaving that side unconstrained rather\n * than filtering on a date nobody asked for.\n */\nfunction parseIsoDayBound(day: string | null | undefined): IsoDayBound | null {\n if (typeof day !== \"string\") return null;\n const trimmed = day.trim();\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(trimmed)) return null;\n const ms = Date.parse(`${trimmed}T00:00:00Z`);\n if (!Number.isFinite(ms)) return null;\n return new Date(ms).toISOString().slice(0, ISO_DAY_LENGTH) === trimmed\n ? { day: trimmed, ms }\n : null;\n}\n\n/** A finite number, or `null` for every other input (including `NaN`). */\nfunction finite(value: number | null | undefined): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * `[\"get\", field]` coerced for comparison, guarded by `[\"has\", field]` where a\n * missing property would otherwise coerce into range (`to-number` turns a\n * missing property into `0`, and `to-string` into `\"\"`, either of which can sit\n * inside a one-sided bound).\n */\nfunction presenceGuard(field: string): unknown[] {\n return [\"has\", field];\n}\n\nfunction compileCategorical(filter: LayerQuickFilter): unknown[] | null {\n const values = filter.values ?? [];\n if (values.length === 0) return null;\n // `in` keeps the values at their original type, so a numeric code and its\n // string spelling never collapse into one another the way `match` labels do.\n return [\"in\", [\"get\", filter.field], [\"literal\", values]];\n}\n\nfunction compileRange(filter: LayerQuickFilter): unknown[] | null {\n const first = finite(filter.min);\n const second = finite(filter.max);\n if (first === null && second === null) return null;\n // Order the bounds rather than trusting which box they were typed into. The\n // slider's thumbs cannot cross, but the two number inputs commit\n // independently, so a bound is briefly inverted on the way to any value whose\n // first digit is smaller (typing `200000` into a max already showing `800000`\n // passes through `2`). Compiling that literally yields an unsatisfiable\n // filter, which empties the layer with nothing on screen to explain why.\n const min = first !== null && second !== null ? Math.min(first, second) : first;\n const max = first !== null && second !== null ? Math.max(first, second) : second;\n const value = [\"to-number\", [\"get\", filter.field]];\n const clauses: unknown[] = [presenceGuard(filter.field)];\n if (min !== null) clauses.push([\">=\", value, min]);\n if (max !== null) clauses.push([\"<=\", value, max]);\n return [\"all\", ...clauses];\n}\n\nfunction compileDate(filter: LayerQuickFilter): unknown[] | null {\n // Both branches read the *validated* bounds, never the raw strings: with one\n // bound valid and the other malformed the filter still compiles, and pushing\n // the raw text would compare against a value no real date can satisfy — which\n // hides every feature instead of leaving that side open.\n const first = parseIsoDayBound(filter.start);\n const second = parseIsoDayBound(filter.end);\n if (first === null && second === null) return null;\n // Ordered for the same reason as `compileRange`: a date typed into the wrong\n // box (or a from/to pair edited out of order) should read as the interval\n // between them, not as an empty layer.\n const inverted = first !== null && second !== null && first.ms > second.ms;\n const start = inverted ? second : first;\n const end = inverted ? first : second;\n const clauses: unknown[] = [presenceGuard(filter.field)];\n\n if ((filter.dateKind ?? \"iso\") === \"iso\") {\n // Compare the leading `YYYY-MM-DD` slice on both sides: ISO text sorts\n // chronologically, and slicing makes a datetime comparable to a plain day\n // bound without parsing. Both bounds are inclusive because the slice drops\n // the time of day.\n const value = [\"slice\", [\"to-string\", [\"get\", filter.field]], 0, ISO_DAY_LENGTH];\n if (start !== null) clauses.push([\">=\", value, start.day]);\n if (end !== null) clauses.push([\"<=\", value, end.day]);\n return [\"all\", ...clauses];\n }\n\n // Epoch numbers: the end bound is exclusive at the *next* day's midnight, so\n // the chosen end day is kept in full rather than only its midnight instant.\n const scale = filter.dateKind === \"epochS\" ? 0.001 : 1;\n const value = [\"to-number\", [\"get\", filter.field]];\n if (start !== null) clauses.push([\">=\", value, start.ms * scale]);\n if (end !== null) clauses.push([\"<\", value, (end.ms + DAY_MS) * scale]);\n return [\"all\", ...clauses];\n}\n\nfunction compileText(filter: LayerQuickFilter): unknown[] | null {\n const needle = filter.text?.trim().toLowerCase() ?? \"\";\n if (needle === \"\") return null;\n // Case-insensitive on both sides: a viewer typing \"portland\" should not have\n // to guess how the data capitalizes it.\n const haystack = [\"downcase\", [\"to-string\", [\"get\", filter.field]]];\n switch (filter.operator ?? \"contains\") {\n case \"equals\":\n return [\"==\", haystack, needle];\n case \"startsWith\":\n return [\"==\", [\"index-of\", needle, haystack], 0];\n default:\n return [\"!=\", [\"index-of\", needle, haystack], -1];\n }\n}\n\n/**\n * Compile one quick filter to a MapLibre filter expression, or `null` when it\n * places no constraint (switched off, no field, or nothing chosen yet). A\n * control that has been added but not yet answered must never hide features.\n *\n * @param filter - The persisted control state.\n * @returns A MapLibre filter expression, or `null`.\n */\nexport function compileQuickFilter(filter: LayerQuickFilter): unknown[] | null {\n if (!isEnabled(filter) || typeof filter.field !== \"string\" || filter.field === \"\") return null;\n switch (filter.kind) {\n case \"categorical\":\n return compileCategorical(filter);\n case \"range\":\n return compileRange(filter);\n case \"date\":\n return compileDate(filter);\n case \"text\":\n return compileText(filter);\n default:\n return null;\n }\n}\n\n/**\n * Compile a layer's quick filters into a single MapLibre filter expression.\n * Multiple controls narrow the data together (`[\"all\", …]`), matching how a\n * viewer reads a stack of filters. Returns `null` when no control constrains\n * anything, so the common path adds no filter at all.\n *\n * @param filters - The layer's persisted quick filters.\n * @returns A MapLibre filter expression, or `null`.\n */\nexport function compileQuickFilters(\n filters: readonly LayerQuickFilter[] | undefined,\n): unknown[] | null {\n if (!filters || filters.length === 0) return null;\n const clauses: unknown[] = [];\n for (const filter of filters) {\n const compiled = compileQuickFilter(filter);\n if (compiled) clauses.push(compiled);\n }\n if (clauses.length === 0) return null;\n return clauses.length === 1 ? (clauses[0] as unknown[]) : [\"all\", ...clauses];\n}\n\n/**\n * The quick filters that actually constrain the layer right now. Drives the\n * \"filtered\" badge on the layer row and its count, so an active filter is\n * never mistaken for missing data.\n *\n * @param layer - The layer to inspect.\n * @returns The constraining filters, in their configured order.\n */\nexport function activeQuickFilters(layer: GeoLibreLayer): LayerQuickFilter[] {\n return (layer.quickFilters ?? []).filter((filter) => compileQuickFilter(filter) !== null);\n}\n\n/**\n * Whether the layer is currently narrowed by at least one quick filter.\n *\n * @param layer - The layer to inspect.\n * @returns `true` when a quick filter constrains the rendered features.\n */\nexport function hasActiveQuickFilter(layer: GeoLibreLayer): boolean {\n return (layer.quickFilters ?? []).some((filter) => compileQuickFilter(filter) !== null);\n}\n\n/**\n * Clear every chosen value while keeping the controls themselves, so \"Clear\n * filters\" is one action that leaves the layer's configured controls in place\n * for the next question.\n *\n * @param filters - The layer's persisted quick filters.\n * @returns The same controls with their selections emptied.\n */\nexport function clearQuickFilterValues(\n filters: readonly LayerQuickFilter[] | undefined,\n): LayerQuickFilter[] {\n return (filters ?? []).map((filter) => ({\n ...filter,\n values: filter.kind === \"categorical\" ? [] : filter.values,\n min: filter.kind === \"range\" ? null : filter.min,\n max: filter.kind === \"range\" ? null : filter.max,\n start: filter.kind === \"date\" ? null : filter.start,\n end: filter.kind === \"date\" ? null : filter.end,\n text: filter.kind === \"text\" ? \"\" : filter.text,\n }));\n}\n","import type { GeoLibreLayer } from \"./types\";\nimport { compileQuickFilters } from \"./quick-filters\";\n\n/** The layer fields needed to inspect project-authored feature filters. */\ntype LayerFilterSource = Pick<GeoLibreLayer, \"filterExpression\" | \"quickFilters\">;\n\n/**\n * Return a layer's persisted expression filter when it has a usable array\n * shape, otherwise return null.\n *\n * @param layer - The layer whose expression filter should be read.\n * @returns The active expression filter, or null when none is configured.\n */\nexport function activeLayerFilterExpression(layer: LayerFilterSource): unknown[] | null {\n const expression = layer.filterExpression;\n return Array.isArray(expression) && expression.length > 0 ? expression : null;\n}\n\n/**\n * Compile all project-authored filters for a layer into one MapLibre boolean\n * expression. An explicit expression and Quick Filter controls narrow the\n * layer together.\n *\n * @param layer - The layer whose authored filters should be composed.\n * @returns A MapLibre filter expression, or null when no authored filter is active.\n */\nexport function compileLayerFilters(layer: LayerFilterSource): unknown[] | null {\n const filters = [\n activeLayerFilterExpression(layer),\n compileQuickFilters(layer.quickFilters),\n ].filter((filter): filter is unknown[] => filter !== null);\n if (filters.length === 0) return null;\n return filters.length === 1 ? filters[0] : [\"all\", ...filters];\n}\n\n/**\n * Whether a project-authored expression or Quick Filter currently narrows a\n * layer. This drives the filtered indicator and clear action in the layer UI.\n *\n * @param layer - The layer to inspect.\n * @returns True when at least one persisted filter is active.\n */\nexport function hasActiveLayerFilter(layer: LayerFilterSource): boolean {\n return compileLayerFilters(layer) !== null;\n}\n","import type {\n AppCapabilities,\n AppPrivilege,\n AppRole,\n GeoLibreLayer,\n LayerCapabilities,\n} from \"./types\";\n\n/**\n * Default inferred capabilities for a layer based on its type and metadata.\n *\n * Inferred rules:\n * - `query`: true for all valid layers.\n * - `create`, `update`, `delete`: true for in-memory or editable GeoJSON vector layers,\n * false for derived queries (DuckDB/SQL), read-only vector tiles, and raster layers.\n * - `export`: true by default.\n */\nexport function inferLayerCapabilities(layer: GeoLibreLayer): Required<LayerCapabilities> {\n const isVector = layer.type === \"geojson\";\n const isDuckDB =\n layer.metadata?.sourceKind === \"duckdb-query\" ||\n (layer.type === \"geojson\" && typeof layer.metadata?.query === \"string\");\n const isExternalNative = layer.metadata?.externalNativeLayer === true;\n const isSketches = layer.metadata?.sourceKind === \"geoeditor-sketches\";\n const isSqlQuery = layer.metadata?.sourceKind === \"sql-query\";\n\n const isReadOnlyVector = isExternalNative && layer.metadata?.sourceKind !== \"maplibre-gl-vector\";\n const isEditable = isVector && !isDuckDB && !isReadOnlyVector && !isSketches && !isSqlQuery;\n\n return {\n query: true,\n create: isEditable,\n update: isEditable,\n delete: isEditable,\n export: true,\n };\n}\n\n/**\n * Resolves the effective capabilities for a layer by overlaying any explicit\n * capability overrides on top of inferred defaults.\n */\nexport function resolveLayerCapabilities(\n layer: GeoLibreLayer | undefined,\n): Required<LayerCapabilities> {\n if (!layer) {\n return {\n query: false,\n create: false,\n update: false,\n delete: false,\n export: false,\n };\n }\n\n const defaults = inferLayerCapabilities(layer);\n if (!layer.capabilities) {\n return defaults;\n }\n\n return {\n query: layer.capabilities.query ?? defaults.query,\n create: layer.capabilities.create ?? defaults.create,\n update: layer.capabilities.update ?? defaults.update,\n delete: layer.capabilities.delete ?? defaults.delete,\n export: layer.capabilities.export ?? defaults.export,\n };\n}\n\n/**\n * Normalizes an untrusted capabilities value from JSON/project data.\n */\nexport function normalizeLayerCapabilities(raw: unknown): LayerCapabilities | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return undefined;\n const obj = raw as Record<string, unknown>;\n const caps: LayerCapabilities = {};\n let hasAny = false;\n\n if (typeof obj.query === \"boolean\") {\n caps.query = obj.query;\n hasAny = true;\n }\n if (typeof obj.create === \"boolean\") {\n caps.create = obj.create;\n hasAny = true;\n }\n if (typeof obj.update === \"boolean\") {\n caps.update = obj.update;\n hasAny = true;\n }\n if (typeof obj.delete === \"boolean\") {\n caps.delete = obj.delete;\n hasAny = true;\n }\n if (typeof obj.export === \"boolean\") {\n caps.export = obj.export;\n hasAny = true;\n }\n\n return hasAny ? caps : undefined;\n}\n\n/**\n * All supported application privilege identifiers in GeoLibre.\n */\nexport const ALL_APP_PRIVILEGES: readonly AppPrivilege[] = [\n \"layers:edit\",\n \"layers:add-remote\",\n \"layers:add-local\",\n \"processing:run\",\n \"processing:sidecar\",\n \"project:save\",\n \"project:share\",\n \"project:share-public\",\n \"plugins:install\",\n \"assistant:use\",\n \"connections:manage\",\n \"export:data\",\n \"export:image\",\n \"settings:manage\",\n] as const;\n\n/**\n * Standard privilege bundles for named application roles.\n */\nexport const ROLE_PRIVILEGES: Record<Exclude<AppRole, \"custom\">, readonly AppPrivilege[]> = {\n viewer: [\"export:image\", \"export:data\"],\n editor: [\n \"export:image\",\n \"export:data\",\n \"layers:edit\",\n \"layers:add-local\",\n \"layers:add-remote\",\n \"processing:run\",\n \"project:save\",\n ],\n publisher: [\n \"export:image\",\n \"export:data\",\n \"layers:edit\",\n \"layers:add-local\",\n \"layers:add-remote\",\n \"processing:run\",\n \"project:save\",\n \"project:share\",\n \"project:share-public\",\n \"processing:sidecar\",\n \"assistant:use\",\n ],\n administrator: ALL_APP_PRIVILEGES,\n};\n\n/**\n * Resolves the effective privilege list for a given role, applying custom overrides if specified.\n */\nexport function resolveRolePrivileges(\n role: AppRole,\n customPrivileges?: readonly AppPrivilege[],\n): AppPrivilege[] {\n if (role === \"custom\") {\n if (!customPrivileges || customPrivileges.length === 0) return [];\n const validSet = new Set<AppPrivilege>(ALL_APP_PRIVILEGES);\n return [...new Set(customPrivileges.filter((p) => validSet.has(p)))];\n }\n return [...ROLE_PRIVILEGES[role]];\n}\n\n/**\n * Intersects multiple sets of privileges to derive the effective permissions when multiple\n * policies (e.g. deployment, organization, and share link) apply simultaneously.\n */\nexport function intersectPrivileges(...privilegeSets: (readonly AppPrivilege[])[]): AppPrivilege[] {\n if (privilegeSets.length === 0) return [];\n if (privilegeSets.length === 1) return [...new Set(privilegeSets[0])];\n let current = new Set<AppPrivilege>(privilegeSets[0]);\n for (let i = 1; i < privilegeSets.length; i++) {\n const nextSet = new Set<AppPrivilege>(privilegeSets[i]);\n current = new Set([...current].filter((p) => nextSet.has(p)));\n }\n return [...current];\n}\n\n/**\n * Evaluates whether an application capability set grants a specific privilege.\n */\nexport function hasAppPrivilege(\n capabilities: AppCapabilities | undefined,\n privilege: AppPrivilege,\n): boolean {\n if (!capabilities) return true;\n return capabilities.privileges.includes(privilege);\n}\n\n/**\n * Resolves why a privilege is withheld: its own reason if it has one, otherwise\n * the reason recorded for the capability set as a whole.\n *\n * @param capabilities - The active capability set, if any.\n * @param privilege - The privilege being explained.\n * @returns The reason, or undefined when none was recorded.\n */\nexport function appPrivilegeReason(\n capabilities: AppCapabilities | undefined,\n privilege: AppPrivilege,\n): string | undefined {\n if (!capabilities) return undefined;\n return capabilities.privilegeReasons?.[privilege] ?? capabilities.reason;\n}\n\n/**\n * Creates the default unconstrained application capabilities (Administrator role).\n */\nexport function createDefaultAppCapabilities(): AppCapabilities {\n return {\n role: \"administrator\",\n privileges: [...ALL_APP_PRIVILEGES],\n };\n}\n\n/**\n * Normalizes an untrusted array of privilege strings.\n */\nexport function normalizeAppPrivileges(raw: unknown): AppPrivilege[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const validSet = new Set<string>(ALL_APP_PRIVILEGES);\n const result: AppPrivilege[] = [];\n for (const item of raw) {\n if (typeof item === \"string\" && validSet.has(item)) {\n result.push(item as AppPrivilege);\n }\n }\n return result.length > 0 ? [...new Set(result)] : [];\n}\n","// The deployment capability model (issue #1673).\n//\n// A *deployment* — a hosted container, a kiosk terminal, a classroom instance,\n// an embed — declares which privileged actions the build it serves is allowed\n// to offer. This is deliberately coarse: it names whole capabilities (\"may add\n// data at all\"), not individual menu items, so a locked-down deployment cannot\n// be defeated by a menu item someone forgot to list.\n//\n// It is *not* the interface profile (`ui-profile.ts` in the desktop app), which\n// hides items to reduce clutter and which the user can turn back on. A denied\n// capability is a statement by whoever stood the deployment up, and the UI\n// never offers a way to grant it back.\n//\n// The client-side gate is one half of the model: it stops the app advertising\n// and invoking what the deployment forbids. The serving layer still has to\n// refuse the corresponding requests, because a client-side-only kiosk mode is a\n// suggestion that anyone with devtools declines.\n\n/** A privileged action a deployment can grant or withhold. */\nexport type DeploymentCapability =\n /** Create, save, or otherwise author the project itself. */\n | \"project:edit\"\n /** Bring new data into the map from files, URLs, or services. */\n | \"data:add\"\n /** Run processing/analysis tools (Whitebox, SQL, vector/raster, ML). */\n | \"processing:run\"\n /** Get data or renderings back out (export image, standalone HTML, …). */\n | \"export:data\"\n /** Install, load, or manage plugins. */\n | \"plugins:install\"\n /** Reach the application settings. */\n | \"settings:manage\";\n\n/** Every capability, in a stable order for docs and tests. */\nexport const DEPLOYMENT_CAPABILITIES: readonly DeploymentCapability[] = [\n \"project:edit\",\n \"data:add\",\n \"processing:run\",\n \"export:data\",\n \"plugins:install\",\n \"settings:manage\",\n];\n\n/**\n * The default grant: everything. `full` must stay the default so an existing\n * deployment that configures nothing behaves exactly as it did before.\n */\nexport const ALL_DEPLOYMENT_CAPABILITIES: ReadonlySet<DeploymentCapability> = new Set(\n DEPLOYMENT_CAPABILITIES,\n);\n\n/** Whether an arbitrary string names a capability we know how to enforce. */\nexport function isDeploymentCapability(value: string): value is DeploymentCapability {\n return (DEPLOYMENT_CAPABILITIES as readonly string[]).includes(value);\n}\n\n/**\n * The reserved token that grants nothing, for a deployment that wants the map\n * and nothing else.\n *\n * Needed because a blank value cannot mean it: the deployment env reader treats\n * an empty string as unset (a `-e VAR=` produces one), and unset has to keep\n * meaning \"full\" so existing deployments are unchanged. Without a spelling for\n * \"none\", the most locked-down configuration would be the one you cannot write.\n */\nexport const NO_DEPLOYMENT_CAPABILITIES = \"none\";\n\n/**\n * Parse a comma-separated capability list from deployment configuration.\n *\n * Unknown tokens are dropped rather than throwing: a newer container image may\n * be configured with a capability this build does not know about, and the safe\n * reading of an unrecognized grant is \"not granted\".\n *\n * Fails closed on purpose. An operator who set the variable at all meant to\n * restrict something, so a list that parses to nothing grants nothing — only an\n * absent (or blank) value falls back to the full set, which is the caller's job\n * to detect.\n *\n * @param raw - The configured value, e.g. `\"data:add, export:data\"`.\n * @returns The granted capabilities, with duplicates and unknown names removed.\n */\nexport function parseDeploymentCapabilities(raw: string): ReadonlySet<DeploymentCapability> {\n const granted = new Set<DeploymentCapability>();\n if (raw.trim().toLowerCase() === NO_DEPLOYMENT_CAPABILITIES) return granted;\n for (const token of raw.split(\",\")) {\n const name = token.trim();\n if (isDeploymentCapability(name)) granted.add(name);\n }\n return granted;\n}\n","/**\n * What this deployment and this session may do: deployment capabilities\n * (issue #1673) and the application role / privilege model (issue #1672).\n * Excluded from the project file and from undo history.\n */\nimport { ALL_DEPLOYMENT_CAPABILITIES, type DeploymentCapability } from \"../deployment-capabilities\";\nimport {\n createDefaultAppCapabilities,\n hasAppPrivilege,\n normalizeAppPrivileges,\n resolveRolePrivileges,\n} from \"../capabilities\";\nimport type { AppCapabilities, AppPrivilege, AppRole } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface CapabilitiesSlice {\n /**\n * What this *deployment* is allowed to do (issue #1673). Set once at startup\n * from the deployment configuration; never from a project file, a URL\n * parameter, or anything else the visitor controls, and never edited from the\n * UI. Defaults to the full set so an unconfigured build behaves as before.\n *\n * Excluded from the project file and from undo history: it describes the\n * server that served the app, not the document being edited.\n */\n deploymentCapabilities: ReadonlySet<DeploymentCapability>;\n /**\n * Ephemeral application capability model (issue #1672). Gating role and\n * privileges for the current session/deployment. Excluded from the project file\n * and undo history.\n */\n capabilities: AppCapabilities;\n\n /**\n * Narrow what this deployment may do. Intended for the startup path only —\n * calling it later would leave already-rendered surfaces stale.\n */\n setDeploymentCapabilities: (capabilities: Iterable<DeploymentCapability>) => void;\n /**\n * Assign an application role (e.g. \"viewer\", \"editor\", \"publisher\", \"administrator\", \"custom\"),\n * deriving the effective privileges and optional reason.\n */\n setAppRole: (\n role: AppRole,\n options?: { customPrivileges?: AppPrivilege[]; reason?: string },\n ) => void;\n /** Set explicit custom privileges and an optional reason. */\n setAppPrivileges: (privileges: AppPrivilege[], reason?: string) => void;\n /** Grant an individual application privilege. */\n grantAppPrivilege: (privilege: AppPrivilege) => void;\n /** Revoke an individual application privilege with an optional reason. */\n revokeAppPrivilege: (privilege: AppPrivilege, reason?: string) => void;\n /** Reset application capabilities back to the default unconstrained Administrator role. */\n resetAppCapabilities: () => void;\n /** Check if the current capabilities grant the requested privilege. */\n hasAppPrivilege: (privilege: AppPrivilege) => boolean;\n}\n\nexport const createCapabilitiesSlice: SliceCreator<CapabilitiesSlice> = (set, get) => ({\n deploymentCapabilities: ALL_DEPLOYMENT_CAPABILITIES,\n capabilities: createDefaultAppCapabilities(),\n\n setDeploymentCapabilities: (capabilities) =>\n set({ deploymentCapabilities: new Set(capabilities) }),\n\n // A new role or privilege list is a new policy, so the per-privilege reasons\n // recorded against the old one go with it — carrying them forward would\n // explain a grant that is no longer withheld for that cause.\n setAppRole: (role, options) => {\n const privileges = resolveRolePrivileges(role, options?.customPrivileges);\n set({\n capabilities: {\n role,\n privileges,\n reason: options?.reason,\n },\n });\n },\n\n setAppPrivileges: (privileges, reason) => {\n set({\n capabilities: {\n role: \"custom\",\n privileges: normalizeAppPrivileges(privileges) ?? [],\n reason,\n },\n });\n },\n\n // An ad-hoc grant or revoke makes the set no longer the bundle its role\n // names, so the role becomes \"custom\" — the same thing setAppPrivileges\n // does for an explicit list. Leaving it as \"editor\" while the privileges\n // are not the editor bundle would mislead anything that branches on the\n // role rather than checking a privilege.\n grantAppPrivilege: (privilege) => {\n const current = get().capabilities;\n if (current.privileges.includes(privilege)) return;\n const { [privilege]: _granted, ...privilegeReasons } = current.privilegeReasons ?? {};\n set({\n capabilities: {\n ...current,\n role: \"custom\",\n privileges: [...current.privileges, privilege],\n privilegeReasons,\n },\n });\n },\n\n // The reason is filed against this privilege, not against the whole set:\n // revoking a second privilege for a different cause must not relabel the\n // first one's explanation. `reason` stays the fallback for the rest.\n //\n // Re-revoking an already-withheld privilege is not a no-op when it carries\n // a new reason: restating why something is denied is a real operation, and\n // an early return would silently keep the stale explanation on screen.\n revokeAppPrivilege: (privilege, reason) => {\n const current = get().capabilities;\n const held = current.privileges.includes(privilege);\n if (!held && !reason) return;\n set({\n capabilities: {\n ...current,\n role: held ? \"custom\" : current.role,\n privileges: held ? current.privileges.filter((p) => p !== privilege) : current.privileges,\n privilegeReasons: reason\n ? { ...current.privilegeReasons, [privilege]: reason }\n : current.privilegeReasons,\n },\n });\n },\n\n resetAppCapabilities: () => {\n set({ capabilities: createDefaultAppCapabilities() });\n },\n\n hasAppPrivilege: (privilege) => {\n return hasAppPrivilege(get().capabilities, privilege);\n },\n});\n","/**\n * Ephemeral live-collaboration session state (issue #307): never saved with the\n * project and never tracked by undo history.\n */\nimport type {\n CollabInvite,\n CollaborationChatMessage,\n CollaborationParticipant,\n CollaborationPresence,\n CollaborationState,\n} from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\n/**\n * A fresh, inactive collaboration slice (no live session). Frozen (like\n * DEFAULT_LEGEND_CONFIG) to guard against accidental in-place mutation; store\n * actions always produce new objects via spread, so the frozen default is only\n * ever read.\n */\nexport const DEFAULT_COLLABORATION_STATE: CollaborationState = Object.freeze({\n isActive: false,\n connecting: false,\n sessionId: null,\n clientId: null,\n role: null,\n mode: \"co-edit\",\n selfName: \"\",\n selfColor: \"\",\n participants: Object.freeze([] as CollaborationParticipant[]) as CollaborationParticipant[],\n presence: Object.freeze({} as Record<string, CollaborationPresence>) as Record<\n string,\n CollaborationPresence\n >,\n followHost: false,\n chat: Object.freeze([] as CollaborationChatMessage[]) as CollaborationChatMessage[],\n requireIdentity: false,\n identitySupported: false,\n lockedLayerIds: Object.freeze([] as string[]) as string[],\n invites: Object.freeze([] as CollabInvite[]) as CollabInvite[],\n error: null,\n});\n\n// Cap the in-store chat log so a long session can't grow it without bound. This\n// is intentionally larger than the relay's persisted history (50): the live\n// session accumulates messages locally, while the relay only retains the tail\n// for late joiners.\nconst MAX_COLLABORATION_CHAT = 200;\n\nexport interface CollaborationSlice {\n // Ephemeral live-collaboration session state (issue #307). Deliberately\n // excluded from the project file (project.ts never reads it) and from undo\n // history (partialize never lists it).\n collaboration: CollaborationState;\n\n setCollaboration: (patch: Partial<CollaborationState>) => void;\n updateCollaborationPresence: (clientId: string, presence: CollaborationPresence | null) => void;\n /** Append a chat message to the session log (bounded; #754). */\n addCollaborationChat: (message: CollaborationChatMessage) => void;\n resetCollaboration: () => void;\n}\n\nexport const createCollaborationSlice: SliceCreator<CollaborationSlice> = (set) => ({\n collaboration: DEFAULT_COLLABORATION_STATE,\n\n setCollaboration: (patch) => set((s) => ({ collaboration: { ...s.collaboration, ...patch } })),\n // Add or remove a single remote participant's presence without rebuilding\n // the whole map on every cursor move. Passing `null` drops the entry (on\n // participant leave).\n updateCollaborationPresence: (clientId, presence) =>\n set((s) => {\n const next = { ...s.collaboration.presence };\n if (presence === null) {\n delete next[clientId];\n } else {\n next[clientId] = presence;\n }\n return { collaboration: { ...s.collaboration, presence: next } };\n }),\n addCollaborationChat: (message) =>\n set((s) => {\n // Default to [] in case an older relay left the slice undefined.\n const current = s.collaboration.chat ?? [];\n // Ignore a duplicate id: the server broadcasts to the sender too, and a\n // reconnect can replay recent history, so de-dupe defensively.\n if (current.some((m) => m.id === message.id)) return s;\n const chat = [...current, message].slice(-MAX_COLLABORATION_CHAT);\n return { collaboration: { ...s.collaboration, chat } };\n }),\n resetCollaboration: () =>\n // Also close the Collaborate dialog: an unexpected disconnect resets the\n // slice without a user-initiated leave(), and leaving the dialog open\n // would drop the user onto the start/join form with no context.\n set((s) => ({\n collaboration: DEFAULT_COLLABORATION_STATE,\n ui: { ...s.ui, collaborateDialogOpen: false },\n })),\n});\n","import type { GeoLibreLayer, LayerGroup } from \"./types\";\n\n/** Opacity a freshly created {@link LayerGroup} starts at (fully opaque). */\nexport const DEFAULT_LAYER_GROUP_OPACITY = 1;\n\n/**\n * One row in the layer panel: either a top-level (ungrouped) layer or a group\n * header together with its child layers.\n */\nexport type LayerTreeItem =\n | { kind: \"layer\"; layer: GeoLibreLayer }\n | { kind: \"group\"; group: LayerGroup; children: GeoLibreLayer[] };\n\n/**\n * One top-level block of the layer panel, in top-of-panel-first order: either a\n * single ungrouped layer or the contiguous run of layers a group owns.\n *\n * A group that owns no layer anywhere beneath it still gets a unit, with an\n * empty `layers` array, so that empty folders have a position in the panel and\n * can be reordered like any other row.\n */\nexport type LayerPanelUnit = {\n /** Group the block belongs to, or `null` for a lone ungrouped layer. */\n groupId: string | null;\n /** Member layers, top-first. Empty for a group with no layers under it. */\n layers: GeoLibreLayer[];\n};\n\n/**\n * The group a group is really nested under: its `parentId`, or `null` when it\n * is top-level or its `parentId` points at a group that no longer exists. Every\n * placement rule reads the parent through here so a dangling reference behaves\n * like a top-level folder rather than falling into a branch of the tree that is\n * not there.\n */\nfunction effectiveParentId(\n group: LayerGroup | undefined,\n groupById: ReadonlyMap<string, LayerGroup>,\n): string | null {\n const parentId = group?.parentId;\n return parentId && groupById.has(parentId) ? parentId : null;\n}\n\n/**\n * Panel index range `[first, last]` each group spans, keyed by group id. A\n * group with no unit of its own (an organizer whose layers all live in child\n * groups) spans the union of its descendants' units, so it is absent only when\n * nothing under it has a position yet.\n */\nfunction groupUnitRanges(\n units: LayerPanelUnit[],\n groupById: ReadonlyMap<string, LayerGroup>,\n): Map<string, [number, number]> {\n const ranges = new Map<string, [number, number]>();\n units.forEach((unit, index) => {\n let id: string | null = unit.groupId;\n const visited = new Set<string>();\n while (id && !visited.has(id)) {\n visited.add(id);\n const range = ranges.get(id);\n if (range) ranges.set(id, [Math.min(range[0], index), Math.max(range[1], index)]);\n else ranges.set(id, [index, index]);\n // A dangling `parentId` ends the walk rather than recording a range for a\n // group that does not exist.\n id = effectiveParentId(groupById.get(id), groupById);\n }\n });\n return ranges;\n}\n\n/**\n * Group indices ordered so that a group's parent always comes first, keeping\n * the `groups` order otherwise. `moveLayerGroupToGroup` reparents in place\n * without touching the array, so a child can sit ahead of its own parent; a\n * parent has to be placed first for the child to have a block to sit under.\n * A `parentId` cycle never resolves, so what is left is emitted in array order\n * rather than dropped.\n */\nfunction parentsFirstOrder(\n groups: LayerGroup[],\n groupById: ReadonlyMap<string, LayerGroup>,\n): number[] {\n const order: number[] = [];\n const placed = new Set<string>();\n let pending = groups.map((_, index) => index);\n while (pending.length > 0) {\n const deferred: number[] = [];\n for (const index of pending) {\n const parentId = groups[index].parentId;\n if (parentId && groupById.has(parentId) && !placed.has(parentId)) deferred.push(index);\n else {\n order.push(index);\n placed.add(groups[index].id);\n }\n }\n if (deferred.length === pending.length) {\n order.push(...deferred);\n break;\n }\n pending = deferred;\n }\n return order;\n}\n\n/**\n * Split the panel into its top-level blocks, **top-of-panel first** (the\n * reverse of the store's render order, where the last array element draws on\n * top).\n *\n * Layers give every populated group a position of its own. A group with no\n * layers under it has none to derive, so it is slotted in next to its\n * **siblings** — the groups sharing its parent — in `groups` order: directly\n * below the nearest sibling above it in that array, else directly above the\n * nearest one below it. Because the neighbours are read from `groups` rather\n * than from the layer array, a folder keeps its place relative to its siblings\n * when one of them gains its first layer (GeoLibre#1739).\n *\n * Only a sibling can place it. An ancestor's range already contains the slot\n * being chosen, and an unrelated group's range covers a different branch of the\n * tree, so anchoring on either drops the folder into someone else's block —\n * which is how a nested folder used to land outside its own parent once a\n * cousin gained a layer (GeoLibre#1739 follow-up). With no sibling positioned\n * yet, a nested folder falls to the end of its parent's block and a top-level\n * one to the top of the panel.\n *\n * An *organizer* — a group with no layers of its own but with some beneath it —\n * gets no block here: the panel draws its header against its top-most\n * descendant layer instead. Only a group whose whole subtree is empty needs one.\n *\n * A `groupId` that does not match any group is treated as ungrouped, so a\n * dangling reference degrades gracefully instead of dropping the layer.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns Panel blocks in top-to-bottom display order.\n */\nexport function buildLayerPanelUnits(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n): LayerPanelUnit[] {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const units: LayerPanelUnit[] = [];\n for (let i = layers.length - 1; i >= 0; i--) {\n const layer = layers[i];\n const groupId = layer.groupId && groupById.has(layer.groupId) ? layer.groupId : null;\n const last = units[units.length - 1];\n if (groupId && last && last.groupId === groupId) last.layers.push(layer);\n else units.push({ groupId, layers: [layer] });\n }\n\n let ranges = groupUnitRanges(units, groupById);\n // Every group a layer reaches — the ones that own a block and the organizers\n // spanning them — already has a position. `ranges` alone cannot stand in for\n // that set once the loop starts: splicing a folder in gives its ancestors an\n // inherited range too, and an ancestor whose subtree holds no layer still\n // needs a block of its own or it renders nowhere at all.\n const positioned = new Set(ranges.keys());\n if (positioned.size === groups.length) return units;\n for (const at of parentsFirstOrder(groups, groupById)) {\n const group = groups[at];\n if (positioned.has(group.id)) continue;\n const parentId = effectiveParentId(group, groupById);\n const isSibling = (other: LayerGroup) => effectiveParentId(other, groupById) === parentId;\n let insertAt: number | undefined;\n for (let i = at - 1; insertAt === undefined && i >= 0; i--) {\n if (!isSibling(groups[i])) continue;\n const range = ranges.get(groups[i].id);\n if (range) insertAt = range[1] + 1;\n }\n for (let i = at + 1; insertAt === undefined && i < groups.length; i++) {\n if (!isSibling(groups[i])) continue;\n const range = ranges.get(groups[i].id);\n if (range) insertAt = range[0];\n }\n // No sibling to sit beside: stay inside the parent's block rather than\n // drifting to the top of the panel past groups this one is nested in.\n if (insertAt === undefined && parentId) insertAt = (ranges.get(parentId)?.[1] ?? -1) + 1;\n units.splice(insertAt ?? 0, 0, { groupId: group.id, layers: [] });\n positioned.add(group.id);\n // Placing this folder shifts the units below it, and makes the folder\n // itself an anchor for the ones still to be placed.\n ranges = groupUnitRanges(units, groupById);\n }\n return units;\n}\n\n/**\n * Where the layer panel should draw every group header, expressed against the\n * layer rows the panel already renders.\n */\nexport type LayerPanelGroupHeaders = {\n /** Headers drawn immediately above the keyed layer's row, in panel order. */\n aboveLayer: Map<string, LayerGroup[]>;\n /** Headers drawn below every layer row, in panel order. */\n bottom: LayerGroup[];\n};\n\n/**\n * Resolve {@link buildLayerPanelUnits} into a draw position for each group\n * header, so a panel that renders row by row from the flat layer list can place\n * them without rebuilding its whole render as a tree.\n *\n * Every header comes from the one walk: the group a layer belongs to, the\n * *organizers* above it whose own layers all live in child groups, and the\n * folders holding no layer at all. Deriving them together is what keeps them in\n * order — each is emitted the first time the walk reaches its subtree, parents\n * ahead of children, so a nested folder can never be drawn above the parent it\n * sits inside (GeoLibre#1739 follow-up).\n *\n * A group is emitted once, at its top-most block, so a group whose members are\n * scattered still gets a single header.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns The anchor layer (or the panel bottom) each header sits above.\n */\nexport function layerPanelGroupHeaders(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n): LayerPanelGroupHeaders {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const units = buildLayerPanelUnits(layers, groups);\n const aboveLayer = new Map<string, LayerGroup[]>();\n const bottom: LayerGroup[] = [];\n const drawn = new Set<string>();\n for (let i = 0; i < units.length; i++) {\n const unit = units[i];\n if (!unit.groupId) continue;\n // Walk up to the root and reverse, so an ancestor's header precedes the\n // headers of everything nested in it. A `parentId` cycle stops the walk\n // instead of hanging.\n const chain: LayerGroup[] = [];\n let id: string | null = unit.groupId;\n const visited = new Set<string>();\n while (id && !visited.has(id) && !drawn.has(id)) {\n visited.add(id);\n const group = groupById.get(id);\n if (!group) break;\n chain.unshift(group);\n id = effectiveParentId(group, groupById);\n }\n if (chain.length === 0) continue;\n // The block's own first layer anchors it; a folder with none borrows the\n // first layer below it, and falls to the panel bottom when there is none.\n let anchorId = unit.layers[0]?.id;\n for (let j = i + 1; anchorId === undefined && j < units.length; j++) {\n anchorId = units[j].layers[0]?.id;\n }\n for (const group of chain) drawn.add(group.id);\n if (anchorId === undefined) {\n bottom.push(...chain);\n continue;\n }\n const at = aboveLayer.get(anchorId);\n if (at) at.push(...chain);\n else aboveLayer.set(anchorId, chain);\n }\n return { aboveLayer, bottom };\n}\n\n/**\n * Derive the layer-panel tree from the flat `layers` array and the group\n * definitions.\n *\n * Items are returned **top-of-panel first** (the reverse of the store's\n * render order, where the last array element draws on top). Within a group the\n * children are likewise ordered top-first. Each group is emitted once, at the\n * panel position of its top-most member; a group's members are normally\n * contiguous in the array, but if they are not, every member is still gathered\n * under a single header. Groups with no members (empty folders) sit where\n * {@link buildLayerPanelUnits} places them.\n *\n * A `groupId` that does not match any group is treated as ungrouped, so a\n * dangling reference degrades gracefully instead of dropping the layer.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns Panel rows in top-to-bottom display order.\n */\nexport function buildLayerTree(layers: GeoLibreLayer[], groups: LayerGroup[]): LayerTreeItem[] {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const items: LayerTreeItem[] = [];\n const emittedAt = new Map<string, LayerTreeItem & { kind: \"group\" }>();\n\n for (const unit of buildLayerPanelUnits(layers, groups)) {\n const group = unit.groupId ? groupById.get(unit.groupId) : undefined;\n if (!group) {\n for (const layer of unit.layers) items.push({ kind: \"layer\", layer });\n continue;\n }\n // A group's members are normally one contiguous run, but a scattered group\n // still gets a single header, at its top-most member.\n const existing = emittedAt.get(group.id);\n if (existing) {\n existing.children.push(...unit.layers);\n continue;\n }\n const item = { kind: \"group\" as const, group, children: [...unit.layers] };\n emittedAt.set(group.id, item);\n items.push(item);\n }\n return items;\n}\n\n/**\n * Fold each group's visibility and opacity into its child layers so the map\n * sync can keep treating every layer independently.\n *\n * A child's effective visibility is its own `visible` ANDed with the group's,\n * and its effective opacity is its own multiplied by the group's. Layers\n * without a group (or with a dangling `groupId`) are returned unchanged, and\n * the original object reference is preserved whenever nothing changes so\n * downstream memoization stays cheap.\n *\n * @param layers Flat layer list.\n * @param groups Group definitions.\n * @returns Layers with group effects applied.\n */\nexport function applyGroupEffects(layers: GeoLibreLayer[], groups: LayerGroup[]): GeoLibreLayer[] {\n if (groups.length === 0) return layers;\n const groupById = new Map(groups.map((g) => [g.id, g]));\n return layers.map((layer) => {\n if (!layer.groupId || !groupById.has(layer.groupId)) return layer;\n const { visible, opacity } = foldGroupChain(layer, groupById);\n if (visible === layer.visible && opacity === layer.opacity) return layer;\n return { ...layer, visible, opacity };\n });\n}\n\n/**\n * Walk a layer's group chain upward, ANDing each group's visibility into the\n * layer's own and multiplying each group's opacity into it. The `visited` set\n * makes a corrupted `parentId` cycle terminate instead of hanging.\n */\nfunction foldGroupChain(\n layer: GeoLibreLayer,\n groupById: ReadonlyMap<string, LayerGroup>,\n): { visible: boolean; opacity: number } {\n let visible = layer.visible;\n let opacity = layer.opacity;\n let group = layer.groupId ? groupById.get(layer.groupId) : undefined;\n const visited = new Set<string>();\n while (group && !visited.has(group.id)) {\n visited.add(group.id);\n visible = visible && group.visible;\n opacity *= group.opacity;\n group = group.parentId ? groupById.get(group.parentId) : undefined;\n }\n return { visible, opacity };\n}\n\n/**\n * The single layer form of {@link applyGroupEffects}: the visibility and\n * opacity a layer should actually render at once its (possibly nested) parent\n * groups are folded in.\n *\n * Group state never mutates a child's own `visible`/`opacity` fields, so any\n * renderer that cannot read the folded array — a plugin control that owns its\n * own paint and is driven layer by layer — asks for a single layer here\n * instead.\n *\n * @param layer The layer to fold.\n * @param groups Group definitions, or an already-built id → group map. Callers\n * in a render path that fold many layers against the same groups (the layer\n * panel) should pass a memoized map, since the array form rebuilds one per\n * call.\n * @returns The effective render state (the layer's own values when it is\n * ungrouped or its `groupId` is dangling).\n */\nexport function effectiveLayerRenderState(\n layer: GeoLibreLayer,\n groups: LayerGroup[] | ReadonlyMap<string, LayerGroup>,\n): { visible: boolean; opacity: number } {\n const size = Array.isArray(groups) ? groups.length : groups.size;\n if (size === 0 || !layer.groupId) {\n return { visible: layer.visible, opacity: layer.opacity };\n }\n return foldGroupChain(\n layer,\n Array.isArray(groups) ? new Map(groups.map((g) => [g.id, g])) : groups,\n );\n}\n\n/**\n * Indices, in store order, of every layer that belongs to `groupId`.\n *\n * @param layers Flat layer list.\n * @param groupId Group whose members to locate.\n * @returns Ascending list of member indices (empty when the group has none).\n */\nexport function groupMemberIndices(layers: GeoLibreLayer[], groupId: string): number[] {\n const indices: number[] = [];\n for (let i = 0; i < layers.length; i++) {\n if (layers[i]?.groupId === groupId) indices.push(i);\n }\n return indices;\n}\n\n/**\n * Reorder `layers` so that every group's members form a single contiguous block,\n * preserving the relative order of layers within each group and of the\n * top-level items. The block for a group is anchored at the position of its\n * current bottom-most (first) member — the first member encountered iterating\n * from index 0, which renders at the bottom of the layer panel.\n *\n * Mutating store actions call this after assigning `groupId`s to restore the\n * contiguity invariant the rest of the system relies on.\n *\n * @param layers Flat layer list, possibly with interleaved group members.\n * @returns A new array with grouped layers made contiguous.\n */\nexport function normalizeGroupContiguity(layers: GeoLibreLayer[]): GeoLibreLayer[] {\n const result: GeoLibreLayer[] = [];\n const placed = new Set<string>();\n for (let i = 0; i < layers.length; i++) {\n const layer = layers[i];\n if (placed.has(layer.id)) continue;\n if (!layer.groupId) {\n result.push(layer);\n placed.add(layer.id);\n continue;\n }\n // Pull in every remaining member of this group, in array order, so the\n // block is anchored at the first member encountered.\n for (let j = i; j < layers.length; j++) {\n const candidate = layers[j];\n if (candidate.groupId === layer.groupId && !placed.has(candidate.id)) {\n result.push(candidate);\n placed.add(candidate.id);\n }\n }\n }\n return result;\n}\n\n/**\n * Which of `parentId`'s own blocks a panel unit belongs to, used to move a group\n * only among the blocks it shares a parent with:\n *\n * - the id of the child of `parentId` the unit is nested in, however deep — the\n * sibling block a move steps over, or the moving group's own block;\n * - `parentId` itself when the unit holds that group's own layers: one more\n * block inside the parent, which a child steps over like any other rather\n * than a wall;\n * - `null` for an ungrouped layer at the root of the panel (`parentId` `null`);\n * - `undefined` when the unit sits outside `parentId` altogether — a wall,\n * since a nested group has no position outside the parent holding it.\n *\n * A `parentId` cycle reads as outside rather than looping.\n */\nfunction branchUnder(\n parentId: string | null,\n groupId: string | null,\n groupById: ReadonlyMap<string, LayerGroup>,\n): string | null | undefined {\n if (groupId === null) return parentId === null ? null : undefined;\n if (groupId === parentId) return groupId;\n let id = groupId;\n const visited = new Set([id]);\n for (;;) {\n const next = effectiveParentId(groupById.get(id), groupById);\n if (next === parentId) return id;\n if (next === null || visited.has(next)) return undefined;\n visited.add(next);\n id = next;\n }\n}\n\n/**\n * How deeply a group is nested: 0 for a top-level folder, 1 for a child of one,\n * and so on. Drives the layer panel's indentation and keeps a parent ahead of\n * its children when a reorder re-sorts the group array. A corrupted `parentId`\n * cycle stops the walk instead of hanging.\n *\n * @param group Group to measure.\n * @param groupById Group lookup, so a caller folding many groups against the\n * same set can pass a memoized map rather than rebuild one per call.\n * @returns The nesting depth.\n */\nexport function layerGroupDepth(\n group: LayerGroup,\n groupById: ReadonlyMap<string, LayerGroup>,\n): number {\n let depth = 0;\n let parentId = group.parentId;\n const visited = new Set([group.id]);\n while (parentId && !visited.has(parentId)) {\n visited.add(parentId);\n const parent = groupById.get(parentId);\n if (!parent) break;\n depth += 1;\n parentId = parent.parentId;\n }\n return depth;\n}\n\n/**\n * Move a group — with everything nested inside it — one block up or down among\n * its siblings, the groups sharing its parent.\n *\n * A nested group moves inside its parent only: it steps over a whole sibling\n * block rather than into it, and the parent's boundary is the end of its\n * travel, so `null` comes back rather than a move that would take it out of the\n * folder holding it. Inside that boundary it steps over its parent's *own*\n * layers as readily as over a sibling folder, since those rows are one more\n * block of the parent and ordering a child against them is a real reorder. A\n * folder holding no layer is the exception: it has no way to record a position\n * between two layer rows, so a move that would only cross them changes nothing\n * and reports `null` — the same limit empty folders already have at the top\n * level. A top-level group moves among the other top-level groups and the\n * ungrouped layers.\n *\n * Both arrays carry part of the panel order, so both come back: `layers` holds\n * the order of every populated block, and `groups` holds the order the empty\n * folders are placed against (see {@link buildLayerPanelUnits}). Swapping two\n * folders that own no layers moves nothing in `layers`, and moving a populated\n * group past an empty one moves nothing but the group order, so a caller must\n * apply both.\n *\n * `normalizeGroupContiguity` keeps each *group's* own members together, but not\n * a whole *subtree*: nesting only rewrites `parentId`, so an unrelated block can\n * sit between two sibling child groups. The subtree still travels as one block —\n * \"with everything nested inside it\" — which means such an in-between block is\n * left behind on the other side of it, and the move can shift more than one row.\n * Compacting the subtree is the intended reading of the operation; the in-between\n * block landing on the far side is the accepted cost.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @param id Group to move.\n * @param direction `\"up\"` toward the top of the panel, `\"down\"` toward the\n * bottom.\n * @returns The new arrays, or `null` when the group is already at that end of\n * the panel and nothing would change.\n */\nexport function reorderLayerGroupInPanel(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n direction: \"up\" | \"down\",\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n return moveGroupThroughUnits(buildLayerPanelUnits(layers, groups), layers, groups, id, direction);\n}\n\n/**\n * The body of {@link reorderLayerGroupInPanel}, against panel blocks the caller\n * already has, so asking about every group in turn splits the panel once rather\n * than once per question.\n */\nfunction moveGroupThroughUnits(\n units: LayerPanelUnit[],\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n direction: \"up\" | \"down\",\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const group = groupById.get(id);\n if (!group) return null;\n // Label every unit with the block it occupies among the moving group's own\n // siblings, so the move steps over a whole neighbouring block and stops at the\n // edge of the parent instead of stepping one unit at a time into it.\n const parentId = effectiveParentId(group, groupById);\n const branches = units.map((unit) => branchUnder(parentId, unit.groupId, groupById));\n const matching = new Set<number>();\n branches.forEach((branch, index) => {\n if (branch === id) matching.add(index);\n });\n if (matching.size === 0) return null;\n const indices = [...matching].sort((a, b) => a - b);\n // Units run top-first, so \"up\" steps over the block above the group's first.\n const neighbor = direction === \"up\" ? indices[0] - 1 : indices[indices.length - 1] + 1;\n if (neighbor < 0 || neighbor >= units.length) return null;\n const neighborBranch = branches[neighbor];\n // Outside the parent: the group is already at that end of its own block.\n if (neighborBranch === undefined) return null;\n // A neighbouring group travels whole, so the move clears everything nested in\n // it; an ungrouped layer is a block of one.\n const neighborUnits =\n neighborBranch === null\n ? [units[neighbor]]\n : units.filter((_, index) => branches[index] === neighborBranch);\n // The moving group's own indices need not be contiguous — sibling child groups\n // can straddle an unrelated block. Gathering them compacts the subtree, which\n // is the point of moving it as a whole; see `reorderLayerGroupInPanel`.\n const block = units.filter((_, index) => matching.has(index));\n const remaining = units.filter((_, index) => !matching.has(index));\n const edge =\n direction === \"up\"\n ? remaining.indexOf(neighborUnits[0])\n : remaining.indexOf(neighborUnits[neighborUnits.length - 1]) + 1;\n const reordered = [...remaining];\n reordered.splice(edge, 0, ...block);\n\n return arraysFromUnits(reordered, layers, groups, groupById);\n}\n\n/**\n * Turn reordered panel blocks back into the two store arrays, or `null` when\n * neither array changed. Shared by every panel reorder so they all agree on how\n * the empty folders follow a move.\n */\nfunction arraysFromUnits(\n reordered: LayerPanelUnit[],\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n groupById: ReadonlyMap<string, LayerGroup>,\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n // Units are top-first and so are the layers inside them; the store keeps the\n // reverse, with the last element drawing on top.\n const nextLayers = reordered.flatMap((unit) => unit.layers).reverse();\n // Re-derive the group order from the new panel so the empty folders, which\n // are placed against it, follow the move. Ties (an organizer and the child\n // block it spans) keep parents first.\n const ranges = groupUnitRanges(reordered, groupById);\n const nextGroups = [...groups].sort((a, b) => {\n const byPosition = (ranges.get(a.id)?.[0] ?? 0) - (ranges.get(b.id)?.[0] ?? 0);\n return byPosition !== 0\n ? byPosition\n : layerGroupDepth(a, groupById) - layerGroupDepth(b, groupById);\n });\n const layersMoved = nextLayers.some((layer, index) => layer.id !== layers[index]?.id);\n const groupsMoved = nextGroups.some((group, index) => group.id !== groups[index]?.id);\n if (!layersMoved && !groupsMoved) return null;\n return { layers: nextLayers, groups: nextGroups };\n}\n\n/** Direction {@link sortLayerGroupInPanel} orders names in, top of panel first. */\nexport type LayerGroupSortOrder = \"asc\" | \"desc\";\n\n/**\n * A numeric-aware, case/accent-insensitive collator, so \"Parcel 2\" sorts before\n * \"Parcel 10\" and \"owner\" sits with \"Owner\", the way a file browser orders.\n * Core has no i18n of its own, so the panel passes the app's display language;\n * without one (or with a tag `Intl` rejects) the runtime default applies.\n */\nfunction nameCollator(locale: string | undefined): Intl.Collator {\n const options: Intl.CollatorOptions = { numeric: true, sensitivity: \"base\" };\n try {\n return new Intl.Collator(locale, options);\n } catch {\n return new Intl.Collator(undefined, options);\n }\n}\n\n/**\n * Sort what sits directly inside a group by name, A to Z (`\"asc\"`) or Z to A\n * (`\"desc\"`) from the top of the panel down (GeoLibre#2599).\n *\n * The group's own layers are sorted among themselves, and its child groups are\n * sorted among themselves by group name, each carrying everything nested inside\n * it. The two are not interleaved: a group's own layers are one contiguous\n * block (see {@link normalizeGroupContiguity}), so that block keeps its slot\n * among the child groups rather than being split between them. Only direct\n * children move; the contents of a child group keep their order.\n *\n * Like {@link reorderLayerGroupInPanel}, both arrays come back, so empty child\n * folders follow the sort too.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @param id Group whose children to sort.\n * @param order `\"asc\"` for A to Z, `\"desc\"` for Z to A.\n * @param locale BCP 47 tag whose collation rules to compare names by, normally\n * the app's display language. Defaults to the runtime locale.\n * @returns The new arrays, or `null` when the children are already in that\n * order and nothing would change.\n */\nexport function sortLayerGroupInPanel(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n order: LayerGroupSortOrder,\n locale?: string,\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n const units = buildLayerPanelUnits(layers, groups);\n return sortGroupThroughUnits(units, layers, groups, id, order, nameCollator(locale));\n}\n\n/**\n * The body of {@link sortLayerGroupInPanel}, against panel blocks the caller\n * already has.\n */\nfunction sortGroupThroughUnits(\n units: LayerPanelUnit[],\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n order: LayerGroupSortOrder,\n collator: Intl.Collator,\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n if (!groupById.has(id)) return null;\n const sign = order === \"asc\" ? 1 : -1;\n const compare = (a: string, b: string) => sign * collator.compare(a, b);\n // Label each unit with the direct child of `id` it sits in (`id` itself for\n // the group's own layers), or `undefined` when it lies outside the group.\n const branches = units.map((unit) => branchUnder(id, unit.groupId, groupById));\n const inside = branches.flatMap((branch, index) => (branch ? [index] : []));\n if (inside.length === 0) return null;\n\n // Blocks in their current top-first order: the group's own layers as one\n // block, each child group as one block carrying its whole subtree.\n const blockUnits = new Map<string, LayerPanelUnit[]>();\n for (const index of inside) {\n const branch = branches[index] as string;\n const list = blockUnits.get(branch);\n if (list) list.push(units[index]);\n else blockUnits.set(branch, [units[index]]);\n }\n const blockIds = [...blockUnits.keys()];\n const childIds = blockIds.filter((branch) => branch !== id);\n const sortedChildIds = [...childIds].sort((a, b) =>\n compare(groupById.get(a)?.name ?? \"\", groupById.get(b)?.name ?? \"\"),\n );\n let nextChild = 0;\n const sortedBlocks = blockIds.map((branch): LayerPanelUnit[] => {\n if (branch !== id) return blockUnits.get(sortedChildIds[nextChild++]) ?? [];\n const own = (blockUnits.get(id) ?? []).flatMap((unit) => unit.layers);\n return [{ groupId: id, layers: [...own].sort((a, b) => compare(a.name, b.name)) }];\n });\n\n // The group's units need not be contiguous (see `reorderLayerGroupInPanel`);\n // gathering them at the first one compacts the subtree, as a move does.\n const insideSet = new Set(inside);\n const reordered = units.filter((_, index) => !insideSet.has(index));\n reordered.splice(inside[0], 0, ...sortedBlocks.flat());\n // Judge \"already sorted\" by what the panel shows, not by `groups` order:\n // a reparent can leave that array out of panel order, and re-deriving it\n // alone would enable a sort that changes nothing visible. This also skips\n // the whole-array rebuild for the common no-op asked by the sort menu.\n const unchanged =\n reordered.length === units.length &&\n reordered.every(\n (unit, index) =>\n unit.groupId === units[index].groupId &&\n unit.layers.length === units[index].layers.length &&\n unit.layers.every((layer, at) => layer.id === units[index].layers[at].id),\n );\n if (unchanged) return null;\n return arraysFromUnits(reordered, layers, groups, groupById);\n}\n\n/**\n * Which directions {@link reorderLayerGroupInPanel} can actually move each\n * group, so the layer panel can disable a menu item that would do nothing.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns Per-group flags, keyed by group id.\n */\nexport function layerGroupMoveability(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n): Map<string, { up: boolean; down: boolean }> {\n const units = buildLayerPanelUnits(layers, groups);\n const result = new Map<string, { up: boolean; down: boolean }>();\n for (const group of groups) {\n result.set(group.id, {\n up: moveGroupThroughUnits(units, layers, groups, group.id, \"up\") !== null,\n down: moveGroupThroughUnits(units, layers, groups, group.id, \"down\") !== null,\n });\n }\n return result;\n}\n\n/**\n * Which orders {@link sortLayerGroupInPanel} would actually change for each\n * group, so the layer panel can disable a sort that would do nothing.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @param locale Collation locale, as for {@link sortLayerGroupInPanel}.\n * @returns Per-group flags, keyed by group id.\n */\nexport function layerGroupSortability(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n locale?: string,\n): Map<string, Record<LayerGroupSortOrder, boolean>> {\n const units = buildLayerPanelUnits(layers, groups);\n const collator = nameCollator(locale);\n const result = new Map<string, Record<LayerGroupSortOrder, boolean>>();\n for (const group of groups) {\n result.set(group.id, {\n asc: sortGroupThroughUnits(units, layers, groups, group.id, \"asc\", collator) !== null,\n desc: sortGroupThroughUnits(units, layers, groups, group.id, \"desc\", collator) !== null,\n });\n }\n return result;\n}\n","import type { DashboardWidget, LegendConfig, ProjectComment } from \"./types\";\n\n/**\n * Normalize the removed-layer-ids argument into a Set for uniform lookup.\n */\nexport function removedLayerIdSet(layerIds: string | Iterable<string>): Set<string> {\n if (typeof layerIds === \"string\") return new Set([layerIds]);\n if (layerIds instanceof Set) return layerIds as Set<string>;\n return new Set(layerIds);\n}\n\n/**\n * Filter out dashboard widgets whose `layerId` references a removed layer.\n * Returns the same array reference when nothing changes.\n */\nexport function scrubWidgetsForRemovedLayers(\n widgets: DashboardWidget[],\n layerIds: string | Iterable<string>,\n): DashboardWidget[] {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0 || widgets.length === 0) return widgets;\n const filtered = widgets.filter((w) => !removed.has(w.layerId));\n return filtered.length === widgets.length ? widgets : filtered;\n}\n\n/**\n * Drop comments whose anchor is a feature on a removed layer.\n * Point-anchored comments are always kept.\n * Returns the same array reference when nothing changes.\n */\nexport function scrubCommentsForRemovedLayers(\n comments: ProjectComment[],\n layerIds: string | Iterable<string>,\n): ProjectComment[] {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0 || comments.length === 0) return comments;\n const filtered = comments.filter(\n (c) => !(c.anchor.type === \"feature\" && removed.has(c.anchor.layerId)),\n );\n return filtered.length === comments.length ? comments : filtered;\n}\n\n/**\n * Scrub legend config of references to removed layers:\n * - `order`: filter out removed layer ids\n * - `overrides`: drop keys that equal a removed id OR start with `${id}::`\n * - `customEntries`: drop keys that equal a removed layer id but KEEP keys\n * starting with `custom:`\n *\n * Returns the same reference when nothing changes.\n */\nexport function scrubLegendForRemovedLayers(\n legend: LegendConfig,\n layerIds: string | Iterable<string>,\n): LegendConfig {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return legend;\n\n let changed = false;\n\n // --- order ---\n const order = legend.order.filter((id) => !removed.has(id));\n if (order.length !== legend.order.length) changed = true;\n\n // --- overrides ---\n const overrides: Record<string, (typeof legend.overrides)[string]> = {};\n for (const [key, value] of Object.entries(legend.overrides)) {\n const base = key.includes(\"::\") ? key.slice(0, key.indexOf(\"::\")) : key;\n if (removed.has(base)) {\n changed = true;\n } else {\n overrides[key] = value;\n }\n }\n\n // --- customEntries ---\n let customEntries = legend.customEntries;\n if (customEntries) {\n let customEntriesChanged = false;\n const nextCustom: Record<string, (typeof customEntries)[string]> = {};\n for (const [key, value] of Object.entries(customEntries)) {\n if (key.startsWith(\"custom:\")) {\n nextCustom[key] = value;\n } else if (removed.has(key)) {\n customEntriesChanged = true;\n } else {\n nextCustom[key] = value;\n }\n }\n if (customEntriesChanged) {\n customEntries = Object.keys(nextCustom).length > 0 ? nextCustom : undefined;\n changed = true;\n }\n }\n\n if (!changed) return legend;\n return { ...legend, order, overrides, customEntries };\n}\n","/**\n * Print Layout composer settings that belong to a project.\n *\n * The Print Layout dialog composes a printable page out of the current map:\n * a title block, page size and orientation, the cartographic furniture\n * (legend, scale bar, north arrow), optional data blocks, and an atlas /\n * map-series definition. All of that describes the *project's* map document,\n * so it round-trips through `.geolibre.json` (GeoLibre discussion #1992 —\n * before this, reopening a project lost the title, page format and\n * orientation, and the dialog kept showing the previous project's settings).\n *\n * Deliberately **not** in here: everything that is per-session rather than\n * part of the document — the captured map image, export progress, error and\n * clipboard notices, the current atlas page, and the dialog's own chrome\n * (panel widths, dialog size).\n *\n * The unions below are the storage contract. Their app-side counterparts\n * (`PaperSizeId`, `Orientation`, `BodyCorner`, ... in\n * `apps/geolibre-desktop/src/lib/`) are checked against these by ordinary\n * assignment in the dialog's snapshot/restore paths, so adding a paper size or\n * a corner on one side without the other fails the build.\n */\n\nimport { removedLayerIdSet } from \"./layer-ref-scrub\";\n\n/** A page corner an overlay block can be pinned to. */\nexport type PrintLayoutCorner = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n\n/** Which features a data block covers when the atlas paginates the map. */\nexport type PrintLayoutPageFilter = \"all\" | \"contained\" | \"intersecting\";\n\n/** One hand-authored swatch in the composer's user-defined legend. */\nexport interface PrintLayoutLegendEntry {\n id: string;\n label: string;\n color: string;\n}\n\nexport interface PrintLayoutConfig {\n // Title block\n title: string;\n subtitle: string;\n titlePlacement: \"outside\" | \"inside\";\n titleAlign: \"left\" | \"center\" | \"right\";\n\n // Page\n paperSize:\n | \"a4\"\n | \"a3\"\n | \"letter\"\n | \"legal\"\n | \"tabloid\"\n | \"fullhd\"\n | \"hd\"\n | \"uhd4k\"\n | \"square\"\n | \"custom\";\n orientation: \"portrait\" | \"landscape\";\n customWidth: number;\n customHeight: number;\n customUnit: \"mm\" | \"px\";\n pageMargin: \"normal\" | \"narrow\" | \"none\";\n showPageBorder: boolean;\n pageBorderColor: string;\n pageBorderWidth: number;\n\n // Map frame\n mapBorderColor: string;\n mapBorderWidth: number;\n mapBackground: string;\n\n // Map elements\n showTitle: boolean;\n showSubtitle: boolean;\n showLegend: boolean;\n showScaleBar: boolean;\n showNorthArrow: boolean;\n navigationGrouped: boolean;\n showFooter: boolean;\n footerText: string;\n showDate: boolean;\n /** Blank means \"today\", resolved when the page is drawn. */\n dateText: string;\n showAttribution: boolean;\n\n // Colorbar\n showColorbar: boolean;\n colorbarRamp: string;\n /** Kept as typed text so a half-entered value survives a round trip. */\n colorbarMin: string;\n colorbarMax: string;\n colorbarLabel: string;\n colorbarOrientation: \"vertical\" | \"horizontal\";\n colorbarLength: number;\n colorbarPosition: PrintLayoutCorner;\n\n // User-defined legend\n showCustomLegend: boolean;\n customLegendTitle: string;\n customLegendEntries: PrintLayoutLegendEntry[];\n customLegendPosition: PrintLayoutCorner;\n\n // Attribute-table block\n showDataTable: boolean;\n tableLayerId: string;\n tableTitle: string;\n tableColumns: string[];\n tableSortField: string;\n tableSortDesc: boolean;\n tableMaxRows: number;\n tableFitRows: boolean;\n tablePosition: PrintLayoutCorner;\n tablePageFilter: PrintLayoutPageFilter;\n tableFilterToAtlasFeature: boolean;\n\n // Chart block\n showDataChart: boolean;\n chartLayerId: string;\n chartTitle: string;\n chartType: \"bar\" | \"pie\" | \"line\";\n chartCategoryField: string;\n chartAggregation: \"count\" | \"sum\" | \"mean\";\n chartValueField: string;\n chartPosition: PrintLayoutCorner;\n chartPageFilter: PrintLayoutPageFilter;\n\n // Info block (\"title block\" / cartouche)\n showInfoBlock: boolean;\n author: string;\n projectNumber: string;\n crs: string;\n revision: string;\n\n // What the page captures\n captureMode: \"viewport\" | \"extent\";\n /** Drawn print extent as `[west, south, east, north]`; null when unset. */\n extentBbox: [number, number, number, number] | null;\n\n // Atlas / map series\n atlasEnabled: boolean;\n atlasLayerId: string;\n atlasCoverage: \"features\" | \"line\";\n atlasSegmentKm: string;\n atlasNameField: string;\n atlasExtentMode: \"margin\" | \"scale\";\n atlasMarginPct: number;\n atlasMaskEnabled: boolean;\n atlasScale: string;\n atlasSortField: string;\n atlasSortDescending: boolean;\n atlasFilter: string;\n atlasFilenamePattern: string;\n}\n\nconst CORNERS = [\"top-left\", \"top-right\", \"bottom-left\", \"bottom-right\"] as const;\nconst PAGE_FILTERS = [\"all\", \"contained\", \"intersecting\"] as const;\n\n/**\n * Settings a project starts with, and the shape every partial or hand-edited\n * `printLayout` is filled out against. Also the dialog's initial state, so\n * \"new project\" and \"a project saved before this feature\" compose identically.\n */\nexport const DEFAULT_PRINT_LAYOUT: PrintLayoutConfig = {\n title: \"\",\n subtitle: \"\",\n titlePlacement: \"outside\",\n titleAlign: \"center\",\n\n paperSize: \"a4\",\n orientation: \"landscape\",\n customWidth: 1280,\n customHeight: 720,\n customUnit: \"px\",\n pageMargin: \"normal\",\n showPageBorder: false,\n pageBorderColor: \"#111827\",\n pageBorderWidth: 2,\n\n mapBorderColor: \"#9ca3af\",\n mapBorderWidth: 1,\n mapBackground: \"#e5e7eb\",\n\n showTitle: true,\n showSubtitle: true,\n showLegend: true,\n showScaleBar: true,\n showNorthArrow: true,\n navigationGrouped: true,\n showFooter: false,\n footerText: \"\",\n showDate: true,\n dateText: \"\",\n showAttribution: true,\n\n showColorbar: false,\n colorbarRamp: \"viridis\",\n colorbarMin: \"0\",\n colorbarMax: \"100\",\n colorbarLabel: \"\",\n colorbarOrientation: \"vertical\",\n colorbarLength: 34,\n colorbarPosition: \"top-right\",\n\n showCustomLegend: false,\n customLegendTitle: \"Legend\",\n customLegendEntries: [\n { id: \"cl-1\", label: \"Class 1\", color: \"#2563eb\" },\n { id: \"cl-2\", label: \"Class 2\", color: \"#16a34a\" },\n ],\n customLegendPosition: \"top-left\",\n\n showDataTable: false,\n tableLayerId: \"\",\n tableTitle: \"\",\n tableColumns: [],\n tableSortField: \"\",\n tableSortDesc: false,\n tableMaxRows: 10,\n tableFitRows: false,\n tablePosition: \"bottom-left\",\n tablePageFilter: \"contained\",\n tableFilterToAtlasFeature: false,\n\n showDataChart: false,\n chartLayerId: \"\",\n chartTitle: \"\",\n chartType: \"bar\",\n chartCategoryField: \"\",\n chartAggregation: \"count\",\n chartValueField: \"\",\n chartPosition: \"top-right\",\n chartPageFilter: \"contained\",\n\n showInfoBlock: false,\n author: \"\",\n projectNumber: \"\",\n crs: \"\",\n revision: \"\",\n\n captureMode: \"viewport\",\n extentBbox: null,\n\n atlasEnabled: false,\n atlasLayerId: \"\",\n atlasCoverage: \"features\",\n atlasSegmentKm: \"20\",\n atlasNameField: \"\",\n atlasExtentMode: \"margin\",\n atlasMarginPct: 10,\n atlasMaskEnabled: false,\n atlasScale: \"50000\",\n atlasSortField: \"\",\n atlasSortDescending: false,\n atlasFilter: \"\",\n atlasFilenamePattern: \"{atlas.pagenumber}-{atlas.name}\",\n};\n\n/** A fresh copy of the defaults, safe to hand to a store or a component. */\nexport function createDefaultPrintLayout(): PrintLayoutConfig {\n return {\n ...DEFAULT_PRINT_LAYOUT,\n tableColumns: [],\n customLegendEntries: DEFAULT_PRINT_LAYOUT.customLegendEntries.map((entry) => ({ ...entry })),\n };\n}\n\nfunction str(value: unknown, fallback: string): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction bool(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nfunction num(value: unknown, fallback: number, min: number, max: number): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return fallback;\n return Math.min(max, Math.max(min, value));\n}\n\nfunction oneOf<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {\n return typeof value === \"string\" && (allowed as readonly string[]).includes(value)\n ? (value as T)\n : fallback;\n}\n\nfunction stringList(value: unknown, fallback: string[]): string[] {\n if (!Array.isArray(value)) return fallback;\n return value.filter((item): item is string => typeof item === \"string\");\n}\n\nfunction legendEntries(\n value: unknown,\n fallback: PrintLayoutLegendEntry[],\n): PrintLayoutLegendEntry[] {\n if (!Array.isArray(value)) return fallback.map((entry) => ({ ...entry }));\n const entries: PrintLayoutLegendEntry[] = [];\n // The editor keys its swatch rows by id, so two entries sharing one would\n // make an edit to either apply to both. A hand-edited file can omit an id or\n // repeat one, so ids are made unique here rather than trusted.\n const used = new Set<string>();\n for (const [index, item] of value.entries()) {\n if (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n const entry = item as Partial<PrintLayoutLegendEntry>;\n const given = typeof entry.id === \"string\" ? entry.id.trim() : \"\";\n let id = given;\n if (!id || used.has(id)) {\n // Synthesize past whatever is already taken, including ids claimed later\n // in the array, so the fallback cannot collide with an explicit one.\n let candidate = index + 1;\n while (used.has(`cl-${candidate}`) || claimsId(value, index, `cl-${candidate}`)) {\n candidate += 1;\n }\n id = `cl-${candidate}`;\n }\n used.add(id);\n entries.push({\n id,\n label: str(entry.label, \"\"),\n color: str(entry.color, \"#2563eb\"),\n });\n }\n return entries;\n}\n\n/** Whether any entry after `index` explicitly carries `id`. */\nfunction claimsId(value: unknown[], index: number, id: string): boolean {\n for (let i = index + 1; i < value.length; i += 1) {\n const item = value[i];\n if (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n const candidate = (item as Partial<PrintLayoutLegendEntry>).id;\n if (typeof candidate === \"string\" && candidate.trim() === id) return true;\n }\n return false;\n}\n\nfunction extent(value: unknown): [number, number, number, number] | null {\n if (!Array.isArray(value) || value.length !== 4) return null;\n if (!value.every((n) => typeof n === \"number\" && Number.isFinite(n))) return null;\n const [west, south, east, north] = value as number[];\n // The draw tool orders its corners, so an inverted or zero-area box only\n // reaches here from a hand-edited file; capturing it would produce an empty\n // image, so treat it as unset (mirrors `normalizeBounds` in project.ts).\n if (west >= east || south >= north) return null;\n return [west, south, east, north];\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited, possibly partial) `printLayout`\n * into a complete {@link PrintLayoutConfig}, filling every missing or\n * malformed field from {@link DEFAULT_PRINT_LAYOUT}.\n *\n * @param value - The raw `printLayout` value read from a project file.\n * @returns The normalized config, or null when the project carries none, so\n * callers can tell \"absent\" from \"explicitly default\".\n */\nexport function normalizePrintLayoutConfig(value: unknown): PrintLayoutConfig | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const raw = value as Record<string, unknown>;\n const d = DEFAULT_PRINT_LAYOUT;\n return {\n title: str(raw.title, d.title),\n subtitle: str(raw.subtitle, d.subtitle),\n titlePlacement: oneOf(raw.titlePlacement, [\"outside\", \"inside\"] as const, d.titlePlacement),\n titleAlign: oneOf(raw.titleAlign, [\"left\", \"center\", \"right\"] as const, d.titleAlign),\n\n paperSize: oneOf(\n raw.paperSize,\n [\"a4\", \"a3\", \"letter\", \"legal\", \"tabloid\", \"fullhd\", \"hd\", \"uhd4k\", \"square\", \"custom\"],\n d.paperSize,\n ),\n orientation: oneOf(raw.orientation, [\"portrait\", \"landscape\"] as const, d.orientation),\n customWidth: num(raw.customWidth, d.customWidth, 1, 100000),\n customHeight: num(raw.customHeight, d.customHeight, 1, 100000),\n customUnit: oneOf(raw.customUnit, [\"mm\", \"px\"] as const, d.customUnit),\n pageMargin: oneOf(raw.pageMargin, [\"normal\", \"narrow\", \"none\"] as const, d.pageMargin),\n showPageBorder: bool(raw.showPageBorder, d.showPageBorder),\n pageBorderColor: str(raw.pageBorderColor, d.pageBorderColor),\n // A page border is drawn only when `showPageBorder` is on, so a zero width\n // would be an invisible \"visible\" border; the editor's own floor is 1.\n pageBorderWidth: num(raw.pageBorderWidth, d.pageBorderWidth, 1, 20),\n\n mapBorderColor: str(raw.mapBorderColor, d.mapBorderColor),\n mapBorderWidth: num(raw.mapBorderWidth, d.mapBorderWidth, 0, 10),\n mapBackground: str(raw.mapBackground, d.mapBackground),\n\n showTitle: bool(raw.showTitle, d.showTitle),\n showSubtitle: bool(raw.showSubtitle, d.showSubtitle),\n showLegend: bool(raw.showLegend, d.showLegend),\n showScaleBar: bool(raw.showScaleBar, d.showScaleBar),\n showNorthArrow: bool(raw.showNorthArrow, d.showNorthArrow),\n navigationGrouped: bool(raw.navigationGrouped, d.navigationGrouped),\n showFooter: bool(raw.showFooter, d.showFooter),\n footerText: str(raw.footerText, d.footerText),\n showDate: bool(raw.showDate, d.showDate),\n dateText: str(raw.dateText, d.dateText),\n showAttribution: bool(raw.showAttribution, d.showAttribution),\n\n showColorbar: bool(raw.showColorbar, d.showColorbar),\n colorbarRamp: str(raw.colorbarRamp, d.colorbarRamp),\n colorbarMin: str(raw.colorbarMin, d.colorbarMin),\n colorbarMax: str(raw.colorbarMax, d.colorbarMax),\n colorbarLabel: str(raw.colorbarLabel, d.colorbarLabel),\n colorbarOrientation: oneOf(\n raw.colorbarOrientation,\n [\"vertical\", \"horizontal\"] as const,\n d.colorbarOrientation,\n ),\n colorbarLength: num(raw.colorbarLength, d.colorbarLength, 1, 100),\n colorbarPosition: oneOf(raw.colorbarPosition, CORNERS, d.colorbarPosition),\n\n showCustomLegend: bool(raw.showCustomLegend, d.showCustomLegend),\n customLegendTitle: str(raw.customLegendTitle, d.customLegendTitle),\n customLegendEntries: legendEntries(raw.customLegendEntries, d.customLegendEntries),\n customLegendPosition: oneOf(raw.customLegendPosition, CORNERS, d.customLegendPosition),\n\n showDataTable: bool(raw.showDataTable, d.showDataTable),\n tableLayerId: str(raw.tableLayerId, d.tableLayerId),\n tableTitle: str(raw.tableTitle, d.tableTitle),\n tableColumns: stringList(raw.tableColumns, d.tableColumns),\n tableSortField: str(raw.tableSortField, d.tableSortField),\n tableSortDesc: bool(raw.tableSortDesc, d.tableSortDesc),\n tableMaxRows: num(raw.tableMaxRows, d.tableMaxRows, 1, 10000),\n tableFitRows: bool(raw.tableFitRows, d.tableFitRows),\n tablePosition: oneOf(raw.tablePosition, CORNERS, d.tablePosition),\n tablePageFilter: oneOf(raw.tablePageFilter, PAGE_FILTERS, d.tablePageFilter),\n tableFilterToAtlasFeature: bool(raw.tableFilterToAtlasFeature, d.tableFilterToAtlasFeature),\n\n showDataChart: bool(raw.showDataChart, d.showDataChart),\n chartLayerId: str(raw.chartLayerId, d.chartLayerId),\n chartTitle: str(raw.chartTitle, d.chartTitle),\n chartType: oneOf(raw.chartType, [\"bar\", \"pie\", \"line\"] as const, d.chartType),\n chartCategoryField: str(raw.chartCategoryField, d.chartCategoryField),\n chartAggregation: oneOf(\n raw.chartAggregation,\n [\"count\", \"sum\", \"mean\"] as const,\n d.chartAggregation,\n ),\n chartValueField: str(raw.chartValueField, d.chartValueField),\n chartPosition: oneOf(raw.chartPosition, CORNERS, d.chartPosition),\n chartPageFilter: oneOf(raw.chartPageFilter, PAGE_FILTERS, d.chartPageFilter),\n\n showInfoBlock: bool(raw.showInfoBlock, d.showInfoBlock),\n author: str(raw.author, d.author),\n projectNumber: str(raw.projectNumber, d.projectNumber),\n crs: str(raw.crs, d.crs),\n revision: str(raw.revision, d.revision),\n\n captureMode: oneOf(raw.captureMode, [\"viewport\", \"extent\"] as const, d.captureMode),\n extentBbox: extent(raw.extentBbox),\n\n atlasEnabled: bool(raw.atlasEnabled, d.atlasEnabled),\n atlasLayerId: str(raw.atlasLayerId, d.atlasLayerId),\n atlasCoverage: oneOf(raw.atlasCoverage, [\"features\", \"line\"] as const, d.atlasCoverage),\n atlasSegmentKm: str(raw.atlasSegmentKm, d.atlasSegmentKm),\n atlasNameField: str(raw.atlasNameField, d.atlasNameField),\n atlasExtentMode: oneOf(raw.atlasExtentMode, [\"margin\", \"scale\"] as const, d.atlasExtentMode),\n atlasMarginPct: num(raw.atlasMarginPct, d.atlasMarginPct, 0, 100),\n atlasMaskEnabled: bool(raw.atlasMaskEnabled, d.atlasMaskEnabled),\n atlasScale: str(raw.atlasScale, d.atlasScale),\n atlasSortField: str(raw.atlasSortField, d.atlasSortField),\n atlasSortDescending: bool(raw.atlasSortDescending, d.atlasSortDescending),\n atlasFilter: str(raw.atlasFilter, d.atlasFilter),\n atlasFilenamePattern: str(raw.atlasFilenamePattern, d.atlasFilenamePattern),\n };\n}\n\n/**\n * Whether a config is still the untouched default, so a project that never\n * opened the composer serializes without a `printLayout` key and stays\n * byte-identical to one saved before this feature.\n */\nexport function isDefaultPrintLayout(config: PrintLayoutConfig): boolean {\n return printLayoutConfigsEqual(config, DEFAULT_PRINT_LAYOUT);\n}\n\n/**\n * Structural equality for two configs. Used to skip no-op store writes, so\n * merely opening the Print Layout dialog never marks a project dirty.\n */\nexport function printLayoutConfigsEqual(a: PrintLayoutConfig, b: PrintLayoutConfig): boolean {\n if (a === b) return true;\n for (const key of Object.keys(DEFAULT_PRINT_LAYOUT) as (keyof PrintLayoutConfig)[]) {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) || Array.isArray(right)) {\n if (JSON.stringify(left) !== JSON.stringify(right)) return false;\n } else if (left !== right) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Clear each data/atlas block whose layer `missing` rejects. Shared by the two\n * entry points below, which differ only in how they decide what is gone.\n *\n * @returns The same object when every block's layer survives.\n */\nfunction clearBlocksForMissingLayers(\n config: PrintLayoutConfig,\n missing: (layerId: string) => boolean,\n): PrintLayoutConfig {\n const gone = (id: string) => id !== \"\" && missing(id);\n if (!gone(config.tableLayerId) && !gone(config.chartLayerId) && !gone(config.atlasLayerId)) {\n return config;\n }\n return {\n ...config,\n ...(gone(config.tableLayerId) ? { tableLayerId: \"\", showDataTable: false } : {}),\n ...(gone(config.chartLayerId) ? { chartLayerId: \"\", showDataChart: false } : {}),\n ...(gone(config.atlasLayerId) ? { atlasLayerId: \"\", atlasEnabled: false } : {}),\n };\n}\n\n/**\n * Drop references to layers the project no longer carries, so a composer that\n * pointed at a since-deleted layer opens with the block cleared instead of\n * rendering nothing from a dangling id. Mirrors the widget/comment/legend\n * scrubbing in `applyProjectToStore`.\n *\n * @param config - The config to scrub.\n * @param existingLayerIds - Ids of the layers present in the project.\n * @returns The same object when nothing referenced a missing layer.\n */\nexport function scrubPrintLayoutForLayers(\n config: PrintLayoutConfig,\n existingLayerIds: ReadonlySet<string>,\n): PrintLayoutConfig {\n return clearBlocksForMissingLayers(config, (id) => !existingLayerIds.has(id));\n}\n\n/**\n * The delete-time counterpart, matching `scrubLegendForRemovedLayers` and its\n * siblings: called when layers are removed from the open project so the saved\n * file never carries a block pointing at a layer that is already gone.\n *\n * @param config - The config to scrub.\n * @param layerIds - The removed layer id, or ids.\n * @returns The same object when no block referenced a removed layer.\n */\nexport function scrubPrintLayoutForRemovedLayers(\n config: PrintLayoutConfig,\n layerIds: string | Iterable<string>,\n): PrintLayoutConfig {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return config;\n return clearBlocksForMissingLayers(config, (id) => removed.has(id));\n}\n","/**\n * Per-session selection and live readouts: the selected layer and features,\n * the Identify target, the attribute-table filter, and the pointer / camera /\n * GPS readouts the status bar shows. None of it is saved with the project or\n * tracked by undo history.\n */\nimport type { SliceCreator } from \"./types\";\n\n/**\n * Latest live device-GPS fix published by the GPS Tracking tool (issue #1316),\n * read by the status bar readout. Device state, not project state: excluded\n * from undo history (partialize never lists it) and from project files, and\n * deliberately left untouched on project switches.\n */\nexport interface GpsStatusFix {\n lng: number;\n lat: number;\n /** Horizontal accuracy radius in meters. */\n accuracy: number;\n /** Satellites used for the fix, or null when the provider does not report it. */\n satellites: number | null;\n /** Ground speed in m/s, or null when the device doesn't report one. */\n speed: number | null;\n /** Fix time in epoch milliseconds. */\n timestamp: number;\n}\n\n/** Reserved Identify target for querying every visible queryable layer at once. */\nexport const IDENTIFY_ALL_LAYERS_ID = \"__geolibre_identify_all_layers__\";\n\n/** The Identify fields of the store, as set together by an Identify change. */\nexport interface IdentifyState {\n identifyLayerId: string | null;\n identifyLayerIds: string[] | null;\n}\n\n/**\n * Resolve an Identify target against the layers that exist.\n *\n * Shared by the scripting `setIdentify` command and by project load, which\n * applies a project's saved `interaction.identify` (issue #2688).\n *\n * @param target - A layer id, `\"all\"`, a list of layer ids, or null/undefined.\n * @param layerIds - Ids of the layers currently in the project.\n * @returns The store fields to set. Ids that name no layer are dropped; a list\n * that keeps one id becomes a single-layer target, and one that keeps several\n * becomes the all-layers mode restricted to those ids.\n */\nexport function resolveIdentifyTarget(\n target: string | readonly string[] | null | undefined,\n layerIds: Iterable<string>,\n): IdentifyState {\n const off: IdentifyState = { identifyLayerId: null, identifyLayerIds: null };\n if (target === null || target === undefined) return off;\n if (target === \"all\") return { identifyLayerId: IDENTIFY_ALL_LAYERS_ID, identifyLayerIds: null };\n const known = new Set(layerIds);\n if (typeof target === \"string\") {\n return known.has(target) ? { identifyLayerId: target, identifyLayerIds: null } : off;\n }\n const ids = [...new Set(target)].filter((id) => known.has(id));\n if (ids.length === 0) return off;\n if (ids.length === 1) return { identifyLayerId: ids[0], identifyLayerIds: null };\n return { identifyLayerId: IDENTIFY_ALL_LAYERS_ID, identifyLayerIds: ids };\n}\n\n/**\n * Whether the all-layers Identify mode may query a layer.\n *\n * @param layerId - The candidate layer's id.\n * @param identifyLayerIds - The store's `identifyLayerIds` restriction.\n * @returns True when there is no restriction or the layer is in it.\n */\nexport function identifyAllIncludes(\n layerId: string,\n identifyLayerIds: readonly string[] | null | undefined,\n): boolean {\n return !identifyLayerIds || identifyLayerIds.includes(layerId);\n}\n\n/**\n * Drop removed layers from the Identify state.\n *\n * @param state - The current Identify fields.\n * @param removed - Ids of the layers being removed.\n * @returns The Identify fields with those layers gone. A restricted all-layers\n * mode left with one layer narrows to it, and with none turns Identify off.\n */\nexport function identifyStateWithoutLayers(\n state: IdentifyState,\n removed: ReadonlySet<string>,\n): IdentifyState {\n if (state.identifyLayerId !== null && removed.has(state.identifyLayerId)) {\n return { identifyLayerId: null, identifyLayerIds: null };\n }\n // Return fresh fields, never `state` itself: callers spread the result into\n // a store update and pass the whole store as `state`.\n const unchanged = {\n identifyLayerId: state.identifyLayerId,\n identifyLayerIds: state.identifyLayerIds,\n };\n if (!state.identifyLayerIds) return unchanged;\n const ids = state.identifyLayerIds.filter((id) => !removed.has(id));\n if (ids.length === state.identifyLayerIds.length) return unchanged;\n if (ids.length === 0) return { identifyLayerId: null, identifyLayerIds: null };\n if (ids.length === 1) return { identifyLayerId: ids[0], identifyLayerIds: null };\n return { identifyLayerId: state.identifyLayerId, identifyLayerIds: ids };\n}\n\nexport interface SessionSlice {\n /** Local viewing switch; saved layer popup settings are left untouched. */\n hoverTooltipsEnabled: boolean;\n setHoverTooltipsEnabled: (enabled: boolean) => void;\n selectedLayerId: string | null;\n selectedFeatureId: string | null;\n /**\n * Full set of selected feature ids. The attribute table extends the single\n * selection to many rows via Ctrl/Cmd (toggle) and Shift (range). The anchor\n * — `selectedFeatureId` — is the primary/last-clicked feature used for map\n * fit, DuckDB highlight, and scripting, and is always one of these ids (or\n * `null` when the set is empty). A single click leaves exactly one id here.\n */\n selectedFeatureIds: string[];\n /**\n * Store-layer id targeted by Identify, or {@link IDENTIFY_ALL_LAYERS_ID} for\n * the map-level mode that queries every visible queryable layer — vector,\n * DuckDB query, WMS, COG, NetCDF image and time-slider raster alike.\n */\n identifyLayerId: string | null;\n /**\n * Layers the all-layers Identify mode is limited to, or null for every\n * visible queryable layer. Set only alongside {@link IDENTIFY_ALL_LAYERS_ID},\n * by a script or project that names several layers (issue #2688), and\n * cleared by every `setIdentifyLayer` call so the in-app buttons keep their\n * plain meaning.\n */\n identifyLayerIds: string[] | null;\n pointerCoords: [number, number] | null;\n /**\n * Ground elevation in true metres under the pointer, for the status bar\n * (issue #1813). Null when it cannot be resolved — the pointer is off the\n * map, terrain is off and the remote lookup has not answered (or failed), or\n * the active body is not Earth. Set alongside `pointerCoords` by MapCanvas,\n * which owns the map instance the terrain sample comes from.\n */\n pointerElevation: number | null;\n /**\n * Camera height above sea level in metres — Google Earth Pro's \"Eye alt\"\n * (issue #1816). Derived from the camera, so deliberately *not* part of\n * `mapView`: that shape is persisted into the project file, and a stored\n * altitude could only drift from the center/zoom/pitch it is computed from.\n * Null before the map loads, or when MapLibre cannot report it.\n */\n cameraAltitude: number | null;\n /** Live GPS fix for the status bar, or null while GPS tracking is off. */\n gpsStatus: GpsStatusFix | null;\n attributeFilter: string;\n\n setPointerCoords: (coords: [number, number] | null) => void;\n setPointerElevation: (elevation: number | null) => void;\n setCameraAltitude: (altitude: number | null) => void;\n setGpsStatus: (fix: GpsStatusFix | null) => void;\n selectLayer: (id: string | null) => void;\n selectFeature: (id: string | null) => void;\n /**\n * Replace the multi-selection with `ids`. The anchor (`selectedFeatureId`)\n * becomes `anchorId` when provided, otherwise the last id in the list (or\n * `null` when the list is empty).\n */\n selectFeatures: (ids: string[], anchorId?: string | null) => void;\n setIdentifyLayer: (id: string | null) => void;\n /** Set both Identify fields at once (see {@link resolveIdentifyTarget}). */\n setIdentifyState: (state: IdentifyState) => void;\n setAttributeFilter: (filter: string) => void;\n}\n\nexport const createSessionSlice: SliceCreator<SessionSlice> = (set) => ({\n hoverTooltipsEnabled: true,\n setHoverTooltipsEnabled: (enabled) => set({ hoverTooltipsEnabled: enabled }),\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n identifyLayerId: null,\n identifyLayerIds: null,\n pointerCoords: null,\n pointerElevation: null,\n cameraAltitude: null,\n gpsStatus: null,\n attributeFilter: \"\",\n\n setPointerCoords: (coords) =>\n set(coords ? { pointerCoords: coords } : { pointerCoords: null, pointerElevation: null }),\n setPointerElevation: (elevation) => set({ pointerElevation: elevation }),\n setCameraAltitude: (altitude) => set({ cameraAltitude: altitude }),\n setGpsStatus: (fix) => set({ gpsStatus: fix }),\n\n selectLayer: (id) =>\n set({\n selectedLayerId: id,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n }),\n // `\"\"` is a valid feature id; only `null` clears the selection.\n selectFeature: (id) =>\n set({ selectedFeatureId: id, selectedFeatureIds: id === null ? [] : [id] }),\n selectFeatures: (ids, anchorId) =>\n set({\n selectedFeatureIds: ids,\n // Enforce the documented invariant for every caller: the anchor is\n // always a member of the set (or null when empty). A supplied anchor\n // that isn't in `ids` falls back to the last id rather than pointing\n // the map fit / calculator sample at an unselected feature.\n selectedFeatureId:\n anchorId != null && ids.includes(anchorId) ? anchorId : (ids.at(-1) ?? null),\n }),\n setIdentifyLayer: (id) => set({ identifyLayerId: id, identifyLayerIds: null }),\n setIdentifyState: ({ identifyLayerId, identifyLayerIds }) =>\n set({ identifyLayerId, identifyLayerIds }),\n setAttributeFilter: (filter) => set({ attributeFilter: filter }),\n});\n","/**\n * Reference scrubbing shared by the actions that delete layers (`removeLayer`\n * and `removeLayerGroup` with `removeChildren`), so a deleted layer never\n * leaves a dangling id in a story map, a secondary pane, or a join.\n */\nimport { cascadeLayerJoinRefresh } from \"../joins\";\nimport { removedLayerIdSet } from \"../layer-ref-scrub\";\nimport type { GeoLibreLayer, SecondaryMapView, StoryMap } from \"../types\";\n\n/**\n * Strip storymap chapter enter/exit opacity rows that reference any of the\n * removed layer ids. Returns the same reference when nothing changes so\n * callers can avoid unnecessary storymap churn.\n */\nexport function scrubStorymapLayerRefs(\n storymap: StoryMap | null,\n layerIds: string | Iterable<string>,\n): StoryMap | null {\n if (!storymap) return null;\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return storymap;\n let changed = false;\n const chapters = storymap.chapters.map((chapter) => {\n const onChapterEnter = chapter.onChapterEnter.filter((change) => !removed.has(change.layerId));\n const onChapterExit = chapter.onChapterExit.filter((change) => !removed.has(change.layerId));\n if (\n onChapterEnter.length === chapter.onChapterEnter.length &&\n onChapterExit.length === chapter.onChapterExit.length\n ) {\n return chapter;\n }\n changed = true;\n return { ...chapter, onChapterEnter, onChapterExit };\n });\n return changed ? { ...storymap, chapters } : storymap;\n}\n\n/**\n * Drop per-pane visibility overrides for removed layer ids so stale keys do\n * not accumulate (or serialize) in secondary panes after deletion.\n */\nexport function scrubSecondaryPaneLayerVisibility(\n panes: SecondaryMapView[],\n layerIds: string | Iterable<string>,\n): SecondaryMapView[] {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return panes;\n let anyChanged = false;\n const next = panes.map((pane) => {\n let changed = false;\n const layerVisibility: Record<string, boolean> = {};\n for (const [id, visible] of Object.entries(pane.layerVisibility)) {\n if (removed.has(id)) {\n changed = true;\n continue;\n }\n layerVisibility[id] = visible;\n }\n if (!changed) return pane;\n anyChanged = true;\n return { ...pane, layerVisibility };\n });\n return anyChanged ? next : panes;\n}\n\n/** Re-derive layers whose joins consumed any of the removed sources. */\nexport function cascadeJoinRefreshForRemoved(\n layers: GeoLibreLayer[],\n layerIds: string | Iterable<string>,\n): GeoLibreLayer[] {\n let current = layers;\n for (const id of removedLayerIdSet(layerIds)) {\n current = cascadeLayerJoinRefresh(current, id);\n }\n return current;\n}\n","/**\n * Layer-panel folders: create, rename, nest, reorder, sort and delete groups,\n * and move layers between them. `layerGroups` is tracked by undo history\n * except for the `collapsed` flag (see `undo-history.ts`).\n */\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n DEFAULT_LAYER_GROUP_OPACITY,\n normalizeGroupContiguity,\n reorderLayerGroupInPanel,\n sortLayerGroupInPanel,\n type LayerGroupSortOrder,\n} from \"../layer-groups\";\nimport {\n scrubCommentsForRemovedLayers,\n scrubLegendForRemovedLayers,\n scrubWidgetsForRemovedLayers,\n} from \"../layer-ref-scrub\";\nimport { scrubPrintLayoutForRemovedLayers } from \"../print-layout-config\";\nimport { identifyStateWithoutLayers } from \"./session-slice\";\nimport type { LayerGroup } from \"../types\";\nimport {\n cascadeJoinRefreshForRemoved,\n scrubSecondaryPaneLayerVisibility,\n scrubStorymapLayerRefs,\n} from \"./layer-removal\";\nimport type { SliceCreator } from \"./types\";\n\n/**\n * Pick the lowest `Group N` name not already taken, so default names stay\n * unique while still preferring small numbers — starting the search at 1 (not\n * `length + 1`) avoids skipping free low numbers when some groups carry custom\n * names. Group counts are small, so the linear scan is negligible.\n */\nfunction nextDefaultGroupName(groups: LayerGroup[]): string {\n const existing = new Set(groups.map((g) => g.name));\n let n = 1;\n while (existing.has(`Group ${n}`)) n++;\n return `Group ${n}`;\n}\n\nexport interface LayerGroupsSlice {\n layerGroups: LayerGroup[];\n\n addLayerGroup: (name?: string, layerIds?: string[]) => string;\n removeLayerGroup: (id: string, options?: { removeChildren?: boolean }) => void;\n renameLayerGroup: (id: string, name: string) => void;\n setLayerGroupVisibility: (id: string, visible: boolean) => void;\n setLayerGroupOpacity: (id: string, opacity: number) => void;\n toggleLayerGroupCollapsed: (id: string) => void;\n moveLayerToGroup: (\n layerId: string,\n groupId: string | null,\n beforeLayerId?: string | null,\n ) => void;\n moveLayersToGroup: (\n layerIds: string[],\n groupId: string | null,\n beforeLayerId?: string | null,\n ) => void;\n moveLayerGroupToGroup: (id: string, parentId: string | null) => void;\n reorderLayerGroup: (id: string, direction: \"up\" | \"down\") => void;\n /**\n * Sort a group's direct children by name, A to Z or Z to A (top of panel\n * first), collating by `locale` (the app's display language) when given.\n */\n sortLayerGroup: (id: string, order: LayerGroupSortOrder, locale?: string) => void;\n}\n\nexport const createLayerGroupsSlice: SliceCreator<LayerGroupsSlice> = (set, get) => ({\n layerGroups: [],\n\n addLayerGroup: (name, layerIds) => {\n const id = uuidv4();\n set((s) => {\n const group: LayerGroup = {\n id,\n name: name?.trim() || nextDefaultGroupName(s.layerGroups),\n collapsed: false,\n visible: true,\n opacity: DEFAULT_LAYER_GROUP_OPACITY,\n };\n const ids = new Set(layerIds ?? []);\n const layers =\n ids.size > 0\n ? normalizeGroupContiguity(\n s.layers.map((l) => (ids.has(l.id) ? { ...l, groupId: id } : l)),\n )\n : s.layers;\n return {\n layers,\n layerGroups: [...s.layerGroups, group],\n isDirty: true,\n };\n });\n return id;\n },\n\n removeLayerGroup: (id, options) =>\n set((s) => {\n const removeChildren = options?.removeChildren ?? false;\n const removedGroup = s.layerGroups.find((g) => g.id === id);\n if (!removedGroup) return s;\n const groupIds = new Set([id]);\n if (removeChildren) {\n let changed = true;\n while (changed) {\n changed = false;\n for (const group of s.layerGroups) {\n if (group.parentId && groupIds.has(group.parentId) && !groupIds.has(group.id)) {\n groupIds.add(group.id);\n changed = true;\n }\n }\n }\n }\n const removedIds = new Set(\n s.layers.filter((l) => l.groupId && groupIds.has(l.groupId)).map((l) => l.id),\n );\n let layers = removeChildren\n ? s.layers.filter((l) => !l.groupId || !groupIds.has(l.groupId))\n : s.layers.map((l) => (l.groupId === id ? { ...l, groupId: undefined } : l));\n // Match removeLayer: refreshing joins and scrubbing secondary-pane /\n // storymap refs for every deleted child so group delete cannot leave\n // stale joined columns or dangling visibility overrides behind.\n if (removeChildren && removedIds.size > 0) {\n layers = cascadeJoinRefreshForRemoved(layers, removedIds);\n }\n const selectionRemoved =\n removeChildren && s.selectedLayerId !== null && removedIds.has(s.selectedLayerId);\n return {\n layers,\n layerGroups: s.layerGroups\n .filter((g) => !groupIds.has(g.id))\n .map((g) => (g.parentId === id ? { ...g, parentId: removedGroup.parentId } : g)),\n secondaryMapViews: removeChildren\n ? scrubSecondaryPaneLayerVisibility(s.secondaryMapViews, removedIds)\n : s.secondaryMapViews,\n storymap: removeChildren ? scrubStorymapLayerRefs(s.storymap, removedIds) : s.storymap,\n widgets: removeChildren ? scrubWidgetsForRemovedLayers(s.widgets, removedIds) : s.widgets,\n comments: removeChildren\n ? scrubCommentsForRemovedLayers(s.comments, removedIds)\n : s.comments,\n legend: removeChildren ? scrubLegendForRemovedLayers(s.legend, removedIds) : s.legend,\n printLayout: removeChildren\n ? scrubPrintLayoutForRemovedLayers(s.printLayout, removedIds)\n : s.printLayout,\n selectedLayerId: selectionRemoved\n ? (layers[layers.length - 1]?.id ?? null)\n : s.selectedLayerId,\n selectedFeatureId: selectionRemoved ? null : s.selectedFeatureId,\n selectedFeatureIds: selectionRemoved ? [] : s.selectedFeatureIds,\n // Children kept at the top level are still in the project, so they\n // keep their place in the Identify target.\n ...(removeChildren\n ? identifyStateWithoutLayers(s, removedIds)\n : { identifyLayerId: s.identifyLayerId, identifyLayerIds: s.identifyLayerIds }),\n ui: removeChildren\n ? {\n ...s.ui,\n selectByExpressionLayerId:\n s.ui.selectByExpressionLayerId !== null &&\n removedIds.has(s.ui.selectByExpressionLayerId)\n ? null\n : s.ui.selectByExpressionLayerId,\n selectByLocationLayerId:\n s.ui.selectByLocationLayerId !== null &&\n removedIds.has(s.ui.selectByLocationLayerId)\n ? null\n : s.ui.selectByLocationLayerId,\n loadEditorFeaturesLayerId:\n s.ui.loadEditorFeaturesLayerId !== null &&\n removedIds.has(s.ui.loadEditorFeaturesLayerId)\n ? null\n : s.ui.loadEditorFeaturesLayerId,\n }\n : s.ui,\n isDirty: true,\n };\n }),\n\n renameLayerGroup: (id, name) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, name } : g)),\n isDirty: true,\n })),\n\n setLayerGroupVisibility: (id, visible) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, visible } : g)),\n isDirty: true,\n })),\n\n setLayerGroupOpacity: (id, opacity) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) =>\n g.id === id ? { ...g, opacity: Math.min(Math.max(opacity, 0), 1) } : g,\n ),\n isDirty: true,\n })),\n\n // Collapsing/expanding a folder is a UI-panel preference, not a data\n // edit: it is still persisted in the project (folders reopen collapsed),\n // but it does not mark the project dirty and is excluded from undo (see\n // the equality comparator in undo-history.ts) so Ctrl-Z never toggles a\n // folder.\n toggleLayerGroupCollapsed: (id) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, collapsed: !g.collapsed } : g)),\n })),\n\n moveLayerToGroup: (layerId, groupId, beforeLayerId = null) =>\n get().moveLayersToGroup([layerId], groupId, beforeLayerId),\n\n moveLayersToGroup: (layerIds, groupId, beforeLayerId = null) =>\n set((s) => {\n if (groupId && !s.layerGroups.some((g) => g.id === groupId)) return s;\n const requestedIds = new Set(layerIds);\n const moving = s.layers.filter(\n (layer) =>\n requestedIds.has(layer.id) &&\n (beforeLayerId !== null || (layer.groupId ?? null) !== groupId),\n );\n if (moving.length === 0) return s;\n const movingIds = new Set(moving.map((layer) => layer.id));\n const without = s.layers.filter((layer) => !movingIds.has(layer.id));\n const updated = moving.map((layer) => ({\n ...layer,\n groupId: groupId ?? undefined,\n }));\n let index: number;\n if (beforeLayerId && !movingIds.has(beforeLayerId)) {\n const at = without.findIndex((layer) => layer.id === beforeLayerId);\n index = at < 0 ? without.length : at;\n } else if (groupId) {\n let last = -1;\n without.forEach((layer, layerIndex) => {\n if (layer.groupId === groupId) last = layerIndex;\n });\n index = last < 0 ? without.length : last + 1;\n } else {\n index = without.length;\n }\n const next = [...without];\n next.splice(index, 0, ...updated);\n const normalized = normalizeGroupContiguity(next);\n const unchanged = normalized.every(\n (layer, layerIndex) =>\n layer.id === s.layers[layerIndex]?.id && layer.groupId === s.layers[layerIndex]?.groupId,\n );\n if (unchanged) return s;\n return { layers: normalized, isDirty: true };\n }),\n\n moveLayerGroupToGroup: (id, parentId) =>\n set((s) => {\n if (parentId === id) return s;\n const group = s.layerGroups.find((g) => g.id === id);\n if (!group) return s;\n if (parentId && !s.layerGroups.some((g) => g.id === parentId)) return s;\n // Walking upward from the proposed parent must never reach the group\n // being moved, otherwise the assignment would create a cycle.\n const byId = new Map(s.layerGroups.map((g) => [g.id, g]));\n let ancestorId = parentId ?? undefined;\n const visited = new Set<string>();\n while (ancestorId && !visited.has(ancestorId)) {\n if (ancestorId === id) return s;\n visited.add(ancestorId);\n ancestorId = byId.get(ancestorId)?.parentId;\n }\n const nextParentId = parentId ?? undefined;\n if (group.parentId === nextParentId) return s;\n return {\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, parentId: nextParentId } : g)),\n isDirty: true,\n };\n }),\n\n // A folder that owns no layers has no position in `layers` to move, so\n // the panel order it takes part in lives across both arrays and the move\n // writes both (GeoLibre#1739).\n reorderLayerGroup: (id, direction) =>\n set((s) => {\n const moved = reorderLayerGroupInPanel(s.layers, s.layerGroups, id, direction);\n if (!moved) return s;\n return { layers: moved.layers, layerGroups: moved.groups, isDirty: true };\n }),\n\n sortLayerGroup: (id, order, locale) =>\n set((s) => {\n const sorted = sortLayerGroupInPanel(s.layers, s.layerGroups, id, order, locale);\n if (!sorted) return s;\n return { layers: sorted.layers, layerGroups: sorted.groups, isDirty: true };\n }),\n});\n","/**\n * A bare Android SAF document ID, as the browser `File.name` carries after a\n * native import: a known volume prefix (`primary:`, `home:`, `raw:`, `msf:`, or a\n * removable `XXXX-XXXX:` volume), or any provider prefix followed by a folder\n * separator. Ordinary encoded filenames such as `Report%3AQ1.geojson` do not match.\n */\nconst SAF_DOCUMENT_ID =\n /^(?:(?:primary|home|raw|msf|[0-9a-f]{4}-[0-9a-f]{4})%3a|[^%/\\\\:]+%3a.*%2f)/i;\n\n/** Extract a display filename without changing the source used for file access. */\nexport function localFileName(path: string): string {\n const isUri = /^(?:content|file):\\/\\//i.test(path);\n const value = isUri ? path.split(/[?#]/)[0] : path;\n const segment = value.split(/[/\\\\]/).pop() ?? value;\n // Do not decode ordinary filenames: percent escapes can be literal text.\n if (!isUri && (!SAF_DOCUMENT_ID.test(segment) || /[/\\\\]/.test(path))) {\n return segment;\n }\n let decoded = segment;\n try {\n decoded = decodeURIComponent(segment);\n } catch {\n return segment;\n }\n // Strip the provider's volume prefix before taking the final path component.\n return (\n decoded\n .replace(/^[^/\\\\:]+:/, \"\")\n .split(/[/\\\\]/)\n .pop() ?? decoded\n );\n}\n\n/**\n * Whether a layer name was derived from its local source filename (with or\n * without the extension) rather than supplied explicitly by a caller.\n */\nexport function isSourceDerivedLayerName(name: string, sourcePath: string): boolean {\n const fileName = localFileName(sourcePath);\n return name === fileName || name === fileName.replace(/\\.[^.]+$/, \"\");\n}\n\n/** Keep imported layers distinguishable, including within a multi-file import. */\nexport function uniqueImportedLayerName(name: string, names: Iterable<string>): string {\n const existing = new Set(names);\n let candidate = name;\n let suffix = 2;\n while (existing.has(candidate)) candidate = `${name}_${suffix++}`;\n return candidate;\n}\n","import type { FeatureCollection } from \"geojson\";\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer, type LayerStyle } from \"./types\";\n\n/**\n * Initial styling for a newly added vector layer (issue #1519).\n *\n * {@link DEFAULT_LAYER_STYLE} stays exactly what it is — the schema fallback\n * that fills gaps when a partial style is read back from a project file. It is\n * deliberately NOT changed here: every load path spreads it under a saved\n * style, so editing it would silently restyle existing projects.\n *\n * What this module adds is a separate decision made only at *add* time: give\n * each new layer its own color, and pick sizes that suit the geometry it\n * actually contains, so a stack of freshly loaded layers is legible without\n * anyone opening the Style panel.\n */\n\n/**\n * Qualitative colors cycled across newly added layers, chosen to stay distinct\n * at low opacity over both light and dark basemaps.\n *\n * Distinct from the classification palette the Style panel uses for the classes\n * *within* one layer: this one separates layers from each other, and the two\n * are free to diverge.\n */\nexport const LAYER_PALETTE = [\n \"#3b82f6\", // blue — matches the historical default, so the first layer looks unchanged\n \"#ef4444\", // red\n \"#22c55e\", // green\n \"#a855f7\", // purple\n \"#f97316\", // orange\n \"#06b6d4\", // cyan\n \"#ec4899\", // pink\n \"#84cc16\", // lime\n] as const;\n\n/** How much darker an outline is than its fill (0 = black, 1 = unchanged). */\nconst STROKE_DARKEN = 0.55;\n\n/** Darken a `#rrggbb` color toward black by `factor`. */\nexport function darkenHex(hex: string, factor: number): string {\n const match = /^#([0-9a-f]{6})$/i.exec(hex.trim());\n if (!match) return hex;\n const value = Number.parseInt(match[1], 16);\n const channels = [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];\n return `#${channels\n .map((channel) =>\n Math.max(0, Math.min(255, Math.round(channel * factor)))\n .toString(16)\n .padStart(2, \"0\"),\n )\n .join(\"\")}`;\n}\n\n/** Case-insensitive palette lookup, returning the canonical (lowercase) entry. */\nfunction paletteEntryOf(color: string | undefined): string | undefined {\n const normalized = (color ?? \"\").trim().toLowerCase();\n return LAYER_PALETTE.find((entry) => entry === normalized);\n}\n\n/**\n * True when a style's colors are a palette assignment {@link initialLayerStyle}\n * made: a palette fill *paired with* the outline derived from it.\n *\n * The pairing is what makes this precise. Testing the fill alone — or the layer\n * type — would miscount every layer built straight from\n * {@link DEFAULT_LAYER_STYLE}, whose `fillColor` happens to be\n * `LAYER_PALETTE[0]`: rasters, tilesets, video overlays, and the non-spatial\n * attribute tables the delimited-text source adds. None of those paint from a\n * collection, so none should reserve blue and push the next added layer to red.\n * They carry the schema's own `strokeColor`, not one derived from the fill, so\n * this test leaves them out.\n */\nfunction wearsPaletteColor(style: LayerStyle | undefined): boolean {\n const entry = paletteEntryOf(style?.fillColor);\n if (!entry) return false;\n return (style?.strokeColor ?? \"\").trim().toLowerCase() === darkenHex(entry, STROKE_DARKEN);\n}\n\n/**\n * Pick the next layer color: the first palette entry no current layer is\n * already using, so deleting a layer frees its color back up rather than\n * leaving the cycle permanently offset. Falls back to cycling by layer count\n * once every entry is in use.\n *\n * @param layers - The project's current layers. Only those actually wearing a\n * palette assignment count; see {@link wearsPaletteColor}.\n * @returns A `#rrggbb` color from {@link LAYER_PALETTE}.\n */\nexport function nextLayerPaletteColor(layers: readonly GeoLibreLayer[]): string {\n const styled = layers.filter((layer) => wearsPaletteColor(layer.style));\n const used = new Set(styled.map((layer) => layer.style.fillColor.trim().toLowerCase()));\n const free = LAYER_PALETTE.find((color) => !used.has(color));\n return free ?? LAYER_PALETTE[styled.length % LAYER_PALETTE.length];\n}\n\n/** The geometry family a layer is mostly made of. */\nexport type DominantGeometry = \"point\" | \"line\" | \"polygon\" | \"mixed\";\n\n/** Features sampled when deciding a collection's dominant geometry. */\nconst GEOMETRY_SAMPLE_SIZE = 500;\n\n/**\n * Classify a collection by the geometry family most of its features carry,\n * reading at most {@link GEOMETRY_SAMPLE_SIZE} features so a very large layer\n * does not pay a full scan for a styling default.\n *\n * @param geojson - The collection to inspect.\n * @returns The dominant family, or `\"mixed\"` when nothing has a clear majority\n * (or the collection is empty).\n */\nexport function dominantGeometry(geojson: FeatureCollection | undefined): DominantGeometry {\n const features = geojson?.features ?? [];\n if (!features.length) return \"mixed\";\n\n const counts = { point: 0, line: 0, polygon: 0 };\n const sampled = Math.min(features.length, GEOMETRY_SAMPLE_SIZE);\n for (let index = 0; index < sampled; index++) {\n switch (features[index]?.geometry?.type) {\n case \"Point\":\n case \"MultiPoint\":\n counts.point += 1;\n break;\n case \"LineString\":\n case \"MultiLineString\":\n counts.line += 1;\n break;\n case \"Polygon\":\n case \"MultiPolygon\":\n counts.polygon += 1;\n break;\n default:\n break;\n }\n }\n\n if (!counts.point && !counts.line && !counts.polygon) return \"mixed\";\n const [family, count] = Object.entries(counts).sort((a, b) => b[1] - a[1])[0] as [\n \"point\" | \"line\" | \"polygon\",\n number,\n ];\n // A clear majority of everything sampled, not just of the geometries this\n // switch understands: two points among three GeometryCollections is not a\n // point layer, so `sampled` is the denominator rather than the matched total.\n // And a majority, not merely the largest slice — a 40/35/25 split has no\n // geometry whose sizing suits the layer.\n return count / sampled > 0.5 ? family : \"mixed\";\n}\n\n/**\n * Per-geometry size and opacity overrides.\n *\n * One set of numbers cannot suit all three families: an opacity that lets a\n * basemap read through a polygon makes a point look washed out, and a stroke\n * heavy enough to see a line is a heavy outline on a polygon.\n */\nconst GEOMETRY_DEFAULTS: Record<DominantGeometry, Partial<LayerStyle>> = {\n // Solid and slightly smaller: a point is read as a dot, not a translucent blob.\n point: { circleRadius: 5, fillOpacity: 0.9, strokeWidth: 1 },\n // A line's stroke *is* the symbol, so it carries the weight.\n line: { strokeWidth: 2.5, fillOpacity: 1 },\n // Translucent enough that the basemap underneath stays readable.\n polygon: { fillOpacity: 0.45, strokeWidth: 1.5 },\n mixed: {},\n};\n\nexport interface InitialLayerStyleOptions {\n /** The layer's data, used to pick geometry-appropriate sizes. */\n geojson?: FeatureCollection;\n /** Current project layers, so the new layer gets an unused palette color. */\n layers?: readonly GeoLibreLayer[];\n /** Style values that win over the computed defaults (e.g. a restored style). */\n overrides?: Partial<LayerStyle>;\n}\n\n/**\n * Build the style a newly added vector layer starts with: the schema defaults,\n * plus its own palette color and geometry-appropriate sizing.\n *\n * @param options - Data and project context to derive the style from.\n * @returns A complete {@link LayerStyle}.\n */\nexport function initialLayerStyle(options: InitialLayerStyleOptions = {}): LayerStyle {\n const { geojson, layers = [], overrides } = options;\n const fillColor = nextLayerPaletteColor(layers);\n return {\n ...DEFAULT_LAYER_STYLE,\n fillColor,\n strokeColor: darkenHex(fillColor, STROKE_DARKEN),\n ...GEOMETRY_DEFAULTS[dominantGeometry(geojson)],\n ...overrides,\n };\n}\n\n/**\n * True when a layer still wears exactly what {@link initialLayerStyle} gave it.\n *\n * The renderer mode alone is too weak a test for \"untouched\": nudging the fill,\n * outline, or opacity leaves the mode on `\"single\"`, and a project restored\n * from disk can carry deliberate single-symbol styling. Both cases should stop\n * the Style panel volunteering suggestions, which is what this gates.\n *\n * The fill color cannot be compared against a fixed value — it is whichever\n * palette entry the layer drew — so the test is that it is *still a palette\n * entry* with the outline derived from it, which a hand-picked color will not\n * satisfy.\n *\n * @param style - The layer's current style.\n * @param geojson - Its data, so the geometry-derived sizes can be recomputed.\n * @returns `true` when nothing about the as-added look has been changed.\n */\nexport function isInitialLayerStyle(style: LayerStyle, geojson?: FeatureCollection): boolean {\n // Authored symbology, whatever the paint fields say.\n if (style.simpleStyleEnabled) return false;\n if (style.vectorStyleMode !== \"single\") return false;\n if (style.pointRenderer !== \"single\") return false;\n if ((style.vectorRules?.length ?? 0) > 0) return false;\n\n if (!wearsPaletteColor(style)) return false;\n\n const expected = { ...DEFAULT_LAYER_STYLE, ...GEOMETRY_DEFAULTS[dominantGeometry(geojson)] };\n return (\n style.fillOpacity === expected.fillOpacity &&\n style.strokeWidth === expected.strokeWidth &&\n style.circleRadius === expected.circleRadius\n );\n}\n","// The Style Manager's data layer (issue #1294): subset extraction, untrusted\n// input normalization, the shareable JSON bundle format, and the built-in\n// presets. UI-free so both the desktop app and tests consume it directly.\n\nimport {\n DEFAULT_LAYER_STYLE,\n type LabelStyle,\n type LayerStyle,\n type LayerType,\n type StyleLibraryEntry,\n type StyleLibraryEntryKind,\n} from \"./types\";\n\n/**\n * Layer types whose symbology is driven by the vector {@link LayerStyle}\n * fields and can therefore receive a Style Manager entry (or a style-file\n * import). The single source of truth for every entry point — the Style\n * Manager dialog, the Style panel header button, and the layer context menu —\n * so a future vector-stylable type cannot be added to one gate and missed in\n * another, leaving a dead-end entry point.\n */\nexport function isStyleLibraryTargetLayer(type: LayerType): boolean {\n return type === \"geojson\" || type === \"vector-tiles\";\n}\n\n/** `type` discriminator of an exported Style Manager bundle file. */\nexport const STYLE_LIBRARY_BUNDLE_TYPE = \"geolibre-style-library\";\n\n/** Current bundle format version, for forward-compatible readers. */\nexport const STYLE_LIBRARY_BUNDLE_VERSION = 1;\n\nconst STYLE_LIBRARY_ENTRY_KINDS: readonly StyleLibraryEntryKind[] = [\n \"style\",\n \"symbol\",\n \"labels\",\n \"ramp\",\n];\n\n/**\n * The {@link LayerStyle} keys a `\"symbol\"` entry captures: fill, stroke,\n * point/circle, marker, and fill-pattern symbology. Deliberately excludes the\n * renderer configuration (`vector*`), labels, zoom range, and raster\n * adjustments so a symbol preset restyles a layer without dismantling its\n * classification or labeling.\n */\nexport const SYMBOL_STYLE_KEYS = [\n \"fillColor\",\n \"strokeColor\",\n \"strokeWidth\",\n \"strokeWidthUnit\",\n \"fillOpacity\",\n \"circleRadius\",\n \"fillPattern\",\n \"fillPatternColor\",\n \"fillPatternSvg\",\n \"markerEnabled\",\n \"markerShape\",\n \"markerColor\",\n \"markerSize\",\n \"markerSvg\",\n] as const satisfies readonly (keyof LayerStyle)[];\n\n/**\n * The {@link LayerStyle} keys a `\"ramp\"` entry captures. The classified\n * attribute (`vectorStyleProperty`) and the concrete stops are deliberately\n * excluded: they are data-specific, so a ramp preset stays reusable across\n * layers with different attributes and value ranges.\n */\nexport const RAMP_STYLE_KEYS = [\n \"vectorStyleColorRamp\",\n \"vectorStyleClassCount\",\n \"vectorStyleClassificationScheme\",\n] as const satisfies readonly (keyof LayerStyle)[];\n\n/**\n * Extract the {@link LayerStyle} subset a Style Manager entry of `kind`\n * stores, deep-cloned so later edits to the live layer cannot mutate the\n * saved entry.\n *\n * @param style - The source layer's full style.\n * @param kind - Which subset to capture.\n * @returns The cloned subset to store in a {@link StyleLibraryEntry}.\n */\nexport function extractStyleLibraryStyle(\n style: LayerStyle,\n kind: StyleLibraryEntryKind,\n): Partial<LayerStyle> {\n if (kind === \"labels\") {\n return structuredClone({ labels: style.labels ?? DEFAULT_LAYER_STYLE.labels });\n }\n if (kind === \"symbol\" || kind === \"ramp\") {\n const keys: readonly (keyof LayerStyle)[] =\n kind === \"symbol\" ? SYMBOL_STYLE_KEYS : RAMP_STYLE_KEYS;\n const subset: Partial<LayerStyle> = {};\n for (const key of keys) {\n const value = style[key] ?? DEFAULT_LAYER_STYLE[key];\n (subset as Record<string, unknown>)[key] = value;\n }\n return structuredClone(subset);\n }\n return structuredClone({ ...DEFAULT_LAYER_STYLE, ...style });\n}\n\n/**\n * Allowed values for the enum-typed {@link LayerStyle} fields, so a\n * hand-edited file cannot smuggle an unknown mode/shape/unit string through\n * the primitive `typeof` check and into the renderer. Fields absent here are\n * free-form strings (colors, expressions, property names, SVG markup).\n */\nconst STYLE_ENUM_VALUES: Partial<Record<keyof LayerStyle, readonly string[]>> = {\n strokeWidthUnit: [\"pixels\", \"meters\"],\n vectorStyleMode: [\"single\", \"graduated\", \"categorized\", \"rule-based\", \"expression\"],\n fillPattern: [\"none\", \"hatch\", \"cross-hatch\", \"horizontal\", \"vertical\", \"dots\", \"svg\"],\n markerShape: [\"circle\", \"square\", \"triangle\", \"diamond\", \"star\", \"cross\", \"pin\", \"custom\"],\n pointRenderer: [\"single\", \"heatmap\", \"cluster\"],\n // Union of the graduated and categorized schemes the Style panel offers.\n vectorStyleClassificationScheme: [\n \"equal-interval\",\n \"quantile\",\n \"natural-breaks\",\n \"top-values\",\n \"alphabetical\",\n \"first-values\",\n ],\n};\n\n/** Allowed values for the enum-typed {@link LabelStyle} fields. */\nconst LABEL_ENUM_VALUES: Partial<Record<keyof LabelStyle, readonly string[]>> = {\n placement: [\"point\", \"line\"],\n anchor: [\n \"center\",\n \"left\",\n \"right\",\n \"top\",\n \"bottom\",\n \"top-left\",\n \"top-right\",\n \"bottom-left\",\n \"bottom-right\",\n ],\n transform: [\"none\", \"uppercase\", \"lowercase\"],\n dedupe: [\"off\", \"unique\", \"concatenate\"],\n};\n\n/**\n * Coerce an untrusted style patch (from a hand-edited project file or an\n * imported bundle) into a safe {@link LayerStyle} subset: unknown keys are\n * dropped, values whose primitive type disagrees with the default (or that\n * are non-finite numbers, or strings outside an enum field's allowed set) are\n * dropped, and a present `labels` object is completed against the default\n * label style so applying it never leaves the renderer reading undefined\n * label fields.\n *\n * @param value - The raw `style` value from JSON.\n * @returns The sanitized subset (empty when nothing usable survives).\n */\nexport function sanitizeLayerStylePatch(value: unknown): Partial<LayerStyle> {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return {};\n const source = value as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(DEFAULT_LAYER_STYLE) as (keyof LayerStyle)[]) {\n if (!(key in source)) continue;\n const given = source[key];\n const fallback = DEFAULT_LAYER_STYLE[key];\n if (key === \"labels\") {\n if (given && typeof given === \"object\" && !Array.isArray(given)) {\n const givenLabels = given as Record<string, unknown>;\n const labels: Record<string, unknown> = {};\n for (const labelKey of Object.keys(DEFAULT_LAYER_STYLE.labels) as (keyof LabelStyle)[]) {\n const labelFallback = DEFAULT_LAYER_STYLE.labels[labelKey];\n const labelGiven = givenLabels[labelKey];\n const labelAllowed = LABEL_ENUM_VALUES[labelKey];\n const labelValid =\n typeof labelGiven === typeof labelFallback &&\n (typeof labelGiven !== \"number\" || Number.isFinite(labelGiven)) &&\n (!labelAllowed || labelAllowed.includes(labelGiven as string));\n labels[labelKey] = labelValid ? labelGiven : labelFallback;\n }\n out.labels = labels as unknown as LabelStyle;\n }\n continue;\n }\n if (Array.isArray(fallback)) {\n if (!Array.isArray(given)) continue;\n // Validate array elements too, not just \"is an array\": malformed stop\n // or rule objects would otherwise survive to the renderer. Rebuild each\n // element from its known fields so extra keys are stripped along the\n // way; an invalid optional label is dropped rather than the whole stop.\n if (key === \"vectorStyleStops\") {\n out[key] = given.flatMap((item) => {\n if (!item || typeof item !== \"object\") return [];\n const stop = item as Record<string, unknown>;\n if (\n (typeof stop.value !== \"string\" &&\n !(typeof stop.value === \"number\" && Number.isFinite(stop.value))) ||\n typeof stop.color !== \"string\"\n ) {\n return [];\n }\n return [\n {\n value: stop.value,\n color: stop.color,\n ...(typeof stop.label === \"string\" ? { label: stop.label } : {}),\n },\n ];\n });\n } else if (key === \"vectorRules\") {\n out[key] = given.flatMap((item) => {\n if (!item || typeof item !== \"object\") return [];\n const rule = item as Record<string, unknown>;\n if (\n typeof rule.id !== \"string\" ||\n typeof rule.label !== \"string\" ||\n typeof rule.filter !== \"string\" ||\n typeof rule.color !== \"string\" ||\n typeof rule.isElse !== \"boolean\"\n ) {\n return [];\n }\n // Optional per-rule fields (enabled toggle, zoom range, nesting,\n // symbol overrides) carry through only when well-typed; a malformed\n // optional field is dropped rather than the whole rule.\n const optionalNumber = (value: unknown) =>\n typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n // Out-of-domain numbers are dropped (the rule inherits the layer\n // value) rather than clamped, so a nonsense hand-edited value never\n // silently becomes a different-but-valid override.\n const optionalInRange = (value: unknown, min: number, max: number) => {\n const parsed = optionalNumber(value);\n return parsed !== undefined && parsed >= min && parsed <= max ? parsed : undefined;\n };\n const optionalString = (value: unknown) =>\n typeof value === \"string\" ? value : undefined;\n const minZoom = optionalInRange(rule.minZoom, 0, 24);\n const maxZoom = optionalInRange(rule.maxZoom, 0, 24);\n const parentId = optionalString(rule.parentId);\n const strokeColor = optionalString(rule.strokeColor);\n const strokeWidth = optionalInRange(rule.strokeWidth, 0, Number.POSITIVE_INFINITY);\n const fillOpacity = optionalInRange(rule.fillOpacity, 0, 1);\n const circleRadius = optionalInRange(rule.circleRadius, 0, Number.POSITIVE_INFINITY);\n return [\n {\n id: rule.id,\n label: rule.label,\n filter: rule.filter,\n color: rule.color,\n isElse: rule.isElse,\n ...(rule.enabled === false ? { enabled: false } : {}),\n ...(minZoom !== undefined ? { minZoom } : {}),\n ...(maxZoom !== undefined ? { maxZoom } : {}),\n ...(parentId !== undefined ? { parentId } : {}),\n ...(strokeColor !== undefined ? { strokeColor } : {}),\n ...(strokeWidth !== undefined ? { strokeWidth } : {}),\n ...(fillOpacity !== undefined ? { fillOpacity } : {}),\n ...(circleRadius !== undefined ? { circleRadius } : {}),\n },\n ];\n });\n } else {\n out[key] = given;\n }\n continue;\n }\n if (typeof given !== typeof fallback) continue;\n if (typeof given === \"number\" && !Number.isFinite(given)) continue;\n const allowed = STYLE_ENUM_VALUES[key];\n if (allowed && !allowed.includes(given as string)) continue;\n out[key] = given;\n }\n return structuredClone(out) as Partial<LayerStyle>;\n}\n\n/**\n * Restrict a sanitized style patch to the key set its entry `kind` declares,\n * mirroring what {@link extractStyleLibraryStyle} captures on save. Without\n * this, a hand-edited project file or imported bundle could declare\n * `kind: \"symbol\"` with extra full-style fields in the payload, and applying\n * that \"Symbol only\" entry (a partial merge) would silently change labels or\n * renderer settings on the target layer.\n */\nfunction restrictStylePatchToKind(\n style: Partial<LayerStyle>,\n kind: StyleLibraryEntryKind,\n): Partial<LayerStyle> {\n if (kind === \"style\") return style;\n const allowed: readonly (keyof LayerStyle)[] =\n kind === \"symbol\" ? SYMBOL_STYLE_KEYS : kind === \"ramp\" ? RAMP_STYLE_KEYS : [\"labels\"];\n const out: Partial<LayerStyle> = {};\n for (const key of allowed) {\n if (key in style) {\n (out as Record<string, unknown>)[key] = style[key];\n }\n }\n return out;\n}\n\n/**\n * Coerce an untrusted (hand-edited project file or imported bundle) entries\n * array into valid {@link StyleLibraryEntry} records. Drops entries without a\n * usable id or name, de-duplicates by id, coerces an unknown kind to\n * `\"style\"`, and sanitizes each style payload (restricted to the declared\n * kind's key set); entries whose payload sanitizes to nothing are dropped.\n *\n * @param value - The raw `styleLibrary` / bundle `entries` value.\n * @returns Normalized, de-duplicated entries (empty when none survive).\n */\nexport function normalizeStyleLibraryEntries(value: unknown): StyleLibraryEntry[] {\n if (!Array.isArray(value)) return [];\n const entries: StyleLibraryEntry[] = [];\n const seen = new Set<string>();\n for (const raw of value) {\n if (!raw || typeof raw !== \"object\") continue;\n const candidate = raw as Partial<StyleLibraryEntry>;\n const id = typeof candidate.id === \"string\" ? candidate.id.trim() : \"\";\n const name = typeof candidate.name === \"string\" ? candidate.name.trim() : \"\";\n if (!id || !name || seen.has(id)) continue;\n const kind = STYLE_LIBRARY_ENTRY_KINDS.includes(candidate.kind as StyleLibraryEntryKind)\n ? (candidate.kind as StyleLibraryEntryKind)\n : \"style\";\n const style = restrictStylePatchToKind(sanitizeLayerStylePatch(candidate.style), kind);\n if (Object.keys(style).length === 0) continue;\n seen.add(id);\n const tags = Array.isArray(candidate.tags)\n ? [\n ...new Set(\n candidate.tags\n .filter((tag): tag is string => typeof tag === \"string\")\n .map((tag) => tag.trim())\n .filter((tag) => tag !== \"\"),\n ),\n ]\n : [];\n entries.push({\n id,\n name,\n kind,\n tags,\n style,\n updatedAt: typeof candidate.updatedAt === \"string\" ? candidate.updatedAt : \"\",\n });\n }\n return entries;\n}\n\n/**\n * Serialize Style Manager entries into the shareable bundle JSON written by\n * the Export action and read back by {@link parseStyleLibrary}.\n *\n * @param entries - The entries to export.\n * @returns Pretty-printed bundle JSON.\n */\nexport function serializeStyleLibrary(entries: StyleLibraryEntry[]): string {\n return JSON.stringify(\n {\n type: STYLE_LIBRARY_BUNDLE_TYPE,\n version: STYLE_LIBRARY_BUNDLE_VERSION,\n entries,\n },\n null,\n 2,\n );\n}\n\n/**\n * Parse a Style Manager bundle produced by {@link serializeStyleLibrary} (a\n * bare entries array is also accepted, so hand-authored files work). Entries\n * are normalized through {@link normalizeStyleLibraryEntries}.\n *\n * @param json - The bundle file content.\n * @returns The normalized entries.\n * @throws Error when the JSON is not a style-library bundle or holds no\n * usable entries.\n */\nexport function parseStyleLibrary(json: string): StyleLibraryEntry[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"Not a valid style library file (invalid JSON).\");\n }\n let rawEntries: unknown = null;\n if (Array.isArray(parsed)) {\n rawEntries = parsed;\n } else if (\n parsed &&\n typeof parsed === \"object\" &&\n (parsed as { type?: unknown }).type === STYLE_LIBRARY_BUNDLE_TYPE\n ) {\n // Refuse bundles from a newer format rather than misreading them with v1\n // semantics (e.g. coercing kinds this version does not know to \"style\").\n // Bare arrays stay accepted for hand-authored files.\n if ((parsed as { version?: unknown }).version !== STYLE_LIBRARY_BUNDLE_VERSION) {\n throw new Error(\"Unsupported style library version.\");\n }\n rawEntries = (parsed as { entries?: unknown }).entries;\n }\n if (rawEntries === null) {\n throw new Error(\"Not a valid style library file.\");\n }\n const entries = normalizeStyleLibraryEntries(rawEntries);\n if (entries.length === 0) {\n throw new Error(\"The style library file holds no usable entries.\");\n }\n return entries;\n}\n\n/**\n * Create a fresh unique id for a new library entry.\n *\n * @returns A random id (UUID when available).\n */\nexport function createStyleLibraryEntryId(): string {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n return crypto.randomUUID();\n }\n return `style-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Built-in presets shown in the Style Manager so the library is not empty on\n * first open (issue #1294). Read-only: they are not persisted, cannot be\n * deleted, and their ids are namespaced with `preset-` so user entries never\n * collide. Names are plain English by the same convention as the ramp labels\n * in `color-ramp.ts`.\n */\nexport const BUILT_IN_STYLE_PRESETS: readonly StyleLibraryEntry[] = [\n {\n id: \"preset-boundary-outline\",\n name: \"Boundary outline\",\n kind: \"symbol\",\n tags: [\"boundary\", \"basemap\"],\n style: {\n fillColor: \"#4b5563\",\n fillOpacity: 0,\n strokeColor: \"#4b5563\",\n strokeWidth: 1.5,\n strokeWidthUnit: \"pixels\",\n fillPattern: \"none\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-boundary-bold\",\n name: \"Boundary outline (bold)\",\n kind: \"symbol\",\n tags: [\"boundary\", \"basemap\"],\n style: {\n fillColor: \"#111827\",\n fillOpacity: 0,\n strokeColor: \"#111827\",\n strokeWidth: 2.5,\n strokeWidthUnit: \"pixels\",\n fillPattern: \"none\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-muted-polygon\",\n name: \"Muted polygon\",\n kind: \"symbol\",\n tags: [\"basemap\", \"polygon\"],\n style: {\n fillColor: \"#e5e7eb\",\n fillOpacity: 0.35,\n strokeColor: \"#9ca3af\",\n strokeWidth: 1,\n fillPattern: \"none\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-highlight\",\n name: \"Highlight\",\n kind: \"symbol\",\n tags: [\"polygon\", \"selection\"],\n style: {\n fillColor: \"#facc15\",\n fillOpacity: 0.45,\n strokeColor: \"#d97706\",\n strokeWidth: 2,\n fillPattern: \"none\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-water\",\n name: \"Water\",\n kind: \"symbol\",\n tags: [\"polygon\", \"hydrology\"],\n style: {\n fillColor: \"#60a5fa\",\n fillOpacity: 0.5,\n strokeColor: \"#2563eb\",\n strokeWidth: 1,\n fillPattern: \"none\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-marker-pin\",\n name: \"Pin marker\",\n kind: \"symbol\",\n tags: [\"point\", \"marker\"],\n style: {\n markerEnabled: true,\n markerShape: \"pin\",\n markerColor: \"#dc2626\",\n markerSize: 22,\n circleRadius: 6,\n fillColor: \"#dc2626\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-labels-dark\",\n name: \"Labels (dark on light halo)\",\n kind: \"labels\",\n tags: [\"labels\"],\n style: {\n labels: {\n ...DEFAULT_LAYER_STYLE.labels,\n enabled: true,\n size: 12,\n color: \"#111827\",\n haloColor: \"#ffffff\",\n haloWidth: 1.5,\n },\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-ramp-viridis-5\",\n name: \"Viridis (5 classes, quantile)\",\n kind: \"ramp\",\n tags: [\"ramp\"],\n style: {\n vectorStyleColorRamp: \"viridis\",\n vectorStyleClassCount: 5,\n vectorStyleClassificationScheme: \"quantile\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-ramp-blues-5\",\n name: \"Blues (5 classes, equal interval)\",\n kind: \"ramp\",\n tags: [\"ramp\"],\n style: {\n vectorStyleColorRamp: \"blues\",\n vectorStyleClassCount: 5,\n vectorStyleClassificationScheme: \"equal-interval\",\n },\n updatedAt: \"\",\n },\n {\n id: \"preset-ramp-spectral-7\",\n name: \"Spectral (7 classes, quantile)\",\n kind: \"ramp\",\n tags: [\"ramp\"],\n style: {\n vectorStyleColorRamp: \"spectral\",\n vectorStyleClassCount: 7,\n vectorStyleClassificationScheme: \"quantile\",\n },\n updatedAt: \"\",\n },\n];\n","// Copy/paste layer symbology between layers (issue #1339), QGIS-style. The\n// transient clipboard slot and its two store actions live in `store.ts`; this\n// module holds the UI-free data layer so both the desktop app and tests consume\n// the same extract/apply logic.\n\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer, type LayerStyle } from \"./types\";\nimport { isStyleLibraryTargetLayer } from \"./style-library\";\n\n/**\n * `metadata.sourceKind` marking a deck.gl COG raster managed by the\n * maplibre-gl-raster control. Mirrors `RASTER_SOURCE_KIND` in\n * `@geolibre/plugins` (which `@geolibre/core` must not depend on). The clipboard\n * test imports the real constant directly from the module that defines it\n * (`packages/plugins/src/plugins/raster-layer-sync.ts`, not the package barrel,\n * whose browser-only plugins crash under the Node test runner) and asserts a\n * layer tagged with it classifies as `\"raster\"`, so a rename of the upstream\n * constant makes the test fail here rather than silently dropping raster\n * copy/paste.\n */\nconst RASTER_SOURCE_KIND = \"maplibre-gl-raster\";\n\n/**\n * Which copy/paste style family a layer belongs to. Vector-styled layers and\n * deck.gl rasters store their symbology in different places (the {@link\n * LayerStyle} bag vs `metadata.rasterState`), so a paste only applies between\n * layers of the same family.\n */\nexport type LayerStyleClipboardKind = \"vector\" | \"raster\";\n\n/**\n * The `metadata.rasterState` keys a raster style paste carries over: the\n * appearance of the render (colormap, stretch range, gamma, nodata), but not\n * the data selection (`mode`/`bands`/`index`), which stays with the target so\n * pasting a style never points a raster at a band it does not have.\n */\nexport const RASTER_APPEARANCE_STATE_KEYS = [\n \"colormap\",\n \"reversed\",\n \"rescale\",\n \"nodata\",\n \"stretch\",\n \"gamma\",\n] as const;\n\n/**\n * Fields every clipboard entry carries, regardless of style family. Layer\n * `opacity` is deliberately excluded: like QGIS \"Paste Style\" and GeoLibre's\n * own Style Manager, copy/paste transfers symbology only and leaves the\n * target's opacity alone (opacity is a per-layer render setting, not style).\n *\n * {@link LayerStyle.blendMode} is deliberately *included*, even though the\n * Layers panel puts the Blend menu beside the opacity slider. The two differ in\n * kind despite sitting together: opacity is a transient \"turn this down so I\n * can see through it\" knob, while a blend mode is a cartographic decision about\n * how the layer reads against the map -- colour over a hillshade is a look, and\n * copying a look should bring it. QGIS agrees; its layer style, and so its\n * Paste Style, carries the blending mode. `tests/layer-style-clipboard.test.ts`\n * pins both halves of this so neither is changed by accident.\n */\ninterface CopiedLayerStyleBase {\n /** Source layer name, surfaced in the paste tooltip and status message. */\n sourceName: string;\n}\n\n/** A copied vector layer style: the whole {@link LayerStyle} bag. */\nexport interface CopiedVectorStyle extends CopiedLayerStyleBase {\n kind: \"vector\";\n /** Deep-cloned full vector layer style. */\n style: LayerStyle;\n}\n\n/**\n * A copied deck.gl raster style: the `metadata.rasterState` appearance and the\n * classification symbology. There is no `style` field — the vector\n * {@link LayerStyle} bag holds nothing a deck.gl raster renders from.\n */\nexport interface CopiedRasterStyle extends CopiedLayerStyleBase {\n kind: \"raster\";\n /** Raster visualization state (`metadata.rasterState`). */\n rasterState?: Record<string, unknown>;\n /** Whether the source carried a `metadata.rasterSymbology`. */\n hasRasterSymbology: boolean;\n /** Raster classification symbology (`metadata.rasterSymbology`). */\n rasterSymbology?: unknown;\n}\n\n/**\n * A layer's symbology captured for the paste clipboard. A discriminated union\n * on {@link LayerStyleClipboardKind} so raster-only fields cannot be read off a\n * vector entry (or vice versa); a paste only applies between layers of the same\n * family.\n */\nexport type CopiedLayerStyle = CopiedVectorStyle | CopiedRasterStyle;\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * The copy/paste style family of a layer, or `null` when the layer has no\n * copyable symbology. Deck.gl rasters are `\"raster\"`; the vector-styled layers\n * the Style Manager targets are `\"vector\"`.\n *\n * @param layer - The layer to classify.\n * @returns The style family, or `null`.\n */\nexport function copyableLayerStyleKind(layer: GeoLibreLayer): LayerStyleClipboardKind | null {\n if (layer.metadata.sourceKind === RASTER_SOURCE_KIND) return \"raster\";\n if (isStyleLibraryTargetLayer(layer.type)) return \"vector\";\n return null;\n}\n\n/**\n * Snapshot a layer's style for the clipboard, deep-cloned so later edits to the\n * live layer cannot mutate the copied entry.\n *\n * @param layer - The source layer to copy from.\n * @returns The clipboard entry, or `null` when the layer has no copyable\n * symbology.\n */\nexport function extractCopiedLayerStyle(layer: GeoLibreLayer): CopiedLayerStyle | null {\n const kind = copyableLayerStyleKind(layer);\n if (!kind) return null;\n if (kind === \"vector\") {\n return {\n kind,\n sourceName: layer.name,\n style: structuredClone({ ...DEFAULT_LAYER_STYLE, ...layer.style }),\n };\n }\n // A raster entry never carries the vector `style` bag, so the full-style\n // clone is skipped here.\n const rasterState = layer.metadata.rasterState;\n const rasterSymbology = layer.metadata.rasterSymbology;\n // Only a real symbology object counts as \"has symbology\". A missing or null\n // value (the latter only reachable via a hand-edited project file) both mean\n // \"none\", so the paste clears the target's symbology rather than writing a\n // literal null onto it — symmetric with the delete path in applyCopiedLayerStyle.\n const hasRasterSymbology = isPlainObject(rasterSymbology);\n return {\n kind,\n sourceName: layer.name,\n rasterState: isPlainObject(rasterState) ? structuredClone(rasterState) : undefined,\n hasRasterSymbology,\n ...(hasRasterSymbology ? { rasterSymbology: structuredClone(rasterSymbology) } : {}),\n };\n}\n\n/**\n * Build the {@link GeoLibreLayer} patch that pastes a copied style onto a\n * target layer, or `null` when the target's style family does not match the\n * clipboard entry's (so the caller can disable the paste action).\n *\n * @param target - The layer to paste onto.\n * @param copied - The clipboard entry.\n * @returns A partial layer patch to merge, or `null` when incompatible.\n */\nexport function applyCopiedLayerStyle(\n target: GeoLibreLayer,\n copied: CopiedLayerStyle,\n): Partial<GeoLibreLayer> | null {\n if (copyableLayerStyleKind(target) !== copied.kind) return null;\n if (copied.kind === \"vector\") {\n // The whole style is applied verbatim, including attribute-bound fields\n // (`vectorStyleProperty` and its stops/rules, `labels.field`,\n // `extrusionHeightProperty`, `proportionalSizeProperty`, `diagramFields`).\n // This is deliberate QGIS \"Paste Style\" behavior and matches the Style\n // Manager's full-\"style\" preset: a target lacking those attributes just\n // renders that facet inert (no classification / labels), which the user\n // re-points, rather than the paste silently dropping parts of the style.\n return { style: structuredClone(copied.style) };\n }\n // Merge the appearance keys onto the target's existing rasterState, keeping\n // its own `mode`/`bands`/`index` so the paste restyles the layer without\n // repointing it at the source's band selection.\n const targetState = isPlainObject(target.metadata.rasterState) ? target.metadata.rasterState : {};\n const source = copied.rasterState ?? {};\n const expectedRescaleLength = rescaleEntryCount(targetState);\n const mergedState: Record<string, unknown> = { ...targetState };\n for (const key of RASTER_APPEARANCE_STATE_KEYS) {\n if (!(key in source)) continue;\n // `rescale` is a per-channel min/max array whose length is set by the\n // render mode (one entry for single/index, one per band for rgb), not by\n // `bands.length` (index mode holds two operand bands but a single-entry\n // rescale). Copying a rescale whose length disagrees with the target's mode\n // — e.g. an RGB source's 3 entries onto a single-band target — would leave a\n // rescale that no longer matches the preserved render mode, the same \"band\n // it does not have\" hazard one level down. Skip it then and let the target\n // keep its own stretch. A null source rescale (auto) is safe to carry over.\n if (\n key === \"rescale\" &&\n Array.isArray(source.rescale) &&\n source.rescale.length !== expectedRescaleLength\n ) {\n continue;\n }\n mergedState[key] = structuredClone(source[key]);\n }\n const metadata: Record<string, unknown> = { ...target.metadata, rasterState: mergedState };\n if (copied.hasRasterSymbology) {\n metadata.rasterSymbology = structuredClone(copied.rasterSymbology);\n } else {\n delete metadata.rasterSymbology;\n }\n return { metadata };\n}\n\n/**\n * Number of `rescale` entries a rasterState of this render mode expects: one\n * per band for `\"rgb\"`, a single entry for `\"single\"` and `\"index\"` (the\n * latter stretches one derived index even though it reads two operand bands).\n */\nfunction rescaleEntryCount(state: Record<string, unknown>): number {\n if (state.mode === \"rgb\") return Array.isArray(state.bands) ? state.bands.length : 3;\n return 1;\n}\n","// Export every layer's style to one JSON file and apply such a file back by\n// layer name: Project → Export → Export Layer Styles, Project → Import →\n// Import Layer Styles, and the Startup setting that restyles each newly added\n// layer whose name matches an entry. A style here is exactly what copy/paste\n// style carries (see `layer-style-clipboard.ts`), so an exported entry restyles\n// a layer the same way pasting a copied style onto it would.\n\nimport {\n applyCopiedLayerStyle,\n type CopiedLayerStyle,\n copyableLayerStyleKind,\n extractCopiedLayerStyle,\n type LayerStyleClipboardKind,\n RASTER_APPEARANCE_STATE_KEYS,\n} from \"./layer-style-clipboard\";\nimport { sanitizeLayerStylePatch } from \"./style-library\";\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer, type LayerStyle } from \"./types\";\n\n/** `type` tag identifying a layer styles file. */\nexport const LAYER_STYLES_FILE_TYPE = \"geolibre-layer-styles\";\n\n/** Format version written by {@link serializeLayerStylesFile}. */\nexport const LAYER_STYLES_FILE_VERSION = 1;\n\n/** One layer's style in a layer styles file, keyed by the layer's name. */\nexport type LayerStyleFileEntry =\n | {\n /** Name of the layer the style was exported from; the match key. */\n layerName: string;\n kind: \"vector\";\n /** The full vector {@link LayerStyle} bag. */\n style: LayerStyle;\n }\n | {\n layerName: string;\n kind: \"raster\";\n /** Raster visualization state (`metadata.rasterState`). */\n rasterState?: Record<string, unknown>;\n /** Raster classification symbology (`metadata.rasterSymbology`). */\n rasterSymbology?: Record<string, unknown>;\n };\n\n/** The on-disk layer styles document. */\nexport interface LayerStylesFile {\n type: typeof LAYER_STYLES_FILE_TYPE;\n version: typeof LAYER_STYLES_FILE_VERSION;\n styles: LayerStyleFileEntry[];\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Capture the style of every layer that has one. Layers without copyable\n * symbology (basemap tiles, 3D tiles, ...) are skipped. Layers that share a\n * name are all written, in stack order; the first one wins when the file is\n * applied (see {@link findLayerStyleEntryIndex}).\n *\n * @param layers - The layers to export, in store (bottom-to-top) order.\n * @returns The style entries.\n */\nexport function extractLayerStyleEntries(layers: readonly GeoLibreLayer[]): LayerStyleFileEntry[] {\n const entries: LayerStyleFileEntry[] = [];\n for (const layer of layers) {\n const copied = extractCopiedLayerStyle(layer);\n if (!copied) continue;\n if (copied.kind === \"vector\") {\n entries.push({ layerName: layer.name, kind: \"vector\", style: copied.style });\n } else {\n entries.push({\n layerName: layer.name,\n kind: \"raster\",\n ...(copied.rasterState ? { rasterState: copied.rasterState } : {}),\n ...(isPlainObject(copied.rasterSymbology)\n ? { rasterSymbology: copied.rasterSymbology }\n : {}),\n });\n }\n }\n return entries;\n}\n\n/**\n * Serialize style entries into the JSON written by Export Layer Styles.\n *\n * @param entries - The entries to write.\n * @returns Pretty-printed file content.\n */\nexport function serializeLayerStylesFile(entries: readonly LayerStyleFileEntry[]): string {\n const file: LayerStylesFile = {\n type: LAYER_STYLES_FILE_TYPE,\n version: LAYER_STYLES_FILE_VERSION,\n styles: [...entries],\n };\n return JSON.stringify(file, null, 2);\n}\n\nconst RASTER_STRETCHES: readonly unknown[] = [\"linear\", \"log\", \"sqrt\"];\nconst RASTER_NODATA_MODES: readonly unknown[] = [\"off\", \"auto\"];\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\n/**\n * Validators for the raster appearance keys a style file may carry, by the\n * types `maplibre-gl-raster`'s `RasterLayerState` declares for them.\n */\nconst RASTER_APPEARANCE_VALIDATORS: Record<\n (typeof RASTER_APPEARANCE_STATE_KEYS)[number],\n (value: unknown) => boolean\n> = {\n colormap: (value) => typeof value === \"string\" && value.length > 0,\n reversed: (value) => typeof value === \"boolean\",\n rescale: (value) =>\n value === null ||\n (Array.isArray(value) &&\n // An empty array is not a meaningful rescale (as in savedRasterState).\n value.length > 0 &&\n value.every(\n (range) =>\n Array.isArray(range) &&\n range.length === 2 &&\n isFiniteNumber(range[0]) &&\n isFiniteNumber(range[1]),\n )),\n nodata: (value) => isFiniteNumber(value) || RASTER_NODATA_MODES.includes(value),\n stretch: (value) => RASTER_STRETCHES.includes(value),\n gamma: (value) => isFiniteNumber(value) && value > 0,\n};\n\n/**\n * Keep only the raster appearance keys a style applies, each with a value of\n * the type the raster renderer expects. The data selection (`mode`, `bands`,\n * ...) is dropped too: a style never applies it.\n */\nfunction sanitizeRasterAppearanceState(state: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const key of RASTER_APPEARANCE_STATE_KEYS) {\n if (key in state && RASTER_APPEARANCE_VALIDATORS[key](state[key])) {\n out[key] = structuredClone(state[key]);\n }\n }\n return out;\n}\n\n/**\n * Coerce untrusted entries (a hand-edited file, or a copy kept in settings)\n * into clean ones. An entry without a name or a known kind is dropped; a\n * vector style is sanitized and completed against the default style, and a\n * raster state keeps only well-typed appearance keys, so applying an entry\n * never hands a renderer an unknown, missing or wrong-typed field.\n *\n * @param value - The raw `styles` array.\n * @returns The usable entries.\n */\nexport function normalizeLayerStyleEntries(value: unknown): LayerStyleFileEntry[] {\n if (!Array.isArray(value)) return [];\n const entries: LayerStyleFileEntry[] = [];\n for (const item of value) {\n if (!isPlainObject(item)) continue;\n const layerName = typeof item.layerName === \"string\" ? item.layerName : \"\";\n if (!layerName.trim()) continue;\n if (item.kind === \"vector\") {\n if (!isPlainObject(item.style)) continue;\n entries.push({\n layerName,\n kind: \"vector\",\n style: { ...structuredClone(DEFAULT_LAYER_STYLE), ...sanitizeLayerStylePatch(item.style) },\n });\n } else if (item.kind === \"raster\") {\n entries.push({\n layerName,\n kind: \"raster\",\n ...(isPlainObject(item.rasterState)\n ? { rasterState: sanitizeRasterAppearanceState(item.rasterState) }\n : {}),\n // The symbology's schema lives in `@geolibre/plugins`, which validates\n // it on every read (`savedRasterSymbology`), as it does for a project\n // file; a malformed one renders as unclassified there.\n ...(isPlainObject(item.rasterSymbology)\n ? { rasterSymbology: structuredClone(item.rasterSymbology) }\n : {}),\n });\n }\n }\n return entries;\n}\n\n/**\n * Parse a file written by {@link serializeLayerStylesFile}.\n *\n * @param json - The file content.\n * @returns The normalized entries.\n * @throws Error when the content is not a layer styles file, comes from a\n * newer format version, or holds no usable entries.\n */\nexport function parseLayerStylesFile(json: string): LayerStyleFileEntry[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"Not a valid layer styles file (invalid JSON).\");\n }\n if (!isPlainObject(parsed) || parsed.type !== LAYER_STYLES_FILE_TYPE) {\n throw new Error(\"Not a valid layer styles file.\");\n }\n // Refuse a newer format rather than misread it with this version's rules.\n if (parsed.version !== LAYER_STYLES_FILE_VERSION) {\n throw new Error(\"Unsupported layer styles file version.\");\n }\n const entries = normalizeLayerStyleEntries(parsed.styles);\n if (entries.length === 0) {\n throw new Error(\"The layer styles file holds no usable styles.\");\n }\n return entries;\n}\n\nfunction nameKey(name: string): string {\n return name.trim().toLocaleLowerCase();\n}\n\n/**\n * Find the entry for a layer name: an exact match first, then one that differs\n * only in case or surrounding whitespace, so `Roads` still picks up a style\n * saved for `roads`. The first match in file order wins.\n *\n * @param entries - The style entries.\n * @param layerName - The layer name to look up.\n * @param kind - When given, only entries of this style family are considered,\n * so a raster style saved under a shared name does not shadow the vector one.\n * @returns The matching entry's index in `entries`, or -1.\n */\nexport function findLayerStyleEntryIndex(\n entries: readonly LayerStyleFileEntry[],\n layerName: string,\n kind?: LayerStyleClipboardKind,\n): number {\n const ofKind = (entry: LayerStyleFileEntry) => !kind || entry.kind === kind;\n const exact = entries.findIndex((entry) => ofKind(entry) && entry.layerName === layerName);\n if (exact >= 0) return exact;\n const key = nameKey(layerName);\n return entries.findIndex((entry) => ofKind(entry) && nameKey(entry.layerName) === key);\n}\n\nfunction toCopiedLayerStyle(entry: LayerStyleFileEntry): CopiedLayerStyle {\n if (entry.kind === \"vector\") {\n return { kind: \"vector\", sourceName: entry.layerName, style: entry.style };\n }\n return {\n kind: \"raster\",\n sourceName: entry.layerName,\n rasterState: entry.rasterState,\n hasRasterSymbology: entry.rasterSymbology !== undefined,\n ...(entry.rasterSymbology !== undefined ? { rasterSymbology: entry.rasterSymbology } : {}),\n };\n}\n\n/** A layer restyled from a style entry, and the entry it took. */\nexport interface LayerStyleMatch {\n /** Index of the applied entry in the entries array. */\n entryIndex: number;\n /** The patch that applies it. */\n patch: Partial<GeoLibreLayer>;\n}\n\n/**\n * Build the patch that restyles a layer from the entry matching its name, with\n * the same semantics as pasting a copied style: a vector style replaces the\n * whole style bag, a raster style merges its appearance keys and keeps the\n * layer's own band selection, and layer opacity is left alone.\n *\n * @param layer - The layer to restyle.\n * @param entries - The style entries.\n * @returns The matched entry and patch, or `null` when no entry of the layer's\n * style family matches its name (or the layer has no stylable symbology).\n */\nexport function matchLayerStyleEntry(\n layer: GeoLibreLayer,\n entries: readonly LayerStyleFileEntry[],\n): LayerStyleMatch | null {\n const kind = copyableLayerStyleKind(layer);\n if (!kind) return null;\n const entryIndex = findLayerStyleEntryIndex(entries, layer.name, kind);\n if (entryIndex < 0) return null;\n const patch = applyCopiedLayerStyle(layer, toCopiedLayerStyle(entries[entryIndex]));\n return patch ? { entryIndex, patch } : null;\n}\n","/**\n * The layer stack: adding, removing, editing, restyling and reordering layer\n * records, plus the copy/paste style clipboard. `layers` is tracked by undo\n * history. `removeLayer` also scrubs every other slice's references to the\n * deleted layer, so it reads and writes the whole state through `set`.\n */\nimport type { FeatureCollection } from \"geojson\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { isSourceDerivedLayerName, uniqueImportedLayerName } from \"../file-name\";\nimport { initialLayerStyle } from \"../layer-defaults\";\nimport { normalizeGroupContiguity } from \"../layer-groups\";\nimport {\n scrubCommentsForRemovedLayers,\n scrubLegendForRemovedLayers,\n scrubWidgetsForRemovedLayers,\n} from \"../layer-ref-scrub\";\nimport {\n applyCopiedLayerStyle,\n type CopiedLayerStyle,\n extractCopiedLayerStyle,\n} from \"../layer-style-clipboard\";\nimport { matchLayerStyleEntry, type LayerStyleFileEntry } from \"../layer-style-file\";\nimport { applyJoinsToLayer, cascadeLayerJoinRefresh } from \"../joins\";\nimport { normalizeLayerDescriptiveMetadata } from \"../layer-descriptive-metadata\";\nimport { scrubPrintLayoutForRemovedLayers } from \"../print-layout-config\";\nimport { identifyStateWithoutLayers } from \"./session-slice\";\nimport {\n DEFAULT_LAYER_STYLE,\n type AddTileLayerOptions,\n type AttributeFormConfig,\n type EditorTrackingConfig,\n type GeoLibreLayer,\n type LayerJoin,\n type LayerDescriptiveMetadata,\n type LayerPopupConfig,\n type LayerQuickFilter,\n type LayerStyle,\n type LayerVirtualField,\n} from \"../types\";\nimport { hasSimpleStyleProperties } from \"../vector-color\";\nimport {\n cascadeJoinRefreshForRemoved,\n scrubSecondaryPaneLayerVisibility,\n scrubStorymapLayerRefs,\n} from \"./layer-removal\";\nimport type { SliceCreator } from \"./types\";\n\n/**\n * The layer types {@link AppState.addTileLayer} accepts. Each renders through\n * the raster tile path, which is why the layer's `source.type` is always\n * `\"raster\"`.\n */\nconst RASTER_TILE_LAYER_TYPES: ReadonlySet<string> = new Set<\n NonNullable<AddTileLayerOptions[\"type\"]>\n>([\"xyz\", \"wms\", \"wmts\", \"raster\"]);\n\nexport interface LayersSlice {\n layers: GeoLibreLayer[];\n\n addLayer: (layer: GeoLibreLayer, beforeLayerId?: string | null) => void;\n removeLayer: (id: string) => void;\n updateLayer: (id: string, patch: Partial<GeoLibreLayer>) => void;\n setLayerVisibility: (id: string, visible: boolean) => void;\n setLayerOpacity: (id: string, opacity: number) => void;\n setLayerStyle: (id: string, style: Partial<LayerStyle>) => void;\n /**\n * Transient clipboard holding a layer's symbology, captured by\n * {@link copyLayerStyle} and applied by {@link pasteLayerStyle} (copy/paste\n * styles, issue #1339). Runtime-only: excluded from undo history\n * (`partialize` never lists it) and from the saved project. `null` until a\n * style is copied this session. Cleared by `newProject`/`loadProject` so a\n * paste can't apply an entry from a different project; it deliberately\n * survives undo/redo within a project (it holds a deep snapshot, not a live\n * layer reference, so a paste stays valid even if the source layer is undone\n * away — only the displayed source name may then be stale).\n */\n copiedLayerStyle: CopiedLayerStyle | null;\n /**\n * Snapshot the given layer's style into {@link copiedLayerStyle} so it can be\n * pasted onto a compatible layer. No-op when the layer is missing or has no\n * copyable symbology (leaving any prior clipboard entry untouched). Returns\n * `true` when a style was captured, so callers can skip the confirmation on a\n * no-op.\n */\n copyLayerStyle: (id: string) => boolean;\n /**\n * Apply the {@link copiedLayerStyle} clipboard entry onto the given layer.\n * No-op when the clipboard is empty, the layer is missing, or the entry's\n * style family does not match the target layer's. Returns `true` when the\n * style was applied, so callers can skip the confirmation on a no-op.\n */\n pasteLayerStyle: (id: string) => boolean;\n /**\n * Restyle layers from layer styles file entries matched by layer name\n * (Import Layer Styles, and the Startup setting's auto-apply). Each matched\n * layer is patched the way {@link pasteLayerStyle} would patch it, all in a\n * single store update so the import is one undo step.\n *\n * @param entries - The parsed style entries.\n * @param layerIds - Restrict the restyle to these layers; every layer when\n * omitted.\n * @returns Each restyled layer's id with the index of the entry it took, in\n * stack order.\n */\n applyLayerStyleEntries: (\n entries: readonly LayerStyleFileEntry[],\n layerIds?: readonly string[],\n ) => { layerId: string; entryIndex: number }[];\n /**\n * Replace a layer's persistent attribute joins and immediately re-derive its\n * joined columns (strip what the previous joins added, apply the new list).\n * Pass an empty array to detach every join and restore the base attributes.\n */\n setLayerJoins: (id: string, joins: LayerJoin[]) => void;\n /**\n * Replace the layer's Attribute Form designer configuration (per-field edit\n * widgets, constraints, conditional visibility). Pass `undefined` to remove\n * the form config entirely.\n */\n setLayerAttributeForm: (id: string, attributeForm: AttributeFormConfig | undefined) => void;\n /**\n * Replace the layer's popup/tooltip design (which fields the Identify popup\n * shows, in what order and under what labels, plus the hover tooltip). Pass\n * `undefined` to restore the default full-property dump.\n */\n setLayerPopup: (id: string, popup: LayerPopupConfig | undefined) => void;\n /**\n * Replace the layer's user-authored catalog metadata (title, abstract,\n * keywords, license, contact, lineage, temporal extent, links). The record\n * is normalized first; one that cleans to nothing (or `undefined`) removes\n * the metadata entirely. One call is one undo step.\n */\n setLayerDescriptiveMetadata: (id: string, metadata: LayerDescriptiveMetadata | undefined) => void;\n /**\n * Replace the layer's editor tracking configuration (whether creation/edit\n * author and timestamp columns are maintained, and under which names). Pass\n * `undefined` to drop the configuration entirely.\n */\n setLayerEditorTracking: (id: string, editorTracking: EditorTrackingConfig | undefined) => void;\n /**\n * Replace a layer's virtual fields and immediately re-derive its computed\n * columns (strip what the previous fields added, evaluate the new list).\n * Pass an empty array to detach every virtual field.\n */\n setLayerVirtualFields: (id: string, fields: LayerVirtualField[]) => void;\n /**\n * Replace a layer's quick filters (issue #2114). The controls persist with\n * the project and are compiled to a MapLibre filter at sync time, so this\n * only stores state — nothing re-derives the layer's data. Pass an empty\n * array to remove every control.\n */\n setLayerQuickFilters: (id: string, filters: LayerQuickFilter[]) => void;\n /** Set or clear the project-persisted expression filter for a layer. */\n setLayerFilterExpression: (id: string, expression: unknown[] | null) => void;\n reorderLayer: (id: string, direction: \"up\" | \"down\") => void;\n moveLayer: (id: string, targetIndex: number) => void;\n moveLayersRelative: (\n layerIds: string[],\n targetLayerId: string,\n position: \"above\" | \"below\",\n ) => void;\n addGeoJsonLayer: (\n name: string,\n geojson: FeatureCollection,\n sourcePath?: string,\n beforeLayerId?: string | null,\n ) => string;\n /**\n * Add a georeferenced image overlay (a MapLibre `image` source rendered as a\n * raster layer) from an image URL and its four corner coordinates, and return\n * its id. Used for KML/KMZ `<GroundOverlay>` imports; the layer persists and\n * renders exactly like a Raster Georeferencer overlay. Corners are `[lng,\n * lat]` in top-left, top-right, bottom-right, bottom-left order.\n */\n addImageOverlayLayer: (\n name: string,\n source: { url: string; coordinates: [number, number][] },\n options?: {\n opacity?: number;\n bounds?: [number, number, number, number];\n sourcePath?: string;\n /** Initial visibility (default true); a time-slider frame past the first\n * starts hidden. */\n visible?: boolean;\n /** Epoch-ms time bounds of a KML `<TimeSpan>`/`<TimeStamp>` frame; the\n * Time Slider toggles this frame's visibility by the current date. */\n timeSpan?: { begin: number | null; end: number | null };\n /**\n * What produced the overlay, e.g. a NetCDF grid baked to pixels. Defaults\n * to the KML ground overlay this was first written for; panels gate their\n * per-source controls on it.\n */\n sourceKind?: string;\n /** Extra metadata merged onto the layer (e.g. a symbology record). */\n metadata?: Record<string, unknown>;\n },\n beforeLayerId?: string | null,\n ) => string;\n /**\n * Add a native raster tile layer (XYZ, WMS, or WMTS) from one or more tile\n * URL templates and return its id. The layer appears in the Layers panel and\n * persists with the project exactly like a layer added through the Add Data\n * dialog, so callers (e.g. an external plugin) do not have to touch MapLibre\n * directly. See {@link AddTileLayerOptions}.\n */\n addTileLayer: (\n name: string,\n options: AddTileLayerOptions,\n beforeLayerId?: string | null,\n ) => string;\n}\n\nexport const createLayersSlice: SliceCreator<LayersSlice> = (set, get) => ({\n layers: [],\n copiedLayerStyle: null,\n\n addLayer: (layer, beforeLayerId = null) =>\n set((s) => {\n const layers = [...s.layers];\n // Plugin source identifiers (for example pmtiles://) are not local files.\n const { sourcePath } = layer;\n const localSource =\n sourcePath &&\n (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(sourcePath) || /^(content|file):\\/\\//i.test(sourcePath));\n // Only filename-derived names are deduplicated; an explicit name (an\n // embedded document title, a tool output label, a user-typed name)\n // is kept as supplied.\n if (localSource && isSourceDerivedLayerName(layer.name, sourcePath)) {\n layer = {\n ...layer,\n name: uniqueImportedLayerName(\n layer.name,\n layers.map((item) => item.name),\n ),\n };\n }\n const beforeIndex = beforeLayerId ? layers.findIndex((l) => l.id === beforeLayerId) : -1;\n const layerWithBeforeId =\n beforeLayerId && beforeIndex < 0\n ? { ...layer, beforeId: beforeLayerId }\n : { ...layer, beforeId: layer.beforeId };\n if (beforeIndex >= 0) {\n layers.splice(beforeIndex, 0, layerWithBeforeId);\n } else {\n layers.push(layerWithBeforeId);\n }\n return {\n layers,\n selectedLayerId: layer.id,\n isDirty: true,\n };\n }),\n\n removeLayer: (id) =>\n set((s) => ({\n // Re-derive any layer whose joins consumed the removed layer: with\n // the source gone the join resolves to nothing, so its previously\n // materialized columns strip away instead of staying frozen (the\n // join definition itself stays, shown as missing in the Joins UI).\n layers: cascadeJoinRefreshForRemoved(\n s.layers.filter((l) => l.id !== id),\n id,\n ),\n secondaryMapViews: scrubSecondaryPaneLayerVisibility(s.secondaryMapViews, id),\n // Drop storymap chapter enter/exit opacity rows that pointed at the\n // removed layer so they do not keep a dangling id across save/reload.\n storymap: scrubStorymapLayerRefs(s.storymap, id),\n widgets: scrubWidgetsForRemovedLayers(s.widgets, id),\n comments: scrubCommentsForRemovedLayers(s.comments, id),\n legend: scrubLegendForRemovedLayers(s.legend, id),\n // Clear a Print Layout data/atlas block built on the removed layer,\n // so a save that follows the delete cannot write a dangling id.\n printLayout: scrubPrintLayoutForRemovedLayers(s.printLayout, id),\n selectedLayerId:\n s.selectedLayerId === id\n ? (s.layers.find((l) => l.id !== id)?.id ?? null)\n : s.selectedLayerId,\n selectedFeatureId: s.selectedLayerId === id ? null : s.selectedFeatureId,\n selectedFeatureIds: s.selectedLayerId === id ? [] : s.selectedFeatureIds,\n ...identifyStateWithoutLayers(s, new Set([id])),\n ui: {\n ...s.ui,\n selectByExpressionLayerId:\n s.ui.selectByExpressionLayerId === id ? null : s.ui.selectByExpressionLayerId,\n selectByLocationLayerId:\n s.ui.selectByLocationLayerId === id ? null : s.ui.selectByLocationLayerId,\n loadEditorFeaturesLayerId:\n s.ui.loadEditorFeaturesLayerId === id ? null : s.ui.loadEditorFeaturesLayerId,\n },\n isDirty: true,\n })),\n\n updateLayer: (id, patch) =>\n set((s) => {\n let layers = s.layers.map((l) => (l.id === id ? { ...l, ...patch } : l));\n // A geojson replacement re-derives the layer's derived columns\n // (persistent joins, then virtual fields): on the layer itself\n // (file reload, attribute edits, processing writes — derived\n // columns stay derived, QGIS-style), then transitively on every\n // layer whose joins consume the updated one, so editing a join\n // table refreshes its targets and their dependents in turn. The\n // `patch.joins`/`patch.virtualFields` guard exists for external\n // callers of this public store API (plugins, programmatic loads):\n // a patch that carries `geojson` alongside the definitions is taken\n // verbatim as already-derived state — no in-repo caller does this\n // today.\n if (patch.geojson !== undefined) {\n if (patch.joins === undefined && patch.virtualFields === undefined) {\n layers = layers.map((l) =>\n l.id === id && (l.joins?.length || l.virtualFields?.length)\n ? applyJoinsToLayer(l, layers)\n : l,\n );\n }\n layers = cascadeLayerJoinRefresh(layers, id);\n }\n return { layers, isDirty: true };\n }),\n\n setLayerJoins: (id, joins) =>\n set((s) => {\n let layers = s.layers.map((l) => (l.id === id ? applyJoinsToLayer(l, s.layers, joins) : l));\n // Changing this layer's joins changes its materialized columns, so\n // layers joining against it (directly or transitively) re-derive too.\n layers = cascadeLayerJoinRefresh(layers, id);\n return { layers, isDirty: true };\n }),\n\n setLayerAttributeForm: (id, attributeForm) => get().updateLayer(id, { attributeForm }),\n setLayerPopup: (id, popup) => get().updateLayer(id, { popup }),\n setLayerDescriptiveMetadata: (id, metadata) =>\n get().updateLayer(id, { descriptiveMetadata: normalizeLayerDescriptiveMetadata(metadata) }),\n\n setLayerEditorTracking: (id, editorTracking) => get().updateLayer(id, { editorTracking }),\n\n setLayerVirtualFields: (id, fields) =>\n set((s) => {\n let layers = s.layers.map((l) =>\n l.id === id ? applyJoinsToLayer(l, s.layers, undefined, fields) : l,\n );\n // Virtual columns are ordinary materialized properties, so a layer\n // joining against this one (directly or transitively) sees them and\n // must re-derive too.\n layers = cascadeLayerJoinRefresh(layers, id);\n return { layers, isDirty: true };\n }),\n\n setLayerQuickFilters: (id, filters) =>\n get().updateLayer(id, { quickFilters: filters.length > 0 ? filters : undefined }),\n\n setLayerFilterExpression: (id, expression) =>\n get().updateLayer(id, { filterExpression: expression ?? undefined }),\n\n setLayerVisibility: (id, visible) => get().updateLayer(id, { visible }),\n\n setLayerOpacity: (id, opacity) => get().updateLayer(id, { opacity }),\n\n setLayerStyle: (id, style) =>\n set((s) => ({\n layers: s.layers.map((l) => (l.id === id ? { ...l, style: { ...l.style, ...style } } : l)),\n isDirty: true,\n })),\n\n copyLayerStyle: (id) => {\n const layer = get().layers.find((l) => l.id === id);\n if (!layer) return false;\n const copied = extractCopiedLayerStyle(layer);\n // Leave any prior clipboard entry in place when this layer is not\n // copyable, so opening a non-stylable layer's menu never clears it.\n if (!copied) return false;\n set({ copiedLayerStyle: copied });\n return true;\n },\n\n pasteLayerStyle: (id) => {\n const s = get();\n const copied = s.copiedLayerStyle;\n if (!copied) return false;\n const layer = s.layers.find((l) => l.id === id);\n if (!layer) return false;\n const patch = applyCopiedLayerStyle(layer, copied);\n if (!patch) return false;\n // Go through updateLayer so the paste picks up any cross-cutting layer\n // update logic (it also sets isDirty); the patch never carries geojson,\n // so the join-cascade branch is a no-op.\n get().updateLayer(id, patch);\n return true;\n },\n\n applyLayerStyleEntries: (entries, layerIds) => {\n if (entries.length === 0) return [];\n const scope = layerIds ? new Set(layerIds) : null;\n const applied: { layerId: string; entryIndex: number }[] = [];\n const layers = get().layers.map((layer) => {\n if (scope && !scope.has(layer.id)) return layer;\n const match = matchLayerStyleEntry(layer, entries);\n if (!match) return layer;\n applied.push({ layerId: layer.id, entryIndex: match.entryIndex });\n return { ...layer, ...match.patch };\n });\n // A style patch never carries geojson, so unlike updateLayer there is no\n // join cascade to run.\n if (applied.length > 0) set({ layers, isDirty: true });\n return applied;\n },\n\n reorderLayer: (id, direction) =>\n set((s) => {\n const idx = s.layers.findIndex((l) => l.id === id);\n if (idx < 0) return s;\n const target = direction === \"up\" ? idx + 1 : idx - 1;\n if (target < 0 || target >= s.layers.length) return s;\n const next = [...s.layers];\n const [item] = next.splice(idx, 1);\n next.splice(target, 0, item);\n return { layers: next, isDirty: true };\n }),\n\n moveLayer: (id, targetIndex) =>\n set((s) => {\n const currentIndex = s.layers.findIndex((layer) => layer.id === id);\n if (currentIndex < 0) return s;\n const next = [...s.layers];\n const [layer] = next.splice(currentIndex, 1);\n const nextIndex = Math.min(Math.max(targetIndex, 0), next.length);\n next.splice(nextIndex, 0, layer);\n if (next.every((item, index) => item.id === s.layers[index]?.id)) {\n return s;\n }\n return { layers: next, isDirty: true };\n }),\n\n moveLayersRelative: (layerIds, targetLayerId, position) =>\n set((s) => {\n const requestedIds = new Set(layerIds);\n if (requestedIds.has(targetLayerId)) return s;\n const target = s.layers.find((layer) => layer.id === targetLayerId);\n if (!target) return s;\n const targetGroupId = target.groupId ?? null;\n // This is a pure reorder — `groupId` is never touched — so a requested\n // layer from another group can only be lifted out of its own block,\n // and `normalizeGroupContiguity` would then drag that group's\n // untouched members along to reunite it. Move only the layers that\n // already sit in the target's group.\n const moving = s.layers.filter(\n (layer) => requestedIds.has(layer.id) && (layer.groupId ?? null) === targetGroupId,\n );\n if (moving.length === 0) return s;\n const movingIds = new Set(moving.map((layer) => layer.id));\n const without = s.layers.filter((layer) => !movingIds.has(layer.id));\n const targetIndex = without.findIndex((layer) => layer.id === targetLayerId);\n if (targetIndex < 0) return s;\n // Store order is the reverse of panel display order, so \"above\" is\n // immediately after the target in this array.\n const insertIndex = position === \"above\" ? targetIndex + 1 : targetIndex;\n const next = [...without];\n next.splice(insertIndex, 0, ...moving);\n const normalized = normalizeGroupContiguity(next);\n if (normalized.every((layer, index) => layer.id === s.layers[index]?.id)) return s;\n return { layers: normalized, isDirty: true };\n }),\n\n addGeoJsonLayer: (name, geojson, sourcePath, beforeLayerId = null) => {\n const id = uuidv4();\n const layer: GeoLibreLayer = {\n id,\n name,\n type: \"geojson\",\n source: { type: \"geojson\" },\n visible: true,\n opacity: 1,\n // Its own palette color and geometry-appropriate sizing (#1519), so a\n // stack of freshly added layers is legible without restyling each one.\n style: initialLayerStyle({\n geojson,\n layers: get().layers,\n overrides: {\n simpleStyleEnabled: hasSimpleStyleProperties(geojson),\n },\n }),\n metadata: {},\n geojson,\n sourcePath,\n };\n get().addLayer(layer, beforeLayerId);\n return id;\n },\n\n addImageOverlayLayer: (name, source, options, beforeLayerId = null) => {\n const id = uuidv4();\n const layer: GeoLibreLayer = {\n id,\n name,\n type: \"image\",\n source: {\n type: \"image\",\n url: source.url,\n coordinates: source.coordinates,\n },\n visible: options?.visible ?? true,\n opacity: options?.opacity ?? 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: options?.sourceKind ?? \"kml-ground-overlay\",\n ...(options?.bounds ? { bounds: options.bounds } : {}),\n ...(options?.timeSpan ? { timeSpan: options.timeSpan } : {}),\n ...(options?.metadata ?? {}),\n },\n ...(options?.sourcePath ? { sourcePath: options.sourcePath } : {}),\n };\n get().addLayer(layer, beforeLayerId);\n return id;\n },\n\n addTileLayer: (name, options, beforeLayerId = null) => {\n // Every layer this builds carries a raster source, so a non-raster\n // `type` from an untyped JS caller (e.g. \"vector-tiles\") would persist\n // a layer whose `type` and `source.type` disagree. Reject it instead of\n // silently mislabelling the source; vector tiles need their own style\n // layers and go through the vector-tile path, not this one.\n const type = options.type ?? \"xyz\";\n if (!RASTER_TILE_LAYER_TYPES.has(type)) {\n throw new Error(\n `addTileLayer: unsupported type \"${String(type)}\"; expected one of ` +\n `${[...RASTER_TILE_LAYER_TYPES].join(\", \")}. Only raster tile layers are supported.`,\n );\n }\n const id = uuidv4();\n // Trim each template and drop blanks, then reject a registration that\n // sanitizes down to nothing: syncRasterTileLayer returns early on an\n // empty tile list, so without this the store would persist and select a\n // layer that can never render.\n const tiles = options.tiles\n .filter((tile): tile is string => typeof tile === \"string\")\n .map((tile) => tile.trim())\n .filter((tile) => tile !== \"\");\n if (tiles.length === 0) {\n throw new Error(\"addTileLayer requires at least one non-empty tile URL template.\");\n }\n // MapLibre's addSource throws synchronously when maxzoom < minzoom, and\n // syncRasterTileLayer does not catch it, which would strand a layer in\n // the store with no rendered source. Reject the bad range up front.\n if (\n options.minzoom !== undefined &&\n options.maxzoom !== undefined &&\n options.minzoom > options.maxzoom\n ) {\n throw new Error(\n `addTileLayer: minzoom (${options.minzoom}) must be <= maxzoom (${options.maxzoom}).`,\n );\n }\n const layer: GeoLibreLayer = {\n id,\n name,\n type,\n source: {\n // Extra source fields (e.g. WMS layers/styles) merge first so the\n // required raster descriptor below always wins. `type` above is\n // validated as a raster kind, so \"raster\" here always agrees with it.\n ...(options.source ?? {}),\n type: \"raster\",\n tiles,\n tileSize: options.tileSize ?? 256,\n ...(options.url !== undefined ? { url: options.url } : {}),\n ...(options.attribution !== undefined ? { attribution: options.attribution } : {}),\n ...(options.bounds !== undefined ? { bounds: options.bounds } : {}),\n ...(options.minzoom !== undefined ? { minzoom: options.minzoom } : {}),\n ...(options.maxzoom !== undefined ? { maxzoom: options.maxzoom } : {}),\n ...(options.scheme !== undefined ? { scheme: options.scheme } : {}),\n },\n visible: options.visible ?? true,\n opacity: options.opacity ?? 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: { ...(options.metadata ?? {}) },\n };\n get().addLayer(layer, beforeLayerId);\n return id;\n },\n});\n","import { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from \"./types\";\n\n/**\n * CZML (Cesium Language) domain models and layer authoring (issue #2290).\n *\n * CZML is a JSON-based format for describing dynamic, time-varying 3D geospatial\n * scenes on the Cesium globe (trajectories, satellites, vehicles, sensor\n * networks, moving entities with orientations and paths). The globe loads CZML\n * natively through `CzmlDataSource`; the 2D map has no equivalent, so CZML\n * layers are badged \"3D only\" there.\n */\n\n/** `metadata.sourceKind` of a layer that carries or references CZML data. */\nexport const CZML_SOURCE_KIND = \"czml\";\n\n/** One CZML packet in a document stream. */\nexport type CzmlPacket = Record<string, unknown>;\n\n/** A CZML document represented as an array of packets. */\nexport type CzmlDocument = CzmlPacket[];\n\n/** Minimal Point sample in CZML. */\nexport const CZML_SAMPLE_POINT: CzmlPacket[] = [\n {\n id: \"document\",\n name: \"CZML Point Sample\",\n version: \"1.0\",\n },\n {\n id: \"point 1\",\n name: \"Extruded Point\",\n position: {\n cartographicDegrees: [-75.59777, 40.03883, 1000],\n },\n point: {\n color: {\n rgba: [255, 128, 0, 255],\n },\n pixelSize: 14,\n outlineColor: {\n rgba: [255, 255, 255, 255],\n },\n outlineWidth: 2,\n },\n },\n];\n\n/** Time-dynamic trajectory sample with an orbit track and clock. */\nexport const CZML_SAMPLE_DYNAMIC: CzmlPacket[] = [\n {\n id: \"document\",\n name: \"CZML Trajectory Sample\",\n version: \"1.0\",\n clock: {\n interval: \"2026-09-09T00:00:00Z/2026-09-09T02:00:00Z\",\n currentTime: \"2026-09-09T00:00:00Z\",\n multiplier: 60,\n range: \"LOOP_STOP\",\n },\n },\n {\n id: \"satellite\",\n name: \"Satellite Track\",\n availability: \"2026-09-09T00:00:00Z/2026-09-09T02:00:00Z\",\n path: {\n material: {\n solidColor: {\n color: {\n rgba: [0, 200, 255, 255],\n },\n },\n },\n width: 2,\n leadTime: 1800,\n trailTime: 1800,\n },\n position: {\n epoch: \"2026-09-09T00:00:00Z\",\n cartographicDegrees: [\n 0, -75, 40, 250000, 1800, -30, 20, 250000, 3600, 20, 0, 250000, 5400, 70, -20, 250000, 7200,\n 120, -40, 250000,\n ],\n },\n point: {\n color: {\n rgba: [255, 255, 255, 255],\n },\n pixelSize: 10,\n },\n },\n];\n\n/** One-click CZML samples offered in the Add Data dialog. */\nexport const CZML_QUICK_PICKS: ReadonlyArray<{\n name: string;\n data: CzmlPacket[];\n}> = [\n { name: \"CZML Point\", data: CZML_SAMPLE_POINT },\n { name: \"CZML Dynamic Trajectory\", data: CZML_SAMPLE_DYNAMIC },\n];\n\n/**\n * Parse and validate a CZML payload from a JSON string or raw object.\n * Returns an array of CZML packets or null when the input is malformed.\n */\nexport function parseCzml(value: unknown): CzmlPacket[] | null {\n if (!value) return null;\n let parsed = value;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed.startsWith(\"[\") && !trimmed.startsWith(\"{\")) return null;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return null;\n }\n }\n if (Array.isArray(parsed)) {\n return parsed.length > 0 && parsed.every((item) => item && typeof item === \"object\")\n ? (parsed as CzmlPacket[])\n : null;\n }\n if (parsed && typeof parsed === \"object\") {\n return [parsed as CzmlPacket];\n }\n return null;\n}\n\n/**\n * Whether `layer` represents a CZML dynamic 3D scene.\n */\nexport function isCzmlLayer(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): boolean {\n return layer.metadata?.sourceKind === CZML_SOURCE_KIND;\n}\n\n/** Extracted CZML source definition for a layer. */\nexport interface CzmlSource {\n url?: string;\n data?: CzmlPacket[] | string;\n}\n\n/**\n * Extract the CZML content and/or URL from a layer, or null if not a CZML layer.\n */\nexport function czmlSource(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): CzmlSource | null {\n if (!isCzmlLayer(layer)) return null;\n const raw = layer.source?.czmlData as CzmlPacket[] | CzmlPacket | string | undefined;\n // An empty packet array is a document with nothing in it, not a document; a\n // bare packet (the Python API accepts one) is wrapped the way `parseCzml`\n // does, since Cesium indexes a document as an array.\n const data = Array.isArray(raw)\n ? raw.length > 0\n ? raw\n : undefined\n : raw && typeof raw === \"object\"\n ? [raw]\n : raw || undefined;\n const rawUrl = layer.source?.url;\n const url = typeof rawUrl === \"string\" && rawUrl.trim() ? rawUrl.trim() : undefined;\n if (!data && !url) return null;\n return { url, data };\n}\n\n/** Generate a unique identifier for a newly created layer. */\nfunction newLayerId(): string {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Options for constructing a CZML layer.\n */\nexport interface CzmlLayerOptions {\n /** Optional layer id; defaults to a new UUID. */\n id?: string;\n /** Display name of the layer in the Layers panel. */\n name: string;\n /** Inlined CZML packet array or serialized JSON string. */\n data?: CzmlPacket[] | string;\n /** Remote URL pointing to a .czml document. */\n url?: string;\n /** Local path when loaded from disk. */\n sourcePath?: string;\n /** Credit shown in the globe's attribution control while the layer is present. */\n attribution?: string;\n}\n\n/**\n * Build a typed GeoLibreLayer representing a CZML dynamic scene on the Cesium globe.\n */\nexport function createCzmlLayer(options: CzmlLayerOptions): GeoLibreLayer {\n const id = options.id ?? newLayerId();\n const data = options.data;\n const url = options.url?.trim() || undefined;\n const sourcePath = options.sourcePath?.trim() || undefined;\n\n return {\n id,\n name: options.name,\n type: \"3d-tiles\",\n ...(sourcePath ? { sourcePath } : {}),\n source: {\n type: \"3d-tiles\",\n sourceId: id,\n ...(data !== undefined ? { czmlData: data } : {}),\n ...(url ? { url } : {}),\n ...(sourcePath ? { sourcePath } : {}),\n ...(options.attribution?.trim() ? { attribution: options.attribution.trim() } : {}),\n },\n visible: true,\n opacity: 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: CZML_SOURCE_KIND,\n externalNativeLayer: true,\n // Cesium builds real entities from the document and\n // `CesiumLayerSync.resolveFeature` answers for them, so a click can read a\n // packet's name and custom `properties` (issue #2504). Positions are\n // time-dynamic properties rather than stored geometry, so the answer\n // carries no geometry.\n identifiable: true,\n sourceId: id,\n nativeLayerIds: [id],\n },\n };\n}\n","import { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from \"./types\";\n\n/** Native KML/KMZ documents retain their own styling on the globe. */\nexport const CESIUM_KML_SOURCE_KIND = \"cesium-kml\";\n\nexport function isCesiumKmlLayer(layer: Pick<GeoLibreLayer, \"metadata\">): boolean {\n return layer.metadata?.sourceKind === CESIUM_KML_SOURCE_KIND;\n}\n\nexport interface CesiumKmlLayerOptions {\n id?: string;\n name: string;\n /** HTTP(S) KML or KMZ URL. Relative resources resolve against this URL. */\n url?: string;\n /** Inline XML, or a KMZ data URL so local archives survive project saves. */\n data?: string;\n sourcePath?: string;\n}\n\nexport function cesiumKmlSource(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): string | null {\n if (!isCesiumKmlLayer(layer)) return null;\n for (const value of [layer.source.kmlData, layer.source.url]) {\n if (typeof value === \"string\" && value.trim()) return value.trim();\n }\n return null;\n}\n\nexport function createCesiumKmlLayer(options: CesiumKmlLayerOptions): GeoLibreLayer {\n const data = options.data?.trim();\n const url = options.url?.trim();\n if (!data && !url) throw new Error(\"Provide a KML/KMZ document or URL.\");\n const id = options.id ?? crypto.randomUUID();\n return {\n id,\n name: options.name,\n type: \"3d-tiles\",\n ...(options.sourcePath ? { sourcePath: options.sourcePath } : {}),\n source: { type: \"3d-tiles\", sourceId: id, ...(data ? { kmlData: data } : { url }) },\n visible: true,\n opacity: 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: CESIUM_KML_SOURCE_KIND,\n externalNativeLayer: true,\n identifiable: false,\n },\n };\n}\n","import { isCzmlLayer } from \"./czml\";\nimport { isCesiumKmlLayer } from \"./cesium-kml\";\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from \"./types\";\n\n// Cesium Ion assets (issue #2290): 3D Tiles and imagery referenced by Ion\n// asset id rather than by URL. The globe loads them natively through the Ion\n// token the app already holds for World Terrain; the 2D map has no equivalent,\n// so these layers are flagged \"3D only\" there and never painted.\n\n/** `metadata.sourceKind` of a layer that references a Cesium Ion asset. */\nexport const CESIUM_ION_SOURCE_KIND = \"cesium-ion\";\n\n/** What an Ion asset id resolves to on the globe. */\nexport type CesiumIonAssetKind = \"3d-tiles\" | \"imagery\";\n\n/** Cesium OSM Buildings, the one-click 3D Tiles asset every Ion account can use. */\nexport const CESIUM_OSM_BUILDINGS_ASSET_ID = 96188;\n\n/** Bing Maps Aerial through Ion, the imagery asset every Ion account can use. */\nexport const CESIUM_BING_AERIAL_ASSET_ID = 2;\n\n/**\n * Google Photorealistic 3D Tiles served through Ion. Unlike the two above it\n * is not free on every plan: Ion accounts without it get a 403 the globe\n * surfaces as a layer error.\n */\nexport const CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID = 2275207;\n\n/**\n * Which tier of the Ion catalog a quick pick comes from. `global` assets ship\n * with every Ion account; `depot` assets are Asset Depot samples the account\n * has to add once at ion.cesium.com before they stream (until then the\n * endpoint 404s and the globe reports a layer error).\n */\nexport type CesiumIonQuickPickGroup = \"global\" | \"depot\";\n\nexport interface CesiumIonQuickPick {\n assetId: number;\n kind: CesiumIonAssetKind;\n name: string;\n group: CesiumIonQuickPickGroup;\n}\n\n/**\n * Ion assets offered as one-click picks in the Add Data dialog. Asset ids are\n * the dropdown's option values, so they have to stay unique; the ids below are\n * the ones Cesium's own samples use.\n */\nexport const CESIUM_ION_QUICK_PICKS: ReadonlyArray<CesiumIonQuickPick> = [\n {\n assetId: CESIUM_OSM_BUILDINGS_ASSET_ID,\n kind: \"3d-tiles\",\n name: \"Cesium OSM Buildings\",\n group: \"global\",\n },\n {\n assetId: CESIUM_BING_AERIAL_ASSET_ID,\n kind: \"imagery\",\n name: \"Bing Maps Aerial (Ion)\",\n group: \"global\",\n },\n {\n assetId: CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID,\n kind: \"3d-tiles\",\n name: \"Google Photorealistic 3D Tiles\",\n group: \"global\",\n },\n { assetId: 2602291, kind: \"3d-tiles\", name: \"Japan 3D Building Data\", group: \"depot\" },\n { assetId: 69380, kind: \"3d-tiles\", name: \"Melbourne Photogrammetry\", group: \"depot\" },\n { assetId: 43978, kind: \"3d-tiles\", name: \"Melbourne Point Cloud\", group: \"depot\" },\n { assetId: 28945, kind: \"3d-tiles\", name: \"Montreal Point Cloud\", group: \"depot\" },\n { assetId: 75343, kind: \"3d-tiles\", name: \"New York City 3D Buildings\", group: \"depot\" },\n { assetId: 3827, kind: \"imagery\", name: \"Washington DC 2017\", group: \"depot\" },\n];\n\n/** Parse an Ion asset id the way the Add Data form and the project file carry it. */\nexport function parseCesiumIonAssetId(value: unknown): number | null {\n const id = typeof value === \"string\" ? Number(value.trim()) : value;\n return typeof id === \"number\" && Number.isInteger(id) && id > 0 ? id : null;\n}\n\n/**\n * The Ion asset id a layer references, or null when the layer is not an Ion\n * asset layer (or carries no usable id).\n */\nexport function cesiumIonAssetId(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): number | null {\n if (layer.metadata?.sourceKind !== CESIUM_ION_SOURCE_KIND) return null;\n return parseCesiumIonAssetId(layer.source?.ionAssetId);\n}\n\n/** Whether `layer` references a Cesium Ion asset. */\nexport function isCesiumIonLayer(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): boolean {\n return cesiumIonAssetId(layer) !== null;\n}\n\n/** What the Ion asset draws as: a tileset primitive or an imagery layer. */\nexport function cesiumIonAssetKind(layer: Pick<GeoLibreLayer, \"type\">): CesiumIonAssetKind {\n return layer.type === \"3d-tiles\" ? \"3d-tiles\" : \"imagery\";\n}\n\n/**\n * Whether only the 3D globe can render `layer`: the mirror of the globe's\n * `isCesiumSupportedLayerType`, for the Layers panel to badge on the 2D map.\n */\nexport function isCesiumOnlyLayer(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): boolean {\n return isCesiumIonLayer(layer) || isCzmlLayer(layer) || isCesiumKmlLayer(layer);\n}\n\nfunction newLayerId(): string {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\nexport interface CesiumIonLayerOptions {\n id?: string;\n name: string;\n assetId: number;\n kind: CesiumIonAssetKind;\n /** Metres to shift a tileset vertically; ignored for imagery. */\n altitudeOffset?: number;\n}\n\n/**\n * Build the store layer for a Cesium Ion asset. A tileset is a `3d-tiles`\n * layer whose `sourceKind` keeps it out of the 2D deck.gl control; imagery is\n * a `raster` layer with no tiles, marked external so the 2D sync leaves it\n * alone. Both persist only the asset id: the token is runtime configuration.\n */\nexport function createCesiumIonLayer(options: CesiumIonLayerOptions): GeoLibreLayer {\n const id = options.id ?? newLayerId();\n const isTileset = options.kind === \"3d-tiles\";\n const altitudeOffset = Number.isFinite(options.altitudeOffset) ? options.altitudeOffset! : 0;\n return {\n id,\n name: options.name,\n type: isTileset ? \"3d-tiles\" : \"raster\",\n source: {\n type: isTileset ? \"3d-tiles\" : \"raster\",\n ionAssetId: options.assetId,\n sourceId: id,\n ...(isTileset ? { altitudeOffset } : {}),\n },\n visible: true,\n opacity: 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: CESIUM_ION_SOURCE_KIND,\n externalNativeLayer: true,\n identifiable: false,\n sourceId: id,\n nativeLayerIds: [id],\n ...(isTileset ? { customLayerType: \"3d-tiles\", altitudeOffset } : {}),\n },\n };\n}\n","/**\n * `${NAME}` references in request header values.\n *\n * A header such as `Authorization: Bearer ${TILES_TOKEN}` is saved as typed\n * and resolved from the project's Environment Variables when a request is\n * made, so the secret itself never sits on the layer. This module is pure (no\n * store import): `credentials.ts` depends on it, and the store depends on\n * `credentials.ts`.\n */\n\n/** `${NAME}` where NAME follows the environment-variable name rule. */\nconst HEADER_REFERENCE = /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\nconst HAS_HEADER_REFERENCE = /\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}/;\n/**\n * Optional auth scheme word, whitespace, one reference, nothing else. Kept in\n * lockstep with `_HEADER_REFERENCE_ONLY` in `python/src/geolibre/project.py`.\n */\nconst REFERENCE_ONLY = /^(?:[A-Za-z][A-Za-z0-9._-]*\\s+)?\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}$/;\n\nconst warnedMissing = new Set<string>();\n\nexport function hasHeaderReferences(headers: Record<string, string> | undefined): boolean {\n if (!headers) return false;\n return Object.values(headers).some(\n (value) => typeof value === \"string\" && HAS_HEADER_REFERENCE.test(value),\n );\n}\n\n/**\n * Whether `value` carries no secret of its own: at most a scheme word such as\n * `Bearer` followed by a single `${NAME}`. Redaction keeps such values.\n */\nexport function isHeaderReferenceOnly(value: string): boolean {\n return REFERENCE_ONLY.test(value.trim());\n}\n\n/**\n * Whether credential-bearing request headers may be sent to `url`: HTTPS, or\n * plain HTTP to loopback so a local dev server still works. The scheme is read\n * off a parsed URL, so an unusually cased `HTTPS://` is not misread as\n * plaintext; a relative or unparseable URL is refused.\n */\nexport function allowsCredentialHeaders(url: string): boolean {\n try {\n const { protocol, hostname } = new URL(url);\n if (protocol === \"https:\") return true;\n return (\n protocol === \"http:\" &&\n (hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"[::1]\")\n );\n } catch {\n return false;\n }\n}\n\n/**\n * `headers` with every `${NAME}` replaced by `values[NAME]`. A header that\n * references an unset (or empty) variable is omitted rather than sent with a\n * hole in it. Returns the input unchanged when nothing references a variable,\n * and `undefined` when no header remains.\n */\nexport function resolveHeaderReferences(\n headers: Record<string, string> | undefined,\n values: Readonly<Record<string, string>>,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n if (!Object.values(headers).some((value) => typeof value === \"string\" && value.includes(\"${\"))) {\n return headers;\n }\n const resolved: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n if (typeof value !== \"string\") continue;\n let missing: string | null = null;\n const next = value.replace(HEADER_REFERENCE, (_match, variable: string) => {\n const replacement = Object.hasOwn(values, variable) ? values[variable] : \"\";\n if (!replacement) missing ??= variable;\n return replacement;\n });\n if (missing !== null) {\n const warnKey = `${name}:${missing}`;\n if (!warnedMissing.has(warnKey)) {\n warnedMissing.add(warnKey);\n console.warn(\n `[GeoLibre] Request header \"${name}\" references unset environment variable ${missing}; the header is not sent.`,\n );\n }\n continue;\n }\n resolved[name] = next;\n }\n return Object.keys(resolved).length > 0 ? resolved : undefined;\n}\n","import { isHeaderReferenceOnly } from \"./header-references\";\nimport type { GeoLibreProject, LayerConnection } from \"./types\";\n\n/**\n * Project fields whose values are credentials. Keep this registry as the\n * schema-level inventory. Keep the explicit preference/plugin handling in\n * redactProjectCredentials in sync when adding entries outside layer config.\n */\nexport const PROJECT_CREDENTIAL_FIELDS = {\n preferences: [\"environmentVariables\", \"geocoding.apiKeys\"],\n layerConfiguration: [\n \"requestHeaders\",\n \"headers\",\n \"authorization\",\n \"apiKey\",\n \"apiKeys\",\n \"accessToken\",\n \"token\",\n \"password\",\n \"clientSecret\",\n \"connectionString\",\n \"secret\",\n \"bearer\",\n \"auth\",\n \"authKey\",\n \"sasToken\",\n \"subscriptionKey\",\n \"signature\",\n \"pwd\",\n ],\n pluginState: [\"plugins.settings\"],\n} as const;\n\n/**\n * Plugin settings that survive redaction: plugin id to the sub-keys kept from\n * its blob, or `null` to keep the whole blob.\n *\n * A plugin's settings are free-form and an external plugin can persist a\n * credential there, so the default is to drop all of it. What is listed here is\n * map-control *composition* — the swipe split, legend entries and colors, the\n * colorbar's range and ramp — which is what a shared or exported project needs\n * in order to render the map it was built as, and is structured rather than\n * free text. Dropping it silently stripped every legend, colorbar, and swipe\n * from an export.\n *\n * The components plugin's `html` sub-key is deliberately absent: it holds a\n * custom HTML panel the user authored by hand (`ComponentHtmlGuiEntryState`),\n * so it can carry anything, including a URL with a token in it. It is dropped\n * like any unknown blob. Mirrored by `PUBLISHABLE_PLUGIN_SETTINGS` in\n * `python/src/geolibre/project.py`; the two must agree.\n */\nexport const PUBLISHABLE_PLUGIN_SETTINGS: Readonly<Record<string, readonly string[] | null>> = {\n \"maplibre-gl-swipe\": null,\n \"maplibre-gl-components\": [\"legend\", \"colorbar\"],\n // This is the source of truth for dock-owned temporal layers. Dropping it\n // leaves only their external-native store mirrors in a shared project; those\n // mirrors contain a MapLibre source id, not enough information to recreate\n // the COG/mosaic/tiles for a recipient. The retained blob is still passed\n // through redactConfigurationValue below, including every source URL.\n \"maplibre-gl-time-slider\": null,\n // Feed toggles (one boolean per feed) plus a numeric clock speed — no URLs,\n // no keys, nothing user-authored. Listed as a whole blob rather than by key\n // because the feed set grows with every new feed; an enumerated list would\n // silently start counting each new toggle as a credential, which is the bug\n // this entry fixes. Still swept by redactConfigurationValue below.\n \"gods-eye-view\": null,\n // Point class edits keyed by (node key, index): compressed numbers, no\n // user text. Source URLs are values, so the sweep below still scrubs a\n // credentialed one.\n \"geolibre-point-cloud-annotation\": null,\n};\n\nexport interface CredentialRedactionResult {\n project: GeoLibreProject;\n /** Stable project paths removed or rewritten by the redaction pass. */\n redactedPaths: string[];\n /**\n * Opaque `path=hash` fingerprints identifying *which* secret sat at each\n * redacted path. A caller that remembers an explicit \"keep credentials\"\n * decision compares these between saves, so a different credential, such as\n * a swapped layer or rotated token, is confirmed again instead of being\n * written to disk under the earlier answer. Paths alone cannot carry that:\n * they are positional, so a replacement layer inherits the path of the one it\n * replaced.\n */\n redactedFingerprints: string[];\n /**\n * Whether any redacted value could not be serialized, and so has no entry in\n * {@link redactedFingerprints}. Such a value cannot be compared with what the\n * user accepted, so a caller reusing a remembered decision must ask again\n * rather than assume the unfingerprintable credential is unchanged.\n */\n hasUnfingerprintableCredential: boolean;\n /** Number of individual credential-bearing fields removed or rewritten. */\n redactedCount: number;\n}\n\n/** Collector threaded through the redaction pass. */\ninterface RedactionAccumulator {\n paths: string[];\n fingerprints: string[];\n count: number;\n /** Whether some redacted value could not be serialized, and so not fingerprinted. */\n unfingerprintable: boolean;\n}\n\n/**\n * Hash a credential value to a short, stable 64-bit token: two independent\n * FNV-1a-style lanes, the first with the canonical FNV prime and the second\n * with a MurmurHash3 mixing constant so the lanes do not move together.\n *\n * The digest never leaves memory and is only ever compared for equality, so it\n * needs to be stable and cheap rather than cryptographic. Hashing rather than\n * retaining the value keeps the secret itself out of the remembered choices.\n *\n * A collision costs a *missed* confirmation, not a spurious one: a caller\n * comparing fingerprints would treat a different secret as one the user already\n * accepted. That is why this is 64 bits rather than 32, and why a value that\n * cannot be serialized returns null rather than hashing a lossy `String(value)`\n * that would collapse every such value onto `[object Object]`.\n *\n * @returns The digest, or null when the value cannot be serialized.\n */\nfunction fingerprintValue(value: unknown): string | null {\n let serialized: string;\n try {\n serialized = JSON.stringify(value) ?? String(value);\n } catch {\n return null;\n }\n let low = 0x811c9dc5;\n let high = 0x27220a95;\n for (let index = 0; index < serialized.length; index += 1) {\n const code = serialized.charCodeAt(index);\n low = Math.imul(low ^ code, 0x01000193);\n high = Math.imul(high ^ code, 0x85ebca6b);\n }\n return `${(low >>> 0).toString(36)}.${(high >>> 0).toString(36)}`;\n}\n\n/** Record one redaction, with the fingerprint of the value being removed. */\nfunction recordRedaction(\n accumulator: RedactionAccumulator,\n path: string,\n value: unknown,\n count = 1,\n): void {\n const digest = fingerprintValue(value);\n accumulator.paths.push(path);\n // A value that cannot be serialized fails closed: it is reported through\n // `hasUnfingerprintableCredential` rather than fingerprinted, so a caller\n // asks again instead of reusing an answer it cannot verify still applies.\n if (digest === null) accumulator.unfingerprintable = true;\n else accumulator.fingerprints.push(`${path}=${digest}`);\n accumulator.count += count;\n}\n\n/**\n * Fold the spellings of one credential name together, so `apiKey`, `api_key`,\n * `api-key`, and `APIKEY` are a single registry entry on both the object-key\n * and the URL-parameter side.\n */\nfunction normalizeCredentialName(name: string): string {\n return name.toLowerCase().replace(/[-_]/g, \"\");\n}\n\nconst SENSITIVE_KEYS = new Set(\n PROJECT_CREDENTIAL_FIELDS.layerConfiguration.map(normalizeCredentialName),\n);\n/**\n * Credential names in query strings. This is the wider of the two registries by\n * design: everything here is also an object-key credential *except* `key` and\n * the Azure SAS positional parameters (`sv`/`sr`/`st`/`se`/`sp`/`sig`/`skoid`),\n * which are only credentials because of where they appear. As configuration\n * field names they collide with ordinary state — `sr` is a spatial reference on\n * an ArcGIS source, `key` is a generic identifier — so matching them on object\n * keys would silently drop layer configuration rather than a secret.\n */\nconst URL_CREDENTIAL_PARAMS = new Set(\n [\n ...PROJECT_CREDENTIAL_FIELDS.layerConfiguration,\n \"key\",\n \"sig\",\n \"se\",\n \"sp\",\n \"sv\",\n \"sr\",\n \"st\",\n \"skoid\",\n ].map(normalizeCredentialName),\n);\n/**\n * Depth at which the redaction pass stops descending and fails closed.\n * Exported so a caller that predicts what redaction will remove (the Share\n * dialog's readiness check) scans exactly as deep as this pass does, instead of\n * keeping a second, shallower cap that would silently disagree.\n */\nexport const MAX_REDACT_DEPTH = 12;\n\n/** Whether an object key in layer/plugin configuration holds a credential. */\nexport function isCredentialFieldName(name: string): boolean {\n return SENSITIVE_KEYS.has(normalizeCredentialName(name));\n}\n\nfunction isCredentialParam(name: string): boolean {\n let decoded = name.replace(/\\+/g, \" \");\n try {\n decoded = decodeURIComponent(decoded);\n } catch {\n // Malformed names cannot match the registry, but must not break export.\n }\n const lowered = decoded.toLowerCase();\n return (\n URL_CREDENTIAL_PARAMS.has(normalizeCredentialName(lowered)) || lowered.startsWith(\"x-amz-\")\n );\n}\n\n/**\n * Whether a URL query-parameter name carries a credential. This is the wider of\n * the two registries (see `URL_CREDENTIAL_PARAMS`). Exported so a caller that\n * must leave the parameter in place — the native diagnostics log rewrites its\n * value to `[redacted]` rather than dropping the pair — shares this registry\n * instead of keeping a second, narrower list that can drift from it.\n */\nexport function isCredentialUrlParam(name: string): boolean {\n return isCredentialParam(name);\n}\n\nfunction redactParameterString(value: string): string {\n return value\n .split(\"&\")\n .filter((pair) => pair !== \"\" && !isCredentialParam(pair.split(\"=\", 1)[0]))\n .join(\"&\");\n}\n\n/**\n * Remove credentials from URL-shaped values without encoding tile-template\n * placeholders such as `{z}/{x}/{y}`.\n */\nexport function redactUrlCredentials(value: string): string {\n const hashIndex = value.indexOf(\"#\");\n const beforeHash = hashIndex === -1 ? value : value.slice(0, hashIndex);\n const fragment = hashIndex === -1 ? undefined : value.slice(hashIndex + 1);\n const queryIndex = beforeHash.indexOf(\"?\");\n let base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n const query = queryIndex === -1 ? undefined : beforeHash.slice(queryIndex + 1);\n\n const schemeIndex = base.indexOf(\"://\");\n if (schemeIndex !== -1) {\n const authorityStart = schemeIndex + 3;\n const authorityEnd = base.indexOf(\"/\", authorityStart);\n const authority = base.slice(authorityStart, authorityEnd === -1 ? undefined : authorityEnd);\n const at = authority.lastIndexOf(\"@\");\n if (at !== -1) {\n base =\n base.slice(0, authorityStart) +\n authority.slice(at + 1) +\n (authorityEnd === -1 ? \"\" : base.slice(authorityEnd));\n }\n }\n\n const keptQuery = query === undefined ? undefined : redactParameterString(query);\n const keptFragment =\n fragment === undefined || !fragment.includes(\"=\") ? fragment : redactParameterString(fragment);\n return base + (keptQuery ? `?${keptQuery}` : \"\") + (keptFragment ? `#${keptFragment}` : \"\");\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isGeoJsonPayload(value: Record<string, unknown>): boolean {\n return (\n value.type === \"FeatureCollection\" ||\n value.type === \"Feature\" ||\n value.type === \"GeometryCollection\"\n );\n}\n\nfunction redactConfigurationValue(\n value: unknown,\n path: string,\n accumulator: RedactionAccumulator,\n depth = 0,\n): unknown {\n if (depth >= MAX_REDACT_DEPTH) {\n // Fail closed. A deeply nested configuration shape is not needed to render\n // any built-in layer, and returning it unchanged would let a credential\n // bypass the invariant merely by exceeding the traversal cap.\n recordRedaction(accumulator, path, value);\n return undefined;\n }\n if (typeof value === \"string\") {\n const redacted = redactUrlCredentials(value);\n if (redacted !== value) {\n recordRedaction(accumulator, path, value);\n }\n return redacted;\n }\n if (Array.isArray(value)) {\n return value.map((item, index) =>\n redactConfigurationValue(item, `${path}[${index}]`, accumulator, depth + 1),\n );\n }\n if (!isPlainObject(value)) return value;\n if (isGeoJsonPayload(value)) return structuredClone(value);\n\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value)) {\n const nestedPath = path ? `${path}.${key}` : key;\n const normalizedKey = normalizeCredentialName(key);\n if (\n (normalizedKey === \"requestheaders\" || normalizedKey === \"headers\") &&\n isPlainObject(nested)\n ) {\n // `Bearer ${TOKEN}` names a variable instead of carrying a secret, so it\n // survives; any other header value is dropped. Mirrors `_redact_config`\n // in python/src/geolibre/project.py.\n const kept: Record<string, string> = {};\n const removed: Record<string, unknown> = {};\n for (const [name, headerValue] of Object.entries(nested)) {\n if (typeof headerValue === \"string\" && isHeaderReferenceOnly(headerValue)) {\n kept[name] = headerValue;\n } else {\n removed[name] = headerValue;\n }\n }\n if (Object.keys(removed).length > 0) recordRedaction(accumulator, nestedPath, removed);\n if (Object.keys(kept).length > 0) result[key] = kept;\n continue;\n }\n if (isCredentialFieldName(key)) {\n recordRedaction(accumulator, nestedPath, nested);\n continue;\n }\n result[key] = redactConfigurationValue(nested, nestedPath, accumulator, depth + 1);\n }\n return result;\n}\n\nfunction countLeafValues(value: unknown): number {\n if (Array.isArray(value)) return value.reduce((count, item) => count + countLeafValues(item), 0);\n if (isPlainObject(value)) {\n return Object.values(value).reduce(\n (count: number, nested) => count + countLeafValues(nested),\n 0,\n );\n }\n return 1;\n}\n\n/**\n * Settings keys that registry-listed external plugins declared publishable,\n * keyed by plugin id. `null` keeps the whole blob. Populated at runtime by the\n * host from the curated plugin registry, never from the project file itself,\n * so a project cannot vouch for its own state. The Python mirror in\n * `python/src/geolibre/project.py` only knows the static built-in list, so a\n * project redacted there still drops these plugins' state.\n */\nconst registryPublishableSettings = new Map<string, readonly string[] | null>();\n\n/**\n * Replace the registry-declared publishable plugin settings.\n *\n * Built-in ids in {@link PUBLISHABLE_PLUGIN_SETTINGS} always win, so a registry\n * entry cannot widen a first-party plugin's allowlist.\n *\n * @param entries Plugin ids mapped to the settings keys to keep, or `null` to\n * keep the whole blob. Ids that declare nothing should be omitted.\n */\nexport function setRegistryPublishableSettings(\n entries: Iterable<readonly [string, readonly string[] | null]>,\n): void {\n registryPublishableSettings.clear();\n for (const [id, keys] of entries) {\n if (Object.prototype.hasOwnProperty.call(PUBLISHABLE_PLUGIN_SETTINGS, id)) continue;\n registryPublishableSettings.set(id, keys);\n }\n}\n\n/**\n * Look up the publishable settings for a plugin id.\n *\n * @param id Plugin id.\n * @returns `undefined` when nothing is publishable, `null` to keep the whole\n * blob, otherwise the list of keys to keep.\n */\nfunction publishableSettingsFor(id: string): readonly string[] | null | undefined {\n if (Object.prototype.hasOwnProperty.call(PUBLISHABLE_PLUGIN_SETTINGS, id)) {\n return PUBLISHABLE_PLUGIN_SETTINGS[id];\n }\n return registryPublishableSettings.get(id);\n}\n\n/**\n * Return a detached project safe for any external egress.\n *\n * Environment variables and geocoder keys are always removed. Layer\n * configuration is recursively scrubbed for credential fields and credential\n * URL parameters. Plugin settings are omitted wholesale because external\n * plugins can persist arbitrary shapes; retaining unknown state cannot provide\n * a no-secret guarantee. The first-party map controls in\n * {@link PUBLISHABLE_PLUGIN_SETTINGS} are the exception: their state is known\n * and carries no credentials, and dropping it stripped the legend, colorbar,\n * and swipe from the exported map. Manifest URLs, activation, and control\n * positions stay intact so recipients can still load and configure the plugin\n * themselves. A registry-listed external plugin can opt in through the\n * `publishableSettings` field of its registry entry (see\n * {@link setRegistryPublishableSettings}); what it keeps is still swept below.\n */\nexport function redactProjectCredentials(project: GeoLibreProject): CredentialRedactionResult {\n const accumulator: RedactionAccumulator = {\n paths: [],\n fingerprints: [],\n count: 0,\n unfingerprintable: false,\n };\n const basemapStyleUrl =\n typeof project.basemapStyleUrl === \"string\"\n ? redactUrlCredentials(project.basemapStyleUrl)\n : project.basemapStyleUrl;\n if (basemapStyleUrl !== project.basemapStyleUrl) {\n recordRedaction(accumulator, \"basemapStyleUrl\", project.basemapStyleUrl);\n }\n const geocoding = project.preferences?.geocoding\n ? { ...project.preferences.geocoding, apiKeys: {} }\n : project.preferences?.geocoding;\n if (geocoding) {\n for (const field of [\"forwardEndpoint\", \"reverseEndpoint\"] as const) {\n const endpoint = geocoding[field];\n if (typeof endpoint !== \"string\") continue;\n const redacted = redactUrlCredentials(endpoint);\n if (redacted !== endpoint) {\n geocoding[field] = redacted;\n recordRedaction(accumulator, `preferences.geocoding.${field}`, endpoint);\n }\n }\n }\n // The Mapbox-only style is a URL like the shared basemap and can carry an\n // access token; sweep it the same way so the save prompt counts it and a\n // \"strip\" choice actually removes it.\n const mapboxStyleUrl = project.preferences?.map?.mapboxStyleUrl;\n const redactedMapboxStyleUrl =\n typeof mapboxStyleUrl === \"string\" ? redactUrlCredentials(mapboxStyleUrl) : mapboxStyleUrl;\n if (redactedMapboxStyleUrl !== mapboxStyleUrl) {\n recordRedaction(accumulator, \"preferences.map.mapboxStyleUrl\", mapboxStyleUrl);\n }\n const preferences = project.preferences\n ? {\n ...project.preferences,\n // Only rows explicitly marked non-secret travel; everything else is\n // treated as a credential.\n environmentVariables: (project.preferences.environmentVariables ?? []).filter(\n (variable) => variable.secret === false,\n ),\n geocoding,\n ...(redactedMapboxStyleUrl !== mapboxStyleUrl\n ? { map: { ...project.preferences.map, mapboxStyleUrl: redactedMapboxStyleUrl } }\n : {}),\n }\n : project.preferences;\n // A secret row with an empty value carries nothing (desktop keeps the value\n // in the keychain), so it must not count toward the save prompt. A populated\n // row with no name still needs an explicit keep/strip choice.\n const populatedEnvironmentVariables =\n project.preferences?.environmentVariables?.filter(\n (variable) => variable.secret !== false && variable.value !== \"\",\n ) ?? [];\n if (populatedEnvironmentVariables.length > 0) {\n recordRedaction(\n accumulator,\n \"preferences.environmentVariables\",\n populatedEnvironmentVariables,\n populatedEnvironmentVariables.length,\n );\n }\n const geocodingApiKeys = project.preferences?.geocoding?.apiKeys ?? {};\n if (Object.keys(geocodingApiKeys).length > 0) {\n recordRedaction(\n accumulator,\n \"preferences.geocoding.apiKeys\",\n geocodingApiKeys,\n Object.keys(geocodingApiKeys).length,\n );\n }\n\n const layers = (project.layers ?? []).map((layer, index) => {\n // `export: false` strips the feature data this project *carries* — the\n // inline `geojson` and the `embeddedGeoJSON` snapshot. It deliberately\n // leaves `source`/`connection` alone, so a layer that fetches live (WFS,\n // ArcGIS FeatureServer, a remote GeoJSON or tile URL) still travels with a\n // reachable URL and re-fetches the same data when the shared project is\n // opened. Stripping those would ship a layer that cannot render at all;\n // whether that is the right trade is tracked in the docs' caveat.\n const isNoExport = layer.capabilities?.export === false;\n const cleanMetadata = redactConfigurationValue(\n layer.metadata,\n `layers[${index}].metadata`,\n accumulator,\n ) as Record<string, unknown>;\n if (isNoExport && \"embeddedGeoJSON\" in cleanMetadata) {\n delete cleanMetadata.embeddedGeoJSON;\n }\n return {\n ...layer,\n ...(isNoExport ? { geojson: undefined } : {}),\n source: redactConfigurationValue(\n layer.source,\n `layers[${index}].source`,\n accumulator,\n ) as Record<string, unknown>,\n metadata: cleanMetadata,\n ...(typeof layer.sourcePath === \"string\"\n ? {\n sourcePath: redactConfigurationValue(\n layer.sourcePath,\n `layers[${index}].sourcePath`,\n accumulator,\n ) as string,\n }\n : {}),\n // `connection.lastError` is free-form text taken from a caught error, and a\n // refresh path that words it as `Failed to fetch ${url}` would carry the\n // request's credential parameters. Sweeping it costs nothing and keeps the\n // no-secret guarantee from depending on how an error message is phrased.\n ...(layer.connection\n ? {\n connection: redactConfigurationValue(\n layer.connection,\n `layers[${index}].connection`,\n accumulator,\n ) as LayerConnection,\n }\n : {}),\n };\n });\n\n let plugins = project.plugins;\n if (plugins) {\n const manifestUrls = plugins.manifestUrls.map((url, index) => {\n const redacted = redactUrlCredentials(url);\n if (redacted !== url) {\n recordRedaction(accumulator, `plugins.manifestUrls[${index}]`, url);\n }\n return redacted;\n });\n const settings = plugins.settings ?? {};\n const kept: Record<string, unknown> = {};\n const dropped: Record<string, unknown> = {};\n for (const [id, value] of Object.entries(settings)) {\n const allowed = publishableSettingsFor(id);\n if (allowed === undefined) {\n dropped[id] = value;\n continue;\n }\n if (allowed === null) {\n kept[id] = value;\n continue;\n }\n // An unexpected shape is dropped rather than passed through.\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n dropped[id] = value;\n continue;\n }\n const subset: Record<string, unknown> = {};\n const rest: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n if (allowed.includes(key)) subset[key] = item;\n else rest[key] = item;\n }\n if (Object.keys(subset).length > 0) kept[id] = subset;\n if (Object.keys(rest).length > 0) dropped[id] = rest;\n }\n if (Object.keys(dropped).length > 0) {\n recordRedaction(accumulator, \"plugins.settings\", dropped, countLeafValues(dropped));\n }\n // What survives is still swept by the same pass layer configuration gets,\n // so a credentialed URL inside a kept blob is scrubbed rather than trusted.\n plugins = {\n ...plugins,\n manifestUrls,\n settings: redactConfigurationValue(kept, \"plugins.settings\", accumulator) as Record<\n string,\n unknown\n >,\n };\n }\n\n return {\n project: {\n // Remaining project-level fields are structural/user content with no\n // current credential-bearing schema. Add explicit handling above when a\n // future field (for example a processing parameter) accepts credentials.\n ...project,\n basemapStyleUrl,\n ...(preferences ? { preferences } : {}),\n layers,\n ...(plugins ? { plugins } : {}),\n ...(project.metadata\n ? {\n metadata: redactConfigurationValue(project.metadata, \"metadata\", accumulator) as Record<\n string,\n unknown\n >,\n }\n : {}),\n },\n redactedPaths: [...new Set(accumulator.paths)],\n redactedFingerprints: [...new Set(accumulator.fingerprints)],\n hasUnfingerprintableCredential: accumulator.unfingerprintable,\n redactedCount: accumulator.count,\n };\n}\n\n/**\n * Remove credentials from a single configuration value (a layer `source`,\n * `metadata`, or `sourcePath`) with the same rules the project redaction pass\n * applies to layer configuration: credential-named fields and non-reference\n * header values are dropped, and credential URL parameters are stripped from\n * every URL-shaped string.\n *\n * @param value - The configuration value to scrub. Not mutated.\n * @returns A detached, redacted copy.\n */\nexport function redactConfigurationCredentials<T>(value: T): T {\n const accumulator: RedactionAccumulator = {\n paths: [],\n fingerprints: [],\n count: 0,\n unfingerprintable: false,\n };\n return redactConfigurationValue(value, \"\", accumulator) as T;\n}\n\n/** Convenience wrapper for callers that only need the safe project. */\nexport function redactCredentials(project: GeoLibreProject): GeoLibreProject {\n return redactProjectCredentials(project).project;\n}\n","// The Layer Library's data layer (issue #1520): capturing a configured layer as\n// a reusable entry, planning how to re-add one to a project, the shareable JSON\n// bundle format, and untrusted-input normalization. UI-free so both the desktop\n// app and tests consume it directly. Mirrors the shape of `style-library.ts`,\n// which does the same job for the Style Manager.\n//\n// The guiding rule is that an entry stores the layer's **source specification,\n// not its data**: the library stays small and re-adding an entry always shows\n// the current contents of its source. Layers whose features exist only in\n// memory (drawn features, processing output) or only in a local file have no\n// re-fetchable source, so those embed their features behind a size cap.\n\nimport { cesiumIonAssetId } from \"./cesium-ion\";\nimport type { FeatureCollection } from \"geojson\";\nimport {\n DEFAULT_LAYER_STYLE,\n LAYER_TYPES,\n type AttributeFormConfig,\n type GeoLibreLayer,\n type LayerDescriptiveMetadata,\n type LayerJoin,\n type LayerLibraryEntry,\n type LayerPopupConfig,\n type LayerStyle,\n type LayerType,\n type LayerVirtualField,\n} from \"./types\";\nimport { sanitizeLayerStylePatch } from \"./style-library\";\nimport { normalizeLayerDescriptiveMetadata } from \"./layer-descriptive-metadata\";\nimport { hasPathTraversal, isAbsoluteFilesystemPath } from \"./paths\";\nimport { isCredentialFieldName, redactUrlCredentials } from \"./credentials\";\n\n/** `type` discriminator of an exported Layer Library bundle file. */\nexport const LAYER_LIBRARY_BUNDLE_TYPE = \"geolibre-layer-library\";\n\n/** Current bundle format version, for forward-compatible readers. */\nexport const LAYER_LIBRARY_BUNDLE_VERSION = 1;\n\n/**\n * Size ceiling for one library entry's serialized JSON, in bytes. The library\n * lives in IndexedDB and is meant to be a small, shareable index of *sources*,\n * so an entry that can only be stored by embedding a large feature set is\n * refused rather than silently turning the library into a data store.\n *\n * Applied to the whole entry (not just `geojson`), because an Add Vector Layer\n * entry can also carry a `metadata.embeddedGeoJSON` copy of its features.\n */\nexport const MAX_LAYER_LIBRARY_ENTRY_BYTES = 5 * 1024 * 1024;\n\n/**\n * Cap on how many entries a single normalization pass keeps. Enforced on the way\n * *in* — an imported bundle or a hand-edited database is untrusted, and without\n * a bound one file could push an unlimited number of records through this\n * synchronous pass and into IndexedDB. Mirrors `MAX_FAVORITES`, which caps the\n * Browser panel's other user-grown list on read as well as write.\n */\nexport const MAX_LAYER_LIBRARY_ENTRIES = 500;\n\n/**\n * Metadata keys dropped when capturing an entry. `resolvedUrl` is the dev-server\n * proxy rewrite of an XYZ template — a per-session artifact that must not be\n * baked into a saved source (the same reason `prepareLayerForSave` strips it).\n * `geometryEdited` tracks changes to the live layer, not a library source.\n */\nconst TRANSIENT_METADATA_KEYS = [\"resolvedUrl\", \"geometryEdited\"] as const;\n\nfunction nonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.trim() !== \"\";\n}\n\n/**\n * Whether a layer's features can be re-fetched from its source specification\n * alone, so an entry need not embed them: a tile/service template, a remote\n * file URL, or a GeoJSON source pointing at a URL. A `geojson` source whose\n * `data` is an inline FeatureCollection (not a URL string) deliberately does\n * not count.\n *\n * @param layer - The layer to inspect (source + metadata only).\n * @returns True when the source alone is enough to recreate the layer's data.\n */\nexport function hasRestorableLayerSource(\n layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">,\n): boolean {\n const source = layer.source ?? {};\n if (nonEmptyString(source.url)) return true;\n if (nonEmptyString(source.data)) return true;\n if (Array.isArray(source.tiles) && source.tiles.some(nonEmptyString)) return true;\n // A Cesium Ion asset is re-fetched from its id alone (under the cesium-ion\n // source kind, the same contract the globe loads it by).\n if (cesiumIonAssetId(layer) !== null) return true;\n return nonEmptyString((layer.metadata ?? {}).originalUrl);\n}\n\n/**\n * The absolute local path a layer's data can be **re-read** from, or undefined.\n *\n * A non-empty `sourcePath` is not enough on its own: a file picked in the\n * browser has no path, and both the vector and raster controls still record the\n * bare file *name* there for display. Each control marks a genuinely re-readable\n * absolute path differently, so both conventions are honored:\n *\n * - Vector / plain local GeoJSON: `metadata.localFileReloadable` alongside an\n * absolute `sourcePath` (`prepareLayerForSave`, `needsLocalFileReload`,\n * `restoreVectorLayers`).\n * - Raster / COG: `metadata.localFilePath` holds the absolute path outright,\n * and it is what `restoreRasterLayers` re-reads on restore. Without this\n * branch a locally-opened COG could not be saved at all, even though its\n * restore pass is fully able to replay it.\n *\n * Every candidate is re-validated, because a hand-edited project or an imported\n * bundle can set these fields to anything.\n */\nfunction layerLocalPath(layer: Pick<GeoLibreLayer, \"sourcePath\" | \"metadata\">): string | undefined {\n const metadata = layer.metadata ?? {};\n if (isReReadablePath(metadata.localFilePath)) return metadata.localFilePath;\n if (metadata.localFileReloadable !== true) return undefined;\n return isReReadablePath(layer.sourcePath) ? layer.sourcePath : undefined;\n}\n\n/**\n * Whether a value is a path safe to hand to a host file read: a non-empty\n * string, absolute, with no `..` traversal. Takes `unknown` and does the\n * non-empty check itself so every path-like field — `sourcePath`,\n * `metadata.localFilePath`, and any added later — goes through one predicate;\n * splitting the two halves is what let those two fields drift apart in the\n * first place.\n *\n * Applied to captured *and* imported entries, because a shared bundle is\n * untrusted input: a crafted `needsLocalFile` entry could otherwise point at any\n * file on the importing user's disk, and clicking it in My Data would read that\n * file. Mirrors the client-side re-validation the project-restore paths already\n * do before calling into Tauri.\n */\nfunction isReReadablePath(value: unknown): value is string {\n return nonEmptyString(value) && isAbsoluteFilesystemPath(value) && !hasPathTraversal(value);\n}\n\nfunction featureCount(geojson: FeatureCollection | undefined): number {\n return Array.isArray(geojson?.features) ? geojson.features.length : 0;\n}\n\n/**\n * Whether \"Save to library\" applies to a layer at all — the cheap predicate the\n * layer actions menu gates on. Two things have to hold:\n *\n * 1. There is something to re-add *from*: a re-fetchable source, a local file\n * path, or features to embed. A control-painted layer's features live in its\n * control rather than the store record — a tiles-mode Add Vector Layer layer\n * has no `layer.geojson` at all — so the caller can answer for that case with\n * `hasMaterializableFeatures`, keeping this gate consistent with the capture\n * path, which reads those features through `options.features`.\n * 2. The host can actually render it again. For a control-painted layer\n * ({@link isExternalNativeLayerRecord}) that means either the map's own sync\n * rebuilds it from the record or the owning plugin's restore pass can be\n * re-run, which only the host knows — so the caller supplies\n * `canRestoreControlPainted`. Without it, control-painted layers are refused\n * rather than saved into an entry that would re-add unrendered.\n *\n * A layer that qualifies here can still fail to save because its features are\n * too large to embed ({@link MAX_LAYER_LIBRARY_ENTRY_BYTES}); that outcome needs\n * the full capture, so it is reported by {@link captureLayerLibraryEntry} rather\n * than hidden behind a disabled menu item.\n *\n * @param layer - The layer to test.\n * @param options - `canRestoreControlPainted` answers whether the host can\n * re-render a control-painted layer (the desktop app passes its\n * `canRestoreLibraryLayer`); `hasMaterializableFeatures` answers whether it can\n * read the layer's features out of its control (the app passes\n * `isEmbeddableLocalVectorLayer`, the same predicate its save handler uses).\n * @returns Whether the layer can be offered to the library.\n */\nexport function canSaveLayerToLibrary(\n layer: GeoLibreLayer,\n options: {\n canRestoreControlPainted?: (layer: GeoLibreLayer) => boolean;\n hasMaterializableFeatures?: (layer: GeoLibreLayer) => boolean;\n } = {},\n): boolean {\n const hasSomethingToReAddFrom =\n hasRestorableLayerSource(layer) ||\n layerLocalPath(layer) !== undefined ||\n featureCount(layer.geojson) > 0 ||\n options.hasMaterializableFeatures?.(layer) === true;\n if (!hasSomethingToReAddFrom) return false;\n if (!isExternalNativeLayerRecord(layer)) return true;\n return options.canRestoreControlPainted?.(layer) === true;\n}\n\n/**\n * The `source` to store for a layer, with a live XYZ layer's session-specific\n * endpoint rewound to the template it was resolved from.\n *\n * `prepareLayerForSave` does the same thing when writing a project file: an XYZ\n * layer's `source.tiles`/`source.url` can hold a *resolved* endpoint while\n * `metadata.originalUrl` keeps the template, and baking the resolved value into\n * a saved record makes it replay a stale host instead of re-resolving. A library\n * entry outlives a session by design, so it needs the same rewind — stripping\n * `metadata.resolvedUrl` alone would leave the resolved endpoint sitting in\n * `source`.\n */\nfunction sourceForCapture(layer: GeoLibreLayer): Record<string, unknown> {\n const source = layer.source ?? {};\n if (layer.type !== \"xyz\") return source;\n const originalUrl = nonEmptyString((layer.metadata ?? {}).originalUrl)\n ? layer.metadata.originalUrl\n : nonEmptyString(source.url)\n ? source.url\n : null;\n if (!originalUrl) return source;\n // A TileJSON layer keeps its own tile templates — see `prepareLayerForSave`.\n // The library re-adds a captured source verbatim, with no re-resolution step,\n // so collapsing `tiles` onto the document URL here would never load a tile.\n if (typeof (layer.metadata ?? {}).tilejsonUrl === \"string\") {\n return { ...source, url: originalUrl };\n }\n return { ...source, tiles: [originalUrl], url: originalUrl };\n}\n\n/** Why a layer could not be captured into the library. */\nexport type LayerLibraryCaptureFailure =\n /** Nothing to re-add from: no source URL, no local path, no features. */\n | \"no-source\"\n /** Its features are the only copy, and they exceed the per-entry size cap. */\n | \"features-too-large\"\n /**\n * The entry exceeds the cap without embedding any features — its style,\n * attribute form, joins, or virtual fields are simply that large. Reported\n * separately so the message does not blame the layer's data.\n */\n | \"config-too-large\";\n\n/** Outcome of {@link captureLayerLibraryEntry}. */\nexport type LayerLibraryCaptureResult =\n | { ok: true; entry: LayerLibraryEntry }\n | { ok: false; reason: LayerLibraryCaptureFailure };\n\n/**\n * Serialized size of a value in UTF-8 bytes — what IndexedDB and an exported\n * bundle actually cost. Measured with {@link TextEncoder} rather than\n * `JSON.stringify().length`, which counts UTF-16 code units: CJK, Arabic, and\n * Cyrillic text is 1 unit but 2-3 bytes per character, so a length-based check\n * would let an entry through at 2-3x the cap it is supposed to enforce.\n */\nfunction jsonByteLength(value: unknown): number {\n try {\n const json = JSON.stringify(value);\n return json === undefined ? 0 : new TextEncoder().encode(json).length;\n } catch {\n // A source or metadata value with a circular reference cannot be stored at\n // all, so treat it as over any cap.\n return Number.POSITIVE_INFINITY;\n }\n}\n\n/**\n * Whether a layer is painted by a plugin control rather than by GeoLibre's own\n * layer sync (Add Vector Layer, the deck.gl raster/COG control, 3D Tiles,\n * LiDAR, …). These recreate their map output from the layer record through\n * their plugin's restore pass, so an entry for one must carry the metadata that\n * pass reads — and re-adding it has to run that pass.\n *\n * @param layer - The layer to test (metadata only).\n * @returns True when a plugin control owns the layer's rendering.\n */\nexport function isExternalNativeLayerRecord(layer: Pick<GeoLibreLayer, \"metadata\">): boolean {\n return (layer.metadata ?? {}).externalNativeLayer === true;\n}\n\n/**\n * Capture a configured layer as a reusable Layer Library entry: its source\n * spec, layer type, full style (labels included), opacity, metadata, and the\n * per-layer configuration that makes it \"fully configured\" — joins, virtual\n * fields, and the attribute form. Project-specific placement (`id`,\n * `groupId`, `beforeId`) is deliberately not captured.\n *\n * Features are embedded only when the source cannot supply them (an in-memory\n * or local-file layer). Where they land depends on who paints the layer:\n * GeoLibre-rendered layers keep them in {@link LayerLibraryEntry.geojson},\n * while a control-painted layer gets them in `metadata.embeddedGeoJSON` — the\n * same field the project format's Embed/Share flow writes and the plugin's\n * restore pass reads.\n *\n * If the result exceeds {@link MAX_LAYER_LIBRARY_ENTRY_BYTES} the capture\n * degrades rather than failing outright: it falls back to a path-only entry\n * (`needsLocalFile`) when a local file can supply the features.\n *\n * @param layer - The live layer to save.\n * @param options - Fresh entry `id`, ISO `addedAt`, an optional `name` override\n * (defaults to the layer's name), and optional `features` to embed instead of\n * the layer's own `geojson` (the caller reads a control-painted layer's\n * current data from its control).\n * @returns The entry, or the reason it could not be captured.\n */\nexport function captureLayerLibraryEntry(\n layer: GeoLibreLayer,\n options: { id: string; addedAt: string; name?: string; features?: FeatureCollection },\n): LayerLibraryCaptureResult {\n const localPath = layerLocalPath(layer);\n const hasSource = hasRestorableLayerSource(layer);\n const supplied = featureCount(options.features) > 0 ? options.features : undefined;\n const features = supplied ?? (featureCount(layer.geojson) > 0 ? layer.geojson : undefined);\n if (!hasSource && !localPath && !features) {\n return { ok: false, reason: \"no-source\" };\n }\n\n const metadata: Record<string, unknown> = { ...(layer.metadata ?? {}) };\n for (const key of TRANSIENT_METADATA_KEYS) delete metadata[key];\n // A stale embedded copy from the project this layer was loaded from: the\n // capture below re-embeds current data (or drops the copy when the source can\n // re-fetch it), so never carry the old blob through.\n delete metadata.embeddedGeoJSON;\n\n const controlPainted = isExternalNativeLayerRecord(layer);\n // Embed features only when the source cannot re-fetch them; a layer with a\n // restorable source stays data-free so its entry keeps reflecting the source.\n const embed = !hasSource && features ? structuredClone(features) : undefined;\n if (embed && controlPainted) {\n metadata.embeddedGeoJSON = embed;\n // Mirrors the project Embed flow: with the data embedded, the restore pass\n // must replay it rather than a file path that may not exist on this machine.\n delete metadata.localFileReloadable;\n }\n\n const base: LayerLibraryEntry = {\n id: options.id,\n name: options.name?.trim() || layer.name,\n addedAt: options.addedAt,\n layerType: layer.type,\n source: structuredClone(sourceForCapture(layer)),\n style: structuredClone({ ...DEFAULT_LAYER_STYLE, ...layer.style }),\n opacity:\n typeof layer.opacity === \"number\" && Number.isFinite(layer.opacity) ? layer.opacity : 1,\n metadata: structuredClone(metadata),\n ...(localPath ? { sourcePath: localPath } : {}),\n ...(layer.joins?.length ? { joins: structuredClone(layer.joins) } : {}),\n ...(layer.virtualFields?.length ? { virtualFields: structuredClone(layer.virtualFields) } : {}),\n ...(layer.attributeForm ? { attributeForm: structuredClone(layer.attributeForm) } : {}),\n ...(layer.popup ? { popup: structuredClone(layer.popup) } : {}),\n ...(layer.descriptiveMetadata\n ? { descriptiveMetadata: structuredClone(layer.descriptiveMetadata) }\n : {}),\n };\n const withFeatures = embed && !controlPainted ? { ...base, geojson: embed } : base;\n\n if (jsonByteLength(withFeatures) <= MAX_LAYER_LIBRARY_ENTRY_BYTES) {\n return { ok: true, entry: withFeatures };\n }\n if (embed && localPath) {\n // Last resort for an oversized local file: keep the path and let the\n // desktop host re-read it at add time.\n const { embeddedGeoJSON: _dropped, ...leanMetadata } = withFeatures.metadata;\n const pathOnly: LayerLibraryEntry = {\n ...withFeatures,\n metadata: leanMetadata,\n needsLocalFile: true,\n };\n delete pathOnly.geojson;\n if (jsonByteLength(pathOnly) <= MAX_LAYER_LIBRARY_ENTRY_BYTES) {\n return { ok: true, entry: pathOnly };\n }\n }\n // Blame the right thing: an entry can also exceed the cap with no embedded\n // features at all (a rule-based style with hundreds of rules, a large\n // attribute form), and telling that user their \"features are the only copy\"\n // would send them looking in the wrong place.\n return { ok: false, reason: embed ? \"features-too-large\" : \"config-too-large\" };\n}\n\n/**\n * The presentation state a re-added layer takes from its entry. For the\n * `local-file` plan the host imports the file first (producing a default-styled\n * layer) and then applies this patch, so a degraded entry still comes back\n * \"fully configured\" rather than losing the styling the entry preserved.\n */\nexport interface LayerLibraryConfigPatch {\n name: string;\n style: LayerStyle;\n opacity: number;\n joins?: LayerJoin[];\n virtualFields?: LayerVirtualField[];\n attributeForm?: AttributeFormConfig;\n popup?: LayerPopupConfig;\n descriptiveMetadata?: LayerDescriptiveMetadata;\n}\n\n/** How an entry should be re-added to the current project. */\nexport type LayerLibraryAddPlan =\n /** Add this layer record to the store; the map sync renders it. */\n | { kind: \"layer\"; layer: GeoLibreLayer }\n /**\n * The entry's features live only in a local file the host must re-read (its\n * data was too large to embed), so the caller runs its local-file add path for\n * `path` and then applies `config` to the layer that import produced. Only\n * reachable on a host with filesystem access.\n */\n | { kind: \"local-file\"; path: string; config: LayerLibraryConfigPatch };\n\n/**\n * Plan how to re-add a library entry to the current project.\n *\n * @param entry - The library entry to add.\n * @param options - The fresh layer `id` to create it under.\n * @returns Either the layer record to add, or the local file to re-read.\n */\nexport function planLayerLibraryAdd(\n entry: LayerLibraryEntry,\n options: { id: string },\n): LayerLibraryAddPlan {\n if (layerLibraryEntryNeedsLocalFile(entry)) {\n return {\n kind: \"local-file\",\n // Non-null by the guard above, which also re-validates the path.\n path: entry.sourcePath as string,\n config: layerLibraryConfigPatch(entry),\n };\n }\n return {\n kind: \"layer\",\n layer: {\n id: options.id,\n name: entry.name,\n type: entry.layerType,\n source: structuredClone(entry.source),\n // Always added visible: the user just asked for this layer.\n visible: true,\n opacity: entry.opacity,\n style: structuredClone(entry.style),\n metadata: structuredClone(entry.metadata),\n ...(entry.sourcePath ? { sourcePath: entry.sourcePath } : {}),\n ...(entry.geojson ? { geojson: structuredClone(entry.geojson) } : {}),\n ...(entry.joins ? { joins: structuredClone(entry.joins) } : {}),\n ...(entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {}),\n ...(entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {}),\n ...(entry.popup ? { popup: structuredClone(entry.popup) } : {}),\n ...(entry.descriptiveMetadata\n ? { descriptiveMetadata: structuredClone(entry.descriptiveMetadata) }\n : {}),\n },\n };\n}\n\n/**\n * The presentation state to apply to a layer the host imported from an entry's\n * file, so the `local-file` plan preserves everything the entry captured.\n *\n * @param entry - The entry being re-added.\n * @returns The patch to merge onto the imported layer.\n */\nexport function layerLibraryConfigPatch(entry: LayerLibraryEntry): LayerLibraryConfigPatch {\n return {\n name: entry.name,\n style: structuredClone(entry.style),\n opacity: entry.opacity,\n ...(entry.joins ? { joins: structuredClone(entry.joins) } : {}),\n ...(entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {}),\n ...(entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {}),\n ...(entry.popup ? { popup: structuredClone(entry.popup) } : {}),\n ...(entry.descriptiveMetadata\n ? { descriptiveMetadata: structuredClone(entry.descriptiveMetadata) }\n : {}),\n };\n}\n\n/**\n * The saved joins whose source layer is not present in the target project.\n *\n * A {@link LayerJoin} points at another layer by its **project-local id**, so a\n * join travels only as far as the project it was captured in: re-adding an entry\n * elsewhere leaves those joins referring to layers that do not exist. The join\n * engine degrades quietly (`applyLayerJoins` just drops an unresolved join from\n * its active set), which is the right runtime behavior but a silent one — so the\n * caller uses this to tell the user which joins did not come back, instead of\n * letting them find a column missing later.\n *\n * The joins are still captured and reapplied rather than dropped: they resolve\n * whenever the source layer *is* there, which covers re-adding into the project\n * the entry came from, or into one restored from the same saved project.\n *\n * @param entry - The entry being re-added.\n * @param presentLayerIds - Ids of the layers in the target project.\n * @returns Ids of the join-target layers that cannot resolve ({@link LayerJoin}\n * has no display name).\n */\nexport function unresolvedLayerLibraryJoins(\n entry: Pick<LayerLibraryEntry, \"joins\">,\n presentLayerIds: Iterable<string>,\n): string[] {\n if (!entry.joins?.length) return [];\n const present = new Set(presentLayerIds);\n return entry.joins\n .filter((join) => join.enabled !== false && !present.has(join.joinLayerId))\n .map((join) => join.joinLayerId);\n}\n\n/**\n * Whether re-adding an entry needs a host that can read local files — used to\n * badge the entry in the Browser panel and to explain the failure in the\n * browser build, where there is no filesystem to re-read from.\n *\n * @param entry - The entry to test.\n * @returns True when only a filesystem-capable host can add it.\n */\nexport function layerLibraryEntryNeedsLocalFile(entry: LayerLibraryEntry): boolean {\n return entry.needsLocalFile === true && isReReadablePath(entry.sourcePath);\n}\n\n/** A plain JSON object (not an array, not null), or undefined. */\nfunction plainObject(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/** Whether every listed key on an object holds a non-empty string. */\nfunction hasStringKeys(object: Record<string, unknown>, keys: readonly string[]): boolean {\n return keys.every((key) => nonEmptyString(object[key]));\n}\n\n/**\n * Keep only the {@link LayerJoin} members a hand-edited or imported bundle got\n * right. A join missing any of its required fields resolves to nothing and would\n * sit in the Joins UI as a permanently-broken definition, so drop it rather than\n * the whole entry.\n */\nfunction validJoins(value: unknown): LayerJoin[] | undefined {\n const items = objectArray(value)?.filter((join) =>\n hasStringKeys(join, [\"id\", \"joinLayerId\", \"targetField\", \"joinField\"]),\n );\n return items && items.length > 0 ? (structuredClone(items) as unknown as LayerJoin[]) : undefined;\n}\n\n/** Keep only {@link LayerVirtualField} members that carry an id, name, and expression. */\nfunction validVirtualFields(value: unknown): LayerVirtualField[] | undefined {\n const items = objectArray(value)?.filter((field) =>\n hasStringKeys(field, [\"id\", \"name\", \"expression\"]),\n );\n return items && items.length > 0\n ? (structuredClone(items) as unknown as LayerVirtualField[])\n : undefined;\n}\n\n/**\n * Keep an {@link AttributeFormConfig} only when every field entry names the\n * feature property it configures; a field with no `field` key would apply to\n * nothing.\n */\nfunction validAttributeForm(value: unknown): AttributeFormConfig | undefined {\n const config = plainObject(value);\n if (!config || !Array.isArray(config.fields)) return undefined;\n const fields = config.fields.filter(\n (field) =>\n plainObject(field) !== undefined && nonEmptyString((field as { field?: unknown }).field),\n );\n if (fields.length === 0) return undefined;\n return structuredClone({ ...config, fields }) as unknown as AttributeFormConfig;\n}\n\n/**\n * A structurally plausible popup design, or undefined. `fields` is optional (a\n * config may carry only a title or the hover flag), so unlike the attribute\n * form an empty field list does not disqualify the block — but a config with\n * nothing set at all is dropped rather than stored as an empty object.\n */\nfunction validPopup(value: unknown): LayerPopupConfig | undefined {\n const config = plainObject(value);\n if (!config) return undefined;\n const fields = Array.isArray(config.fields)\n ? config.fields.filter(\n (field) =>\n plainObject(field) !== undefined && nonEmptyString((field as { field?: unknown }).field),\n )\n : undefined;\n const rest = { ...config };\n delete rest.fields;\n if (!fields?.length && Object.keys(rest).length === 0) return undefined;\n return structuredClone({\n ...rest,\n ...(fields?.length ? { fields } : {}),\n }) as unknown as LayerPopupConfig;\n}\n\n/** An array of plain JSON objects, or undefined when nothing usable is present. */\nfunction objectArray(value: unknown): Record<string, unknown>[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const items = value.filter((item): item is Record<string, unknown> => Boolean(plainObject(item)));\n return items.length > 0 ? items : undefined;\n}\n\n/** A GeoJSON FeatureCollection shape, or undefined. */\nfunction featureCollection(value: unknown): FeatureCollection | undefined {\n const object = plainObject(value);\n if (!object || object.type !== \"FeatureCollection\" || !Array.isArray(object.features)) {\n return undefined;\n }\n return object as unknown as FeatureCollection;\n}\n\n/**\n * Coerce an untrusted entries array (an imported bundle, or a record written by\n * an older version) into valid {@link LayerLibraryEntry} records. Entries\n * without a usable id, name, or layer type are dropped, ids are de-duplicated,\n * the style is sanitized through the Style Manager's\n * {@link sanitizeLayerStylePatch} and completed against the defaults, and each\n * optional block is kept only when structurally plausible.\n *\n * Entries that cannot be re-added at all (no source, no path, no features) are\n * dropped rather than shown as permanently-failing rows.\n *\n * @param value - The raw entries value.\n * @returns Normalized, de-duplicated entries (empty when none survive).\n */\nexport function normalizeLayerLibraryEntries(value: unknown): LayerLibraryEntry[] {\n if (!Array.isArray(value)) return [];\n const entries: LayerLibraryEntry[] = [];\n const seen = new Set<string>();\n for (const raw of value) {\n const candidate = plainObject(raw);\n if (!candidate) continue;\n const id = typeof candidate.id === \"string\" ? candidate.id.trim() : \"\";\n const name = typeof candidate.name === \"string\" ? candidate.name.trim() : \"\";\n // An unknown layer type is dropped rather than coerced: every downstream\n // renderer branches on it, so a made-up value would render nothing.\n const layerType = LAYER_TYPES.includes(candidate.layerType as LayerType)\n ? (candidate.layerType as LayerType)\n : undefined;\n if (!id || !name || !layerType || seen.has(id)) continue;\n const source = plainObject(candidate.source) ?? {};\n const metadata = { ...(plainObject(candidate.metadata) ?? {}) };\n // Strip the same per-session keys the capture path drops, so a stale one on\n // an older record or an imported bundle is cleaned up on load instead of\n // replaying a dev-server proxy rewrite indefinitely.\n for (const key of TRANSIENT_METADATA_KEYS) delete metadata[key];\n // `metadata.localFilePath` is the *second* path in an entry, and the one\n // `restoreRasterLayers` re-reads — so it needs the same validation as\n // `sourcePath` below, not a free pass because it happens to live in\n // metadata. Without this a shared bundle could keep a valid-looking\n // `sourcePath` while pointing the raster restore at any file on the\n // importing user's disk.\n if (\"localFilePath\" in metadata && !isReReadablePath(metadata.localFilePath)) {\n delete metadata.localFilePath;\n }\n // A bundle is shareable, so its `sourcePath` is untrusted: keep it only when\n // it is an absolute, traversal-free path a host could legitimately re-read.\n // Without this a crafted entry could point `onAddFilePath` at any file on the\n // importing user's disk.\n const sourcePath = isReReadablePath(candidate.sourcePath) ? candidate.sourcePath : undefined;\n const geojson = featureCollection(candidate.geojson);\n // Same \"is there anything to re-add from\" gate as the capture path, so a\n // hand-edited bundle cannot introduce an entry that always fails. Both path\n // conventions count (a local raster's only path is `metadata.localFilePath`,\n // already validated above), and so does a control-painted entry's\n // `metadata.embeddedGeoJSON`, which holds its features instead of `geojson`.\n if (\n !hasRestorableLayerSource({ source, metadata }) &&\n !sourcePath &&\n !nonEmptyString(metadata.localFilePath) &&\n featureCount(geojson) === 0 &&\n featureCount(featureCollection(metadata.embeddedGeoJSON)) === 0\n ) {\n continue;\n }\n const opacity =\n typeof candidate.opacity === \"number\" &&\n Number.isFinite(candidate.opacity) &&\n candidate.opacity >= 0 &&\n candidate.opacity <= 1\n ? candidate.opacity\n : 1;\n const normalized: LayerLibraryEntry = {\n id,\n name,\n addedAt: typeof candidate.addedAt === \"string\" ? candidate.addedAt : \"\",\n layerType,\n source: structuredClone(source),\n style: { ...DEFAULT_LAYER_STYLE, ...sanitizeLayerStylePatch(candidate.style) },\n opacity,\n metadata: structuredClone(metadata),\n ...(sourcePath ? { sourcePath } : {}),\n ...(() => {\n // Each block is kept only when its members carry the fields their engine\n // needs; a malformed member is dropped rather than the whole entry.\n const joins = validJoins(candidate.joins);\n const virtualFields = validVirtualFields(candidate.virtualFields);\n const attributeForm = validAttributeForm(candidate.attributeForm);\n const popup = validPopup(candidate.popup);\n const descriptiveMetadata = normalizeLayerDescriptiveMetadata(\n candidate.descriptiveMetadata,\n );\n return {\n ...(joins ? { joins } : {}),\n ...(virtualFields ? { virtualFields } : {}),\n ...(attributeForm ? { attributeForm } : {}),\n ...(popup ? { popup } : {}),\n ...(descriptiveMetadata ? { descriptiveMetadata } : {}),\n };\n })(),\n ...(geojson ? { geojson: structuredClone(geojson) } : {}),\n // `needsLocalFile` only means anything with a path to read.\n ...(candidate.needsLocalFile === true && sourcePath ? { needsLocalFile: true } : {}),\n };\n // The per-entry size ceiling is enforced here too, not only on capture: an\n // imported bundle is produced elsewhere, so without this a crafted (or just\n // hand-assembled) file could push tens of MB of embedded features per entry\n // into IndexedDB, exactly the \"library as a data store\" outcome the cap\n // exists to prevent.\n if (jsonByteLength(normalized) > MAX_LAYER_LIBRARY_ENTRY_BYTES) continue;\n seen.add(id);\n entries.push(normalized);\n if (entries.length >= MAX_LAYER_LIBRARY_ENTRIES) break;\n }\n return entries;\n}\n\n/**\n * Copy of an entry with per-user credentials removed from its source.\n *\n * `source` fields that can hold a per-user credential are removed on export.\n * `requestHeaders`, for example, carries the bearer token / API key an\n * authenticated 3D Tiles layer was added with (see\n * `persistedThreeDTilesRequestHeaders`, which keeps non-Google auth headers when\n * persisting into a project file).\n *\n * Kept in the local IndexedDB entry so re-adding on this machine still works,\n * and dropped only from the bundle: Export is pitched as a one-click \"share with\n * your team\" action, which makes it a far more direct exposure path than handing\n * someone a whole project file. A recipient re-enters their own credentials,\n * which is the correct outcome.\n *\n * The names come from `isCredentialFieldName`, so this path and project export\n * share one registry instead of drifting apart.\n */\nfunction redactEntryForExport(entry: LayerLibraryEntry): LayerLibraryEntry {\n const source = { ...entry.source };\n let changed = false;\n for (const key of Object.keys(source)) {\n if (isCredentialFieldName(key)) {\n delete source[key];\n changed = true;\n }\n }\n // Sweep every URL-shaped value in `source` rather than naming the fields that\n // hold one. Different layer types put their URL in different places — `url`,\n // `data` (a GeoJSON source pointing at a URL, which\n // `hasRestorableLayerSource` already treats as re-fetchable), `tiles[]` — and\n // a per-field list has to be extended for each new one, with a silent\n // credential leak as the cost of forgetting.\n for (const [key, value] of Object.entries(source)) {\n const redacted = redactSourceValue(value);\n if (redacted !== value) {\n source[key] = redacted;\n changed = true;\n }\n }\n // `metadata.originalUrl` is the template a tile layer restores from, so it\n // carries the same credential as `source.tiles`.\n let metadata = entry.metadata;\n if (typeof metadata.originalUrl === \"string\") {\n const redacted = redactUrlCredentials(metadata.originalUrl);\n if (redacted !== metadata.originalUrl) {\n metadata = { ...metadata, originalUrl: redacted };\n changed = true;\n }\n }\n return changed ? { ...entry, source, metadata } : entry;\n}\n\n/** Depth limit for the export sweep, so a pathological source cannot recurse far. */\nconst MAX_REDACT_DEPTH = 6;\n\n/**\n * Whether a value is GeoJSON data rather than source configuration. The sweep\n * stops at these: `source.data` can hold an inline FeatureCollection, and\n * descending into it would both walk every feature and *rewrite* any feature\n * property that happens to look like a URL with a `key=` parameter — corrupting\n * the user's data in the name of protecting it. Credentials live in\n * configuration, not in features.\n */\nfunction isGeoJsonPayload(value: Record<string, unknown>): boolean {\n return (\n value.type === \"FeatureCollection\" ||\n value.type === \"Feature\" ||\n value.type === \"GeometryCollection\"\n );\n}\n\n/**\n * Redact a `source` value of any shape: strings are treated as possible URLs,\n * arrays as lists of them (tile templates), and nested objects are swept\n * recursively — a layer type that groups its endpoint and credentials as\n * `{ store: { url, headers } }` must not slip a token past the export just\n * because it is one level deeper. Nested `requestHeaders`-style keys are dropped\n * the same way the top level drops them.\n *\n * GeoJSON payloads are left alone (see {@link isGeoJsonPayload}) and recursion is\n * depth-capped. Returns the original reference when nothing changed, so callers\n * can detect that by identity.\n */\nfunction redactSourceValue(value: unknown, depth = 0): unknown {\n if (typeof value === \"string\") return redactUrlCredentials(value);\n if (depth >= MAX_REDACT_DEPTH) {\n // Fail closed, matching `redactConfigurationValue` in credentials.ts: no\n // built-in layer needs configuration nested this deep to be re-addable, and\n // handing the value back unswept would let a credential ride out of the\n // bundle merely by sitting past the traversal cap. Only the exported bundle\n // loses it; the local IndexedDB entry is untouched.\n return undefined;\n }\n if (Array.isArray(value)) {\n const redacted = value.map((item) => redactSourceValue(item, depth + 1));\n return redacted.some((item, index) => item !== value[index]) ? redacted : value;\n }\n const object = plainObject(value);\n if (!object || isGeoJsonPayload(object)) return value;\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(object)) {\n if (isCredentialFieldName(key)) {\n changed = true;\n continue;\n }\n const redacted = redactSourceValue(nested, depth + 1);\n if (redacted !== nested) changed = true;\n out[key] = redacted;\n }\n return changed ? out : value;\n}\n\n/**\n * Serialize Layer Library entries into the shareable bundle JSON written by the\n * Export action and read back by {@link parseLayerLibrary}. Per-user credentials\n * are redacted first (see {@link EXPORT_REDACTED_SOURCE_KEYS}).\n *\n * @param entries - The entries to export.\n * @returns Pretty-printed bundle JSON.\n */\nexport function serializeLayerLibrary(entries: LayerLibraryEntry[]): string {\n return JSON.stringify(\n {\n type: LAYER_LIBRARY_BUNDLE_TYPE,\n version: LAYER_LIBRARY_BUNDLE_VERSION,\n entries: entries.map(redactEntryForExport),\n },\n null,\n 2,\n );\n}\n\n/**\n * Parse a Layer Library bundle produced by {@link serializeLayerLibrary} (a\n * bare entries array is also accepted, so hand-authored files work). Entries\n * are normalized through {@link normalizeLayerLibraryEntries}.\n *\n * @param json - The bundle file content.\n * @returns The normalized entries.\n * @throws Error when the JSON is not a layer-library bundle or holds no usable\n * entries.\n */\nexport function parseLayerLibrary(json: string): LayerLibraryEntry[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"Not a valid layer library file (invalid JSON).\");\n }\n let rawEntries: unknown = null;\n if (Array.isArray(parsed)) {\n rawEntries = parsed;\n } else if (plainObject(parsed)?.type === LAYER_LIBRARY_BUNDLE_TYPE) {\n // Refuse a bundle from a newer format rather than misreading it with v1\n // semantics. Bare arrays stay accepted for hand-authored files.\n if (plainObject(parsed)?.version !== LAYER_LIBRARY_BUNDLE_VERSION) {\n throw new Error(\"Unsupported layer library version.\");\n }\n rawEntries = plainObject(parsed)?.entries;\n }\n if (rawEntries === null) {\n throw new Error(\"Not a valid layer library file.\");\n }\n const entries = normalizeLayerLibraryEntries(rawEntries);\n if (entries.length === 0) {\n throw new Error(\"The layer library file holds no usable entries.\");\n }\n return entries;\n}\n\n/**\n * Create a fresh unique id for a new library entry.\n *\n * @returns A random id (UUID when available).\n */\nexport function createLayerLibraryEntryId(): string {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n return crypto.randomUUID();\n }\n return `layer-lib-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n","/**\n * The Style Manager, Layer Library and Template Library. The app-level lists\n * live outside the project lifecycle (persisted by the desktop app); only the\n * project-scoped style list is saved with the project.\n */\nimport { MAX_LAYER_LIBRARY_ENTRIES } from \"../layer-library\";\nimport type { LayerLibraryEntry, ProjectTemplateEntry, StyleLibraryEntry } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface LibrariesSlice {\n /**\n * App-level Style Manager library (issue #1294). Lives outside the project\n * lifecycle: never serialized into the project file, untouched by\n * newProject/loadProject, and persisted by the desktop app (IndexedDB).\n */\n styleLibrary: StyleLibraryEntry[];\n /**\n * App-level Layer Library (issue #1520) — the Browser panel's My Data\n * section. Like {@link styleLibrary} it lives outside the project lifecycle:\n * never serialized into the project file, untouched by newProject/loadProject,\n * and persisted by the desktop app (IndexedDB). Most recently saved first.\n */\n layerLibrary: LayerLibraryEntry[];\n /**\n * App-level Template Library. Persisted by the desktop app (IndexedDB).\n */\n templateLibrary: ProjectTemplateEntry[];\n /**\n * Project-scoped Style Manager entries (issue #1294), serialized into the\n * `.geolibre.json` `styleLibrary` array and replaced on project load.\n */\n projectStyleLibrary: StyleLibraryEntry[];\n\n /**\n * Replace the app-level style library wholesale. Used by the persistence\n * layer on startup and by bundle imports.\n */\n setStyleLibrary: (entries: StyleLibraryEntry[]) => void;\n /**\n * Insert or replace (matching by `id`) a Style Manager entry in the given\n * scope. Scope-local, like {@link deleteStyleLibraryEntry}: the other\n * scope's list is never touched, since a same id there can belong to an\n * unrelated entry after loading a project authored elsewhere. Project-scope\n * saves mark the project dirty.\n */\n saveStyleLibraryEntry: (entry: StyleLibraryEntry, scope?: \"app\" | \"project\") => void;\n /**\n * Remove a Style Manager entry by id. When `scope` is given only that list\n * is touched — the two scopes can legitimately hold the same id after\n * loading a project authored elsewhere, and deleting a project entry must\n * not erase a local app-library style (or vice versa). Omitting `scope`\n * removes the id from both lists.\n */\n deleteStyleLibraryEntry: (id: string, scope?: \"app\" | \"project\") => void;\n\n /**\n * Replace the app-level Layer Library wholesale. Used by the persistence\n * layer on startup and by bundle imports.\n */\n setLayerLibrary: (entries: LayerLibraryEntry[]) => void;\n /**\n * Insert or replace (matching by `id`) a Layer Library entry. New entries go\n * to the front so the most recently saved layer leads the My Data section.\n */\n saveLayerLibraryEntry: (entry: LayerLibraryEntry) => void;\n /** Rename a Layer Library entry; a blank name is ignored. */\n renameLayerLibraryEntry: (id: string, name: string) => void;\n /** Remove a Layer Library entry by id. */\n deleteLayerLibraryEntry: (id: string) => void;\n\n /** Replace the app-level template library wholesale. */\n setTemplateLibrary: (templates: ProjectTemplateEntry[]) => void;\n /** Insert or replace a template in the Template Library. */\n saveTemplateEntry: (entry: ProjectTemplateEntry) => void;\n /** Remove a template entry by id from the Template Library. */\n deleteTemplateEntry: (id: string) => void;\n}\n\nexport const createLibrariesSlice: SliceCreator<LibrariesSlice> = (set) => ({\n styleLibrary: [],\n layerLibrary: [],\n templateLibrary: [],\n projectStyleLibrary: [],\n\n setStyleLibrary: (entries) => set({ styleLibrary: entries }),\n saveStyleLibraryEntry: (entry, scope = \"app\") =>\n set((s) => {\n const upsert = (list: StyleLibraryEntry[]) =>\n list.some((e) => e.id === entry.id)\n ? list.map((e) => (e.id === entry.id ? entry : e))\n : [...list, entry];\n if (scope === \"project\") {\n return {\n projectStyleLibrary: upsert(s.projectStyleLibrary),\n isDirty: true,\n };\n }\n // App-level saves don't touch the project file, so no dirty flag.\n return { styleLibrary: upsert(s.styleLibrary) };\n }),\n deleteStyleLibraryEntry: (id, scope) =>\n set((s) => {\n const inProject = scope !== \"app\" && s.projectStyleLibrary.some((e) => e.id === id);\n const inLibrary = scope !== \"project\" && s.styleLibrary.some((e) => e.id === id);\n // Keep untouched scopes reference-stable so the IndexedDB\n // persistence (which watches the styleLibrary reference) does not\n // rewrite an unchanged library.\n return {\n styleLibrary: inLibrary ? s.styleLibrary.filter((e) => e.id !== id) : s.styleLibrary,\n projectStyleLibrary: inProject\n ? s.projectStyleLibrary.filter((e) => e.id !== id)\n : s.projectStyleLibrary,\n isDirty: s.isDirty || inProject,\n };\n }),\n\n // The entry cap is applied on every write, not just when reading\n // untrusted input, so ordinary use (repeated saves, importing several\n // bundles over time) cannot grow the library past it. Mirrors\n // `writeBrowserFavorites`, which slices to MAX_FAVORITES on each write.\n setLayerLibrary: (entries) => set({ layerLibrary: entries.slice(0, MAX_LAYER_LIBRARY_ENTRIES) }),\n saveLayerLibraryEntry: (entry) =>\n set((s) => ({\n // App-level saves don't touch the project file, so no dirty flag\n // (mirrors saveStyleLibraryEntry's \"app\" scope). A new entry goes to\n // the front, so at the cap the oldest falls off the end.\n layerLibrary: s.layerLibrary.some((e) => e.id === entry.id)\n ? s.layerLibrary.map((e) => (e.id === entry.id ? entry : e))\n : [entry, ...s.layerLibrary].slice(0, MAX_LAYER_LIBRARY_ENTRIES),\n })),\n renameLayerLibraryEntry: (id, name) =>\n set((s) => {\n const trimmed = name.trim();\n // Keep the array reference stable for a no-op rename so the\n // IndexedDB persistence (which watches the reference) skips a write.\n if (!trimmed || !s.layerLibrary.some((e) => e.id === id && e.name !== trimmed)) {\n return {};\n }\n return {\n layerLibrary: s.layerLibrary.map((e) => (e.id === id ? { ...e, name: trimmed } : e)),\n };\n }),\n deleteLayerLibraryEntry: (id) =>\n set((s) =>\n s.layerLibrary.some((e) => e.id === id)\n ? { layerLibrary: s.layerLibrary.filter((e) => e.id !== id) }\n : {},\n ),\n\n setTemplateLibrary: (templates) => set({ templateLibrary: templates }),\n saveTemplateEntry: (entry) =>\n set((s) => ({\n templateLibrary: s.templateLibrary.some((t) => t.id === entry.id)\n ? s.templateLibrary.map((t) => (t.id === entry.id ? entry : t))\n : [...s.templateLibrary, entry],\n })),\n deleteTemplateEntry: (id) =>\n set((s) => ({\n templateLibrary: s.templateLibrary.filter((t) => t.id !== id),\n })),\n});\n","import { normalizeCesiumBasemap } from \"./cesium-imagery\";\nimport { redactUrlCredentials } from \"./credentials\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n DEFAULT_BASEMAP,\n DEFAULT_LAYER_STYLE,\n DEFAULT_LEGEND_CONFIG,\n DEFAULT_PROJECT_PREFERENCES,\n DEFAULT_DASHBOARD_COLUMNS,\n DEFAULT_MAP_GRID_LAYOUT,\n DEFAULT_PRIMARY_RENDERER,\n DEFAULT_STORY_MAP,\n MAX_DASHBOARD_COLUMNS,\n MAX_MAP_GRID_DIM,\n MIN_DASHBOARD_COLUMNS,\n PROJECT_VERSION,\n type DashboardWidget,\n type DashboardWidgetAggregation,\n type DashboardWidgetType,\n type GeoLibreLayer,\n type GeoLibreProject,\n type IndicatorAggregation,\n type LayerGroup,\n type LayerStyle,\n type LegendConfig,\n type LegendCustomEntry,\n type LegendCustomItem,\n type LegendItemOverride,\n type MapGridLayout,\n type MapRendererKind,\n type MapScaleUnit,\n type MapViewState,\n MAX_PROCESSING_HISTORY,\n type ModelGraphEdge,\n type ModelGraphNode,\n type ModelGraphNodeKind,\n type ModelToolProvider,\n type ProcessingModel,\n type ProcessingModelGraph,\n type ProcessingRun,\n type ProcessingRunKind,\n type SecondaryMapView,\n type ProcessingModelStep,\n type ProjectPluginControlPosition,\n type ProjectPluginState,\n type ProjectPreferences,\n type RuntimeEnvironmentVariable,\n type StoryChapter,\n type StoryChapterAlignment,\n type StoryChapterAnimation,\n type StoryInsetPosition,\n type StoryLayerOpacityChange,\n type StoryMap,\n type StorySlideMode,\n type StyleLibraryEntry,\n type CommentAnchor,\n type CommentAuthor,\n type CommentReply,\n type ProjectComment,\n type ProjectInteraction,\n} from \"./types\";\nimport { DEFAULT_LAYER_GROUP_OPACITY, normalizeGroupContiguity } from \"./layer-groups\";\nimport { normalizeStyleLibraryEntries } from \"./style-library\";\nimport { normalizeLayerCapabilities } from \"./capabilities\";\nimport { validateMapExpression } from \"./expressions\";\nimport { normalizeLayerDescriptiveMetadata } from \"./layer-descriptive-metadata\";\nimport {\n createDefaultPrintLayout,\n isDefaultPrintLayout,\n normalizePrintLayoutConfig,\n scrubPrintLayoutForLayers,\n type PrintLayoutConfig,\n} from \"./print-layout-config\";\nimport { getEllipsoid } from \"./ellipsoids\";\nimport {\n scrubWidgetsForRemovedLayers,\n scrubCommentsForRemovedLayers,\n scrubLegendForRemovedLayers,\n} from \"./layer-ref-scrub\";\n\n/** Placeholder name a project carries before the user names it. */\nexport const DEFAULT_PROJECT_NAME = \"Untitled Project\";\n\nexport interface CreateProjectOptions {\n basemapStyleUrl?: string;\n mapView?: MapViewState;\n /** Celestial body the project describes; defaults to Earth when omitted. */\n ellipsoidId?: string;\n}\n\nexport function normalizeBlankBackgroundColor(value: unknown): string | null {\n return typeof value === \"string\" && /^#[0-9a-f]{6}$/i.test(value) ? value : null;\n}\n\nexport function createDefaultMapView(): MapViewState {\n return {\n center: [-100, 40],\n zoom: 2,\n bearing: 0,\n pitch: 0,\n };\n}\n\nexport function createEmptyProject(\n name = DEFAULT_PROJECT_NAME,\n options: CreateProjectOptions = {},\n): GeoLibreProject {\n return {\n version: PROJECT_VERSION,\n name,\n mapView: options.mapView ?? createDefaultMapView(),\n basemapStyleUrl: options.basemapStyleUrl ?? DEFAULT_BASEMAP,\n basemapVisible: true,\n basemapOpacity: 1,\n blankBackgroundColor: null,\n layers: [],\n layerGroups: [],\n styles: {},\n // A copy, never the shared constant: a caller that edits the new project's\n // preferences — `project.preferences.map.cesiumBasemap = …` — would\n // otherwise rewrite the app-wide defaults for the rest of the process, and\n // every later \"what is the default\" read would answer with its edit.\n preferences: {\n ...DEFAULT_PROJECT_PREFERENCES,\n map: {\n ...DEFAULT_PROJECT_PREFERENCES.map,\n ...(options.ellipsoidId ? { ellipsoidId: getEllipsoid(options.ellipsoidId).id } : {}),\n },\n },\n legend: { ...DEFAULT_LEGEND_CONFIG },\n comments: [],\n metadata: {},\n };\n}\n\n/**\n * GeoJSON `type` values that mark a subtree as feature data rather than project\n * structure. Everything under one of these is written compactly by\n * {@link serializeProject}.\n */\nconst GEOJSON_TYPES = new Set([\n \"FeatureCollection\",\n \"Feature\",\n \"GeometryCollection\",\n \"Point\",\n \"MultiPoint\",\n \"LineString\",\n \"MultiLineString\",\n \"Polygon\",\n \"MultiPolygon\",\n]);\n\n/** One level of indentation in a serialized project. */\nconst PROJECT_INDENT = \" \";\n\n/**\n * Whether a value is a GeoJSON feature, geometry, or collection.\n *\n * Decided from the `type` string alone, so this is an implicit contract on the\n * project schema: no field may store a non-GeoJSON object under a `type` of one\n * of the nine {@link GEOJSON_TYPES} names, or it would silently be written\n * compact as if it were feature data. Nothing in `types.ts` does today; a new\n * field that wants one of those names (a drawing-tool or geometry-filter config,\n * say) needs a different discriminator.\n */\nfunction isGeoJsonValue(value: object): boolean {\n const type = (value as { type?: unknown }).type;\n return typeof type === \"string\" && GEOJSON_TYPES.has(type);\n}\n\n/**\n * Serialize a value exactly as `JSON.stringify(value, null, 2)` would, except\n * that GeoJSON features, geometries and collections are written compactly on a\n * single line.\n *\n * Coordinate arrays are never hand-edited, but indenting them costs roughly\n * three bytes of whitespace for every one byte of data: a project embedding\n * 13,000 features weighed 179 MB pretty-printed and 54 MB compact\n * (GeoLibre#1829), which is the difference between reopening and running the\n * tab out of memory. The surrounding project structure stays indented so the\n * file is still readable and diffs still make sense.\n *\n * @param value Value to serialize.\n * @param depth Current nesting depth, driving the indent width.\n * @param key Property name (or stringified array index) this value sits under,\n * `\"\"` at the root — the argument `JSON.stringify` passes to `toJSON`.\n * @param ancestors Containers currently open on the recursion stack, used to\n * detect cycles.\n * @param presets Objects whose serialized text is already known at the depth\n * they sit at, written verbatim instead of being walked again. Used by\n * {@link serializeProjectWithLayerCache} to splice in cached layers.\n * @returns The serialized text, or undefined for values `JSON.stringify` also\n * drops (undefined, functions, symbols).\n */\nfunction serializeProjectValue(\n value: unknown,\n depth: number,\n key: string,\n ancestors: Set<object>,\n presets?: ReadonlyMap<object, string>,\n): string | undefined {\n if (presets !== undefined && value !== null && typeof value === \"object\") {\n const preset = presets.get(value);\n if (preset !== undefined) return preset;\n }\n if (value !== null && typeof value === \"object\") {\n // Honor the toJSON hook the way JSON.stringify does, so a value that\n // replaces itself is inspected in its replaced form. It receives the same\n // key JSON.stringify would pass, since a custom hook may branch on it.\n const toJSON = (value as { toJSON?: unknown }).toJSON;\n if (typeof toJSON === \"function\") {\n value = (toJSON as (key: string) => unknown).call(value, key);\n }\n }\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n // A boxed Number/String/Boolean writes as its primitive rather than as an\n // object, the way JSON.stringify unwraps it.\n if (value instanceof Number || value instanceof String || value instanceof Boolean) {\n return JSON.stringify(value);\n }\n // Feature data: hand the whole subtree to JSON.stringify with no spacing.\n if (isGeoJsonValue(value)) return JSON.stringify(value);\n\n // A cycle would recurse until the stack overflowed, and the RangeError that\n // raises is indistinguishable from the string-length cap the save path reads\n // as \"project too large\". Fail the way JSON.stringify does instead. Only the\n // open ancestors are tracked, so a value referenced twice side by side (not a\n // cycle) still serializes.\n if (ancestors.has(value)) throw new TypeError(\"Converting circular structure to JSON\");\n ancestors.add(value);\n try {\n const pad = PROJECT_INDENT.repeat(depth + 1);\n const closePad = PROJECT_INDENT.repeat(depth);\n if (Array.isArray(value)) {\n if (value.length === 0) return \"[]\";\n // Indexed rather than mapped so a sparse array's holes are visited: like\n // an unserializable entry, a hole becomes null, matching JSON.stringify.\n const entries = Array.from(\n { length: value.length },\n (_unused, index) =>\n serializeProjectValue(value[index], depth + 1, String(index), ancestors, presets) ??\n \"null\",\n );\n return `[\\n${pad}${entries.join(`,\\n${pad}`)}\\n${closePad}]`;\n }\n const entries: string[] = [];\n for (const [entryKey, entry] of Object.entries(value)) {\n const serialized = serializeProjectValue(entry, depth + 1, entryKey, ancestors, presets);\n // An unserializable object value is omitted, matching JSON.stringify.\n if (serialized === undefined) continue;\n entries.push(`${JSON.stringify(entryKey)}: ${serialized}`);\n }\n if (entries.length === 0) return \"{}\";\n return `{\\n${pad}${entries.join(`,\\n${pad}`)}\\n${closePad}}`;\n } finally {\n ancestors.delete(value);\n }\n}\n\n/**\n * Serialize a project to `.geolibre.json` text: indented project structure with\n * compact (unindented) embedded GeoJSON. See {@link serializeProjectValue} for\n * why the two halves are formatted differently.\n *\n * @param project Project to serialize.\n * @returns The file contents to write.\n */\nexport function serializeProject(project: GeoLibreProject): string {\n return (\n serializeProjectValue(\n { ...project, layers: project.layers.map(portableLayer) },\n 0,\n \"\",\n new Set(),\n ) ?? \"null\"\n );\n}\n\n/**\n * Per-layer serialized text kept across {@link serializeProjectWithLayerCache}\n * calls, keyed by the layer record each entry was built from. A `WeakMap`, so a\n * layer the store has replaced or removed drops its text with it.\n */\nexport type ProjectLayerSerializationCache = WeakMap<object, { index: number; text: string }>;\n\n/** Create an empty {@link ProjectLayerSerializationCache}. */\nexport function createProjectLayerSerializationCache(): ProjectLayerSerializationCache {\n return new WeakMap();\n}\n\n/**\n * Serialize a project exactly as {@link serializeProject} does, reusing the\n * text of every layer whose source record is unchanged since an earlier call.\n *\n * Feature data lives inside layer records, so re-stringifying every layer on\n * each autosave costs megabytes of main-thread work for a project with large\n * GeoJSON layers — even when only the camera moved (GeoLibre#2633). The store\n * replaces a layer object whenever it changes, so object identity is a sound\n * cache key: a camera move reuses every layer, and an edit to one layer\n * re-serializes only that one.\n *\n * `layerSources[i]` must be the record `project.layers[i]` was derived from\n * (e.g. the store's `layers`, before `projectFromStore` prepared them), and that\n * derivation must depend on the record alone — as `buildProjectSnapshot`'s does\n * — or a cached entry could outlive a change it should have reflected. When the\n * two arrays differ in length the cache is bypassed rather than trusted.\n *\n * @param project Project to serialize, as built from `layerSources`.\n * @param layerSources Immutable source record for each entry of\n * `project.layers`, in the same order, used as the cache key.\n * @param cache Cache shared between calls; see\n * {@link createProjectLayerSerializationCache}.\n * @returns The same text {@link serializeProject} returns for `project`.\n */\nexport function serializeProjectWithLayerCache(\n project: GeoLibreProject,\n layerSources: readonly object[],\n cache: ProjectLayerSerializationCache,\n): string {\n const steps = serializeProjectWithLayerCacheSteps(project, layerSources, cache);\n for (;;) {\n const step = steps.next();\n if (step.done) return step.value;\n }\n}\n\n/** Time a main-thread serialization slice may run before it yields (ms). */\nconst SERIALIZE_SLICE_MS = 8;\n\n/** Resolve on a macrotask, letting input and paint run in between slices. */\nfunction yieldToMainThread(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, 0));\n}\n\n/**\n * {@link serializeProjectWithLayerCache} that hands the main thread back\n * between layers, so autosave on a project with several large layers is many\n * short tasks instead of one 300-400 ms freeze (GeoLibre#2869).\n *\n * The cache makes a camera move free, so the cost lands on the first autosave\n * and on edits to a big layer: each layer that misses the cache is serialized\n * in its own slice, and the thread is released once a slice has run longer than\n * a few milliseconds. A single huge layer is still one slice. The result is\n * identical to the synchronous call. `project` and `layerSources` must not be\n * mutated while this runs; the store replaces records rather than editing them,\n * so a snapshot taken from it stays valid.\n *\n * @param project Project to serialize, as built from `layerSources`.\n * @param layerSources Immutable source record for each entry of\n * `project.layers`, in the same order, used as the cache key.\n * @param cache Cache shared between calls.\n * @param yieldFn Hands the thread back; replaced in tests.\n * @returns The same text {@link serializeProject} returns for `project`.\n */\nexport async function serializeProjectWithLayerCacheAsync(\n project: GeoLibreProject,\n layerSources: readonly object[],\n cache: ProjectLayerSerializationCache,\n yieldFn: () => Promise<void> = yieldToMainThread,\n): Promise<string> {\n const steps = serializeProjectWithLayerCacheSteps(project, layerSources, cache);\n let sliceStart = Date.now();\n for (;;) {\n const step = steps.next();\n if (step.done) return step.value;\n if (Date.now() - sliceStart >= SERIALIZE_SLICE_MS) {\n await yieldFn();\n sliceStart = Date.now();\n }\n }\n}\n\n/** Shared body of the sync and async serializers; yields once per layer text built. */\nfunction* serializeProjectWithLayerCacheSteps(\n project: GeoLibreProject,\n layerSources: readonly object[],\n cache: ProjectLayerSerializationCache,\n): Generator<void, string, void> {\n if (layerSources.length !== project.layers.length) return serializeProject(project);\n const layers = project.layers.map(portableLayer);\n // Presets are keyed by layer object, so a record listed twice would get one\n // index's text in both places. The store never does that; bypass if it does.\n if (new Set(layers).size !== layers.length) return serializeProject(project);\n const presets = new Map<object, string>();\n for (let index = 0; index < layers.length; index += 1) {\n const layer = layers[index];\n const source = layerSources[index];\n const cached = cache.get(source);\n // The layer is serialized under its array index as the key (a `toJSON`\n // hook would see it), so text is reused only at the index it was built for.\n let text = cached?.index === index ? cached.text : undefined;\n if (text === undefined) {\n // Depth 2: the root object is depth 0 and its `layers` array depth 1.\n text = serializeProjectValue(layer, 2, String(index), new Set()) ?? \"null\";\n cache.set(source, { index, text });\n yield;\n }\n presets.set(layer, text);\n }\n return serializeProjectValue({ ...project, layers }, 0, \"\", new Set(), presets) ?? \"null\";\n}\n\nexport function parseProject(json: string): GeoLibreProject {\n const data = JSON.parse(json) as Partial<GeoLibreProject>;\n if (!data.version || !data.name || !data.mapView) {\n throw new Error(\"Invalid GeoLibre project: missing required fields\");\n }\n const layerGroups = normalizeLayerGroups(data.layerGroups);\n const validGroupIds = new Set(layerGroups.map((g) => g.id));\n const layers = (data.layers ?? [])\n .map(normalizeLayer)\n .map((layer) =>\n layer.groupId && !validGroupIds.has(layer.groupId) ? { ...layer, groupId: undefined } : layer,\n );\n const selectedLayerId =\n data.selectedLayerId === null\n ? null\n : typeof data.selectedLayerId === \"string\" &&\n layers.some((layer) => layer.id === data.selectedLayerId)\n ? data.selectedLayerId\n : undefined;\n const basemapStyleUrl = data.basemapStyleUrl ?? DEFAULT_BASEMAP;\n const basemapVisible = data.basemapVisible ?? true;\n const basemapOpacity = data.basemapOpacity ?? 1;\n const blankBackgroundColor = normalizeBlankBackgroundColor(data.blankBackgroundColor);\n // Secondary panes already go through normalizeMapViewState; the primary\n // camera must too so a hand-edited project cannot store an out-of-range\n // view that MapLibre would silently clamp, leaving saved state wrong.\n const mapView = normalizeMapViewState(data.mapView);\n const { mapLayout, secondaryMapViews } = resolveMapGrid(\n normalizeMapLayout(data.mapLayout),\n normalizeSecondaryMapViews(data.secondaryMapViews),\n { mapView },\n );\n const styleLibrary = normalizeStyleLibraryEntries(data.styleLibrary);\n const parsedComments = normalizeProjectComments(data.comments);\n const parsedInteraction = normalizeProjectInteraction(data.interaction);\n return {\n version: data.version,\n name: data.name,\n mapView,\n basemapStyleUrl,\n basemapVisible,\n basemapOpacity,\n blankBackgroundColor,\n layers,\n ...(selectedLayerId !== undefined ? { selectedLayerId } : {}),\n ...(layerGroups.length > 0 ? { layerGroups } : {}),\n styles: data.styles ?? {},\n preferences: normalizeProjectPreferences(data.preferences),\n plugins: normalizeProjectPlugins(data.plugins) ?? undefined,\n legend: normalizeLegendConfig(data.legend),\n printLayout: normalizePrintLayoutConfig(data.printLayout) ?? undefined,\n storymap: normalizeStoryMap(data.storymap) ?? undefined,\n models: normalizeModels(data.models) ?? undefined,\n processingHistory: normalizeProcessingHistory(data.processingHistory) ?? undefined,\n widgets: normalizeWidgets(data.widgets) ?? undefined,\n ...(data.dashboardColumns === undefined\n ? {}\n : { dashboardColumns: normalizeDashboardColumns(data.dashboardColumns) }),\n // Only persist the grid when it is larger than a single pane, so default\n // single-map projects serialize byte-identically to before this feature.\n ...(mapLayout.rows * mapLayout.cols > 1\n ? {\n mapLayout,\n secondaryMapViews,\n ...(normalizeString(data.primaryMapLabel)\n ? { primaryMapLabel: normalizeString(data.primaryMapLabel) }\n : {}),\n }\n : {}),\n // The primary renderer is independent of the grid, so it sits outside the\n // `mapLayout` block above: a 1x1 Cesium project has no grid to persist.\n ...(normalizePrimaryRenderer(data.primaryRenderer)\n ? { primaryRenderer: normalizePrimaryRenderer(data.primaryRenderer)! }\n : {}),\n ...(styleLibrary.length > 0 ? { styleLibrary } : {}),\n ...(parsedComments.length > 0 ? { comments: parsedComments } : {}),\n ...(parsedInteraction ? { interaction: parsedInteraction } : {}),\n metadata: data.metadata ?? {},\n };\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) `layerGroups` array into valid\n * {@link LayerGroup} records, dropping entries without a usable id and\n * de-duplicating by id. Always returns an array (empty when absent).\n *\n * @param value Raw `layerGroups` value from the project JSON.\n * @returns Normalized, de-duplicated group definitions.\n */\nfunction normalizeLayerGroups(value: unknown): LayerGroup[] {\n if (!Array.isArray(value)) return [];\n const groups: LayerGroup[] = [];\n const seen = new Set<string>();\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<LayerGroup>;\n const id = typeof candidate.id === \"string\" ? candidate.id.trim() : \"\";\n if (!id || seen.has(id)) continue;\n seen.add(id);\n const opacity =\n typeof candidate.opacity === \"number\" && Number.isFinite(candidate.opacity)\n ? Math.min(Math.max(candidate.opacity, 0), 1)\n : DEFAULT_LAYER_GROUP_OPACITY;\n groups.push({\n id,\n name: typeof candidate.name === \"string\" ? candidate.name : id,\n ...(typeof candidate.parentId === \"string\" && candidate.parentId.trim()\n ? { parentId: candidate.parentId.trim() }\n : {}),\n collapsed: candidate.collapsed === true,\n visible: candidate.visible !== false,\n opacity,\n });\n }\n const ids = new Set(groups.map((group) => group.id));\n const byId = new Map(groups.map((group) => [group.id, group]));\n return groups.map((group) => {\n if (!group.parentId || !ids.has(group.parentId) || group.parentId === group.id) {\n return group.parentId ? { ...group, parentId: undefined } : group;\n }\n let id: string | undefined = group.parentId;\n const seen = new Set([group.id]);\n while (id) {\n if (seen.has(id)) return { ...group, parentId: undefined };\n seen.add(id);\n id = byId.get(id)?.parentId;\n }\n return group;\n });\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) legend config into a valid\n * {@link LegendConfig}, dropping malformed entries. Returns undefined when no\n * usable config is present so the default is applied downstream.\n */\nfunction normalizeLegendConfig(legend: unknown): LegendConfig | undefined {\n if (!legend || typeof legend !== \"object\") return undefined;\n const candidate = legend as Partial<LegendConfig>;\n\n const order = Array.isArray(candidate.order) ? uniqueStrings(candidate.order) : [];\n\n const overrides: Record<string, LegendItemOverride> = {};\n if (candidate.overrides && typeof candidate.overrides === \"object\") {\n for (const [key, value] of Object.entries(candidate.overrides)) {\n if (!key.trim() || !value || typeof value !== \"object\") continue;\n const override = value as Partial<LegendItemOverride>;\n const normalized: LegendItemOverride = {};\n // Mirror setLegendItemLabel / renderedLabel: a blank or whitespace-only\n // label is treated as \"no override\", so don't persist it.\n if (typeof override.label === \"string\" && override.label.trim() !== \"\") {\n normalized.label = override.label;\n }\n // Only the truthy hidden flag is meaningful; `hidden: false` is the\n // default, so dropping it keeps round-tripped projects from accumulating\n // no-op overrides (matches what the UI mutations store).\n if (override.hidden === true) normalized.hidden = true;\n if (normalized.label !== undefined || normalized.hidden !== undefined) {\n overrides[key.trim()] = normalized;\n }\n }\n }\n\n // Hand-authored entries: keep only well-formed items (string label + color);\n // an entry whose items all fail validation is dropped entirely so the panel\n // never renders an empty custom section from a hand-edited file.\n const customEntries: Record<string, LegendCustomEntry> = {};\n if (\n candidate.customEntries &&\n typeof candidate.customEntries === \"object\" &&\n !Array.isArray(candidate.customEntries)\n ) {\n for (const [key, value] of Object.entries(candidate.customEntries)) {\n if (!key.trim() || !value || typeof value !== \"object\" || Array.isArray(value)) continue;\n const entry = value as Partial<LegendCustomEntry>;\n if (!Array.isArray(entry.items)) continue;\n const items: LegendCustomItem[] = [];\n for (const item of entry.items) {\n if (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n const row = item as Partial<LegendCustomItem>;\n if (typeof row.label !== \"string\" || typeof row.color !== \"string\") continue;\n const shape =\n row.shape === \"circle\" || row.shape === \"line\" || row.shape === \"square\"\n ? row.shape\n : undefined;\n // Proportional symbol size in map pixels; bounded so a hand-edited file\n // cannot ask the panel for an absurd swatch.\n const size =\n typeof row.size === \"number\" && Number.isFinite(row.size) && row.size > 0\n ? Math.min(row.size, 1000)\n : undefined;\n items.push({\n label: row.label,\n color: row.color,\n ...(shape ? { shape } : {}),\n ...(size !== undefined ? { size } : {}),\n });\n }\n if (items.length === 0) continue;\n customEntries[key.trim()] = {\n ...(typeof entry.title === \"string\" && entry.title.trim() !== \"\"\n ? { title: entry.title }\n : {}),\n items,\n };\n }\n }\n\n const panelPosition =\n candidate.panelPosition === \"top-left\" ||\n candidate.panelPosition === \"top-right\" ||\n candidate.panelPosition === \"bottom-left\" ||\n candidate.panelPosition === \"bottom-right\"\n ? candidate.panelPosition\n : undefined;\n\n // Hand-resized panel dimensions: keep only sane finite values so a\n // hand-edited file can't collapse the panel or blow it past any viewport.\n const panelSize = (value: unknown): number | undefined =>\n typeof value === \"number\" && Number.isFinite(value) && value >= 120 && value <= 4000\n ? Math.round(value)\n : undefined;\n const panelWidth = panelSize(candidate.panelWidth);\n const panelHeight = panelSize(candidate.panelHeight);\n\n return {\n title: typeof candidate.title === \"string\" ? candidate.title : DEFAULT_LEGEND_CONFIG.title,\n groupByLayer: normalizeBoolean(candidate.groupByLayer, DEFAULT_LEGEND_CONFIG.groupByLayer),\n order,\n overrides,\n ...(Object.keys(customEntries).length > 0 ? { customEntries } : {}),\n ...(candidate.panelVisible === true ? { panelVisible: true } : {}),\n ...(candidate.panelCollapsed === true ? { panelCollapsed: true } : {}),\n ...(panelPosition ? { panelPosition } : {}),\n ...(panelWidth !== undefined ? { panelWidth } : {}),\n ...(panelHeight !== undefined ? { panelHeight } : {}),\n };\n}\n\n/**\n * Validate and coerce a story map loaded from an untrusted project file.\n *\n * Returns null when the value carries no chapters so empty story maps stay out\n * of the saved project, mirroring how plugins are only persisted when present.\n *\n * @param storymap Raw value read from the project JSON.\n * @returns A normalized story map, or null when there is nothing to keep.\n */\nexport function normalizeStoryMap(storymap: unknown): StoryMap | null {\n if (!storymap || typeof storymap !== \"object\") return null;\n\n const candidate = storymap as Partial<StoryMap>;\n // Drop duplicate chapter ids so updates/removals stay unambiguous and keyed\n // rendering stays stable.\n const seenChapterIds = new Set<string>();\n const chapters = Array.isArray(candidate.chapters)\n ? candidate.chapters.map(normalizeStoryChapter).filter((chapter): chapter is StoryChapter => {\n if (!chapter || seenChapterIds.has(chapter.id)) return false;\n seenChapterIds.add(chapter.id);\n return true;\n })\n : [];\n\n const normalized: StoryMap = {\n title: normalizeString(candidate.title),\n subtitle: normalizeString(candidate.subtitle),\n byline: normalizeString(candidate.byline),\n footer: normalizeString(candidate.footer),\n theme: candidate.theme === \"light\" ? \"light\" : \"dark\",\n showMarkers: normalizeBoolean(candidate.showMarkers, false),\n markerColor: normalizeString(candidate.markerColor) || DEFAULT_STORY_MAP.markerColor,\n inset: normalizeBoolean(candidate.inset, false),\n insetPosition: STORY_INSET_POSITIONS.has(candidate.insetPosition as StoryInsetPosition)\n ? (candidate.insetPosition as StoryInsetPosition)\n : DEFAULT_STORY_MAP.insetPosition,\n hideChapterNav: normalizeBoolean(candidate.hideChapterNav, false),\n startSlide: STORY_SLIDE_MODES.has(candidate.startSlide as StorySlideMode)\n ? (candidate.startSlide as StorySlideMode)\n : DEFAULT_STORY_MAP.startSlide,\n endSlide: STORY_SLIDE_MODES.has(candidate.endSlide as StorySlideMode)\n ? (candidate.endSlide as StorySlideMode)\n : DEFAULT_STORY_MAP.endSlide,\n chapters,\n };\n\n // Keep the story if it has chapters or any author-entered settings; only a\n // wholly-default, chapter-less story is dropped (so blank stories stay out of\n // saved projects without discarding settings entered before the first chapter).\n return storyMapHasContent(normalized) ? normalized : null;\n}\n\n/** Whether a story map carries chapters or any non-default setting. */\nexport function storyMapHasContent(story: StoryMap): boolean {\n if (story.chapters.length > 0) return true;\n return (\n story.title.trim() !== \"\" ||\n story.subtitle.trim() !== \"\" ||\n story.byline.trim() !== \"\" ||\n story.footer.trim() !== \"\" ||\n story.theme !== DEFAULT_STORY_MAP.theme ||\n story.showMarkers !== DEFAULT_STORY_MAP.showMarkers ||\n story.markerColor !== DEFAULT_STORY_MAP.markerColor ||\n story.inset !== DEFAULT_STORY_MAP.inset ||\n story.insetPosition !== DEFAULT_STORY_MAP.insetPosition ||\n story.hideChapterNav !== DEFAULT_STORY_MAP.hideChapterNav ||\n story.startSlide !== DEFAULT_STORY_MAP.startSlide ||\n story.endSlide !== DEFAULT_STORY_MAP.endSlide\n );\n}\n\nconst STORY_ALIGNMENTS = new Set<StoryChapterAlignment>([\"left\", \"center\", \"right\", \"full\"]);\n\nconst STORY_ANIMATIONS = new Set<StoryChapterAnimation>([\"flyTo\", \"easeTo\", \"jumpTo\"]);\n\nconst STORY_INSET_POSITIONS = new Set<StoryInsetPosition>([\n \"top-left\",\n \"top-right\",\n \"bottom-left\",\n \"bottom-right\",\n]);\n\nconst STORY_SLIDE_MODES = new Set<StorySlideMode>([\"none\", \"blank\", \"black\", \"global\", \"adjacent\"]);\n\nfunction normalizeStoryChapter(chapter: unknown): StoryChapter | null {\n if (!chapter || typeof chapter !== \"object\") return null;\n\n const candidate = chapter as Partial<StoryChapter>;\n const id = normalizeString(candidate.id);\n if (!id) return null;\n\n const location = candidate.location;\n const center = location?.center;\n if (\n !Array.isArray(center) ||\n center.length !== 2 ||\n !center.every((value) => Number.isFinite(value))\n ) {\n return null;\n }\n\n return {\n id,\n title: normalizeString(candidate.title),\n description: normalizeString(candidate.description),\n image: normalizeString(candidate.image) || undefined,\n alignment: STORY_ALIGNMENTS.has(candidate.alignment as StoryChapterAlignment)\n ? (candidate.alignment as StoryChapterAlignment)\n : \"left\",\n hidden: normalizeBoolean(candidate.hidden, false),\n location: {\n // Clamp to valid lng/lat so a hand-edited file can't make flyTo throw.\n center: [\n clampCoordinate(Number(center[0]), -180, 180),\n clampCoordinate(Number(center[1]), -90, 90),\n ],\n // Clamp to MapLibre's valid ranges so a stored value matches the camera\n // that actually lands (bearing wraps to 0-360).\n zoom: clamp(normalizeNumber(location?.zoom, 2), 0, 24),\n pitch: clamp(normalizeNumber(location?.pitch, 0), 0, 85),\n bearing: ((normalizeNumber(location?.bearing, 0) % 360) + 360) % 360,\n },\n mapAnimation: STORY_ANIMATIONS.has(candidate.mapAnimation as StoryChapterAnimation)\n ? (candidate.mapAnimation as StoryChapterAnimation)\n : \"flyTo\",\n rotateAnimation: normalizeBoolean(candidate.rotateAnimation, false),\n onChapterEnter: normalizeOpacityChanges(candidate.onChapterEnter),\n onChapterExit: normalizeOpacityChanges(candidate.onChapterExit),\n };\n}\n\nfunction normalizeOpacityChanges(value: unknown): StoryLayerOpacityChange[] {\n if (!Array.isArray(value)) return [];\n return value\n .map((entry): StoryLayerOpacityChange | null => {\n if (!entry || typeof entry !== \"object\") return null;\n const candidate = entry as Partial<StoryLayerOpacityChange>;\n const layerId = normalizeString(candidate.layerId);\n if (!layerId) return null;\n const id = normalizeString(candidate.id);\n return {\n ...(id ? { id } : {}),\n layerId,\n opacity: clamp(normalizeNumber(candidate.opacity, 1), 0, 1),\n ...(Number.isFinite(candidate.duration)\n ? { duration: Math.max(0, Number(candidate.duration)) }\n : {}),\n };\n })\n .filter((entry): entry is StoryLayerOpacityChange => Boolean(entry));\n}\n\nfunction normalizeString(value: unknown): string {\n return typeof value === \"string\" ? value : \"\";\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) `models` array into valid\n * {@link ProcessingModel} records. Drops models and steps without a usable id or\n * tool id, de-duplicates models by id, and keeps step `parameters` as a plain\n * object (the runner validates parameter values per tool at run time). Returns\n * `null` when there is nothing worth persisting, so a model-less project stays\n * free of the key.\n *\n * @param value Raw `models` value from the project JSON.\n * @returns Normalized models, or `null` when none survive.\n */\nexport function normalizeModels(value: unknown): ProcessingModel[] | null {\n if (!Array.isArray(value)) return null;\n const models: ProcessingModel[] = [];\n const seen = new Set<string>();\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<ProcessingModel>;\n const id = normalizeString(candidate.id).trim();\n if (!id || seen.has(id)) continue;\n const steps: ProcessingModelStep[] = [];\n const rawSteps = Array.isArray(candidate.steps) ? candidate.steps : [];\n const seenStepIds = new Set<string>();\n for (const rawStep of rawSteps) {\n if (!rawStep || typeof rawStep !== \"object\") continue;\n const step = rawStep as Partial<ProcessingModelStep>;\n const stepId = normalizeString(step.id).trim();\n const toolId = normalizeString(step.toolId).trim();\n if (!stepId || !toolId || seenStepIds.has(stepId)) continue;\n seenStepIds.add(stepId);\n const inputParam = normalizeString(step.inputParam).trim();\n steps.push({\n id: stepId,\n toolId,\n parameters:\n step.parameters && typeof step.parameters === \"object\"\n ? (step.parameters as Record<string, unknown>)\n : {},\n ...(inputParam ? { inputParam } : {}),\n });\n }\n seen.add(id);\n const graph = normalizeModelGraph((candidate as { graph?: unknown }).graph);\n models.push({\n id,\n name: normalizeString(candidate.name),\n steps,\n ...(graph ? { graph } : {}),\n });\n }\n return models.length > 0 ? models : null;\n}\n\nconst MODEL_NODE_KINDS = new Set<ModelGraphNodeKind>([\"input\", \"tool\", \"output\"]);\nconst MODEL_TOOL_PROVIDERS = new Set<ModelToolProvider>([\"vector\", \"whitebox\"]);\n\n/** Coerce an untrusted number to a finite canvas coordinate. */\nfunction normalizeCoordinate(value: unknown): number {\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n}\n\n/**\n * Coerce an untrusted `graph` value into a {@link ProcessingModelGraph}. Drops\n * nodes without a usable id or an unknown kind, de-duplicates node ids, and\n * drops edges that do not connect two surviving nodes or that name an empty\n * port. Self-edges are dropped too, since a node cannot feed itself.\n *\n * Structural validity beyond this (cycles, type mismatches, missing required\n * inputs) is the runner's job — those depend on the tool registries, which the\n * project layer deliberately does not import.\n *\n * @param value Raw `graph` value from the project JSON.\n * @returns The normalized graph, or `null` when it has no usable nodes.\n */\nexport function normalizeModelGraph(value: unknown): ProcessingModelGraph | null {\n if (!value || typeof value !== \"object\") return null;\n const raw = value as Partial<ProcessingModelGraph>;\n const nodes: ModelGraphNode[] = [];\n const nodeIds = new Set<string>();\n for (const entry of Array.isArray(raw.nodes) ? raw.nodes : []) {\n if (!entry || typeof entry !== \"object\") continue;\n const node = entry as Partial<ModelGraphNode>;\n const nodeId = normalizeString(node.id).trim();\n const kind = node.kind as ModelGraphNodeKind;\n if (!nodeId || nodeIds.has(nodeId) || !MODEL_NODE_KINDS.has(kind)) continue;\n nodeIds.add(nodeId);\n const layerId = normalizeString(node.layerId).trim();\n const toolId = normalizeString(node.toolId).trim();\n const name = normalizeString(node.name).trim();\n const provider = node.provider as ModelToolProvider;\n nodes.push({\n id: nodeId,\n kind,\n x: normalizeCoordinate(node.x),\n y: normalizeCoordinate(node.y),\n ...(layerId ? { layerId } : {}),\n ...(toolId ? { toolId } : {}),\n ...(MODEL_TOOL_PROVIDERS.has(provider) ? { provider } : {}),\n ...(node.parameters && typeof node.parameters === \"object\" && !Array.isArray(node.parameters)\n ? { parameters: node.parameters as Record<string, unknown> }\n : {}),\n ...(name ? { name } : {}),\n });\n }\n if (nodes.length === 0) return null;\n\n const edges: ModelGraphEdge[] = [];\n const edgeIds = new Set<string>();\n for (const entry of Array.isArray(raw.edges) ? raw.edges : []) {\n if (!entry || typeof entry !== \"object\") continue;\n const edge = entry as Partial<ModelGraphEdge>;\n const edgeId = normalizeString(edge.id).trim();\n const from = normalizeString(edge.from).trim();\n const to = normalizeString(edge.to).trim();\n const fromPort = normalizeString(edge.fromPort).trim();\n const toPort = normalizeString(edge.toPort).trim();\n if (!edgeId || edgeIds.has(edgeId)) continue;\n if (!nodeIds.has(from) || !nodeIds.has(to) || from === to) continue;\n if (!fromPort || !toPort) continue;\n edgeIds.add(edgeId);\n edges.push({ id: edgeId, from, fromPort, to, toPort });\n }\n return { nodes, edges };\n}\n\nconst PROCESSING_RUN_KINDS = new Set<ProcessingRunKind>([\n \"vector\",\n \"statistics\",\n \"network\",\n \"whitebox\",\n \"raster\",\n \"conversion\",\n \"algorithm\",\n]);\n\n/**\n * Old H3 vector-tool IDs from projects saved before the DGGS rename.\n * Mapped onto current tool ids during project load.\n */\nconst LEGACY_H3_PROCESSING_TOOL_IDS: Readonly<Record<string, string>> = {\n \"h3-grid\": \"dggs-grid\",\n \"h3-bin-points\": \"dggs-bin\",\n};\n\n/**\n * Coerce an untrusted (possibly hand-edited) `processingHistory` array into\n * valid {@link ProcessingRun} records. Drops entries without a usable id, tool\n * id, or known kind, de-duplicates by id, keeps `parameters` as a plain object,\n * and caps the list at {@link MAX_PROCESSING_HISTORY} (keeping the newest,\n * i.e. last, entries). Returns `null` when nothing survives, so a history-less\n * project stays free of the key.\n *\n * @param value Raw `processingHistory` value from the project JSON.\n * @returns Normalized runs, or `null` when none survive.\n */\nexport function normalizeProcessingHistory(value: unknown): ProcessingRun[] | null {\n if (!Array.isArray(value)) return null;\n // Bound the work for a crafted or corrupted file (shared/collaboration\n // projects reach this path too): only the newest entries can survive the\n // cap anyway, so ignore all but a generous tail up front.\n const source =\n value.length > MAX_PROCESSING_HISTORY * 10 ? value.slice(-MAX_PROCESSING_HISTORY * 10) : value;\n const runs: ProcessingRun[] = [];\n const seen = new Set<string>();\n for (const entry of source) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<ProcessingRun>;\n const id = normalizeString(candidate.id).trim();\n let toolId = normalizeString(candidate.toolId).trim();\n const kind = candidate.kind;\n if (!id || !toolId || seen.has(id)) continue;\n if (!kind || !PROCESSING_RUN_KINDS.has(kind)) continue;\n seen.add(id);\n const inputLayerNames =\n candidate.inputLayerNames && typeof candidate.inputLayerNames === \"object\"\n ? Object.fromEntries(\n Object.entries(candidate.inputLayerNames).filter(\n ([, name]) => typeof name === \"string\",\n ),\n )\n : undefined;\n const outputLayerNames = Array.isArray(candidate.outputLayerNames)\n ? candidate.outputLayerNames.filter((name): name is string => typeof name === \"string\")\n : undefined;\n let parameters: Record<string, unknown> =\n candidate.parameters && typeof candidate.parameters === \"object\"\n ? { ...(candidate.parameters as Record<string, unknown>) }\n : {};\n const migrated = LEGACY_H3_PROCESSING_TOOL_IDS[toolId];\n if (migrated) {\n toolId = migrated;\n if (parameters.dggsType == null) parameters = { ...parameters, dggsType: \"h3\" };\n }\n runs.push({\n id,\n kind,\n toolId,\n toolName: normalizeString(candidate.toolName) || toolId,\n engine: normalizeString(candidate.engine),\n parameters,\n ...(inputLayerNames && Object.keys(inputLayerNames).length > 0 ? { inputLayerNames } : {}),\n ...(outputLayerNames?.length ? { outputLayerNames } : {}),\n ...(normalizeString(candidate.inputPath)\n ? { inputPath: normalizeString(candidate.inputPath) }\n : {}),\n ...(normalizeString(candidate.outputPath)\n ? { outputPath: normalizeString(candidate.outputPath) }\n : {}),\n startedAt: normalizeString(candidate.startedAt),\n ...(Number.isFinite(candidate.durationMs)\n ? { durationMs: Math.max(0, Number(candidate.durationMs)) }\n : {}),\n // Only an explicit \"success\" earns the green checkmark; a missing or\n // corrupted status from hand-edited JSON degrades to \"error\" rather than\n // presenting an indeterminate run as having succeeded.\n status: candidate.status === \"success\" ? \"success\" : \"error\",\n ...(normalizeString(candidate.error) ? { error: normalizeString(candidate.error) } : {}),\n });\n }\n if (runs.length === 0) return null;\n return runs.slice(-MAX_PROCESSING_HISTORY);\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) camera object into a valid\n * {@link MapViewState}, falling back to the default view for missing parts.\n */\nexport function normalizeMapViewState(value: unknown): MapViewState {\n const fallback = createDefaultMapView();\n if (!value || typeof value !== \"object\") return fallback;\n const candidate = value as Partial<MapViewState>;\n const center = Array.isArray(candidate.center) ? candidate.center : fallback.center;\n // Clamp to MapLibre's valid ranges (matching normalizeStoryChapter) so a\n // hand-edited project file can't store an out-of-range camera that jumpTo\n // would silently clamp or reject, leaving the saved state inconsistent with\n // what lands on screen. Bearing wraps into [0, 360).\n const view: MapViewState = {\n center: [\n clampCoordinate(normalizeNumber(center[0], fallback.center[0]), -180, 180),\n clampCoordinate(normalizeNumber(center[1], fallback.center[1]), -90, 90),\n ],\n zoom: clamp(normalizeNumber(candidate.zoom, fallback.zoom), 0, 24),\n bearing: ((normalizeNumber(candidate.bearing, fallback.bearing) % 360) + 360) % 360,\n pitch: clamp(normalizeNumber(candidate.pitch, fallback.pitch), 0, 85),\n };\n if (\n Array.isArray(candidate.bbox) &&\n candidate.bbox.length === 4 &&\n candidate.bbox.every((n) => Number.isFinite(n))\n ) {\n view.bbox = [\n Number(candidate.bbox[0]),\n Number(candidate.bbox[1]),\n Number(candidate.bbox[2]),\n Number(candidate.bbox[3]),\n ];\n }\n return view;\n}\n\n/**\n * Coerce an untrusted `mapLayout` into a valid {@link MapGridLayout}. Returns\n * null when absent or effectively single-pane so default projects stay\n * byte-identical (the field is only written when the grid is larger than 1x1).\n */\nexport function normalizeMapLayout(value: unknown): MapGridLayout | null {\n if (!value || typeof value !== \"object\") return null;\n const candidate = value as Partial<MapGridLayout>;\n const rows = clamp(Math.floor(normalizeNumber(candidate.rows, 1)), 1, MAX_MAP_GRID_DIM);\n const cols = clamp(Math.floor(normalizeNumber(candidate.cols, 1)), 1, MAX_MAP_GRID_DIM);\n if (rows * cols <= 1) return null;\n return {\n rows,\n cols,\n syncView: normalizeBoolean(candidate.syncView, DEFAULT_MAP_GRID_LAYOUT.syncView),\n };\n}\n\n/**\n * Coerce an untrusted `primaryRenderer` into a known engine id (issue #2217).\n *\n * Returns null for the default 2D map — absent, unknown, or an explicit\n * `\"maplibre\"` — because the field is only written when it is not the default,\n * so a MapLibre project serializes byte-identically to before this existed.\n * The return type is narrowed to `\"cesium\" | \"mapbox\" | \"arcgis\" | null` rather\n * than the full {@link MapRendererKind} for that reason: `\"maplibre\"` is never a\n * result.\n */\nexport function normalizePrimaryRenderer(value: unknown): \"cesium\" | \"mapbox\" | \"arcgis\" | null {\n return value === \"cesium\" || value === \"mapbox\" || value === \"arcgis\" ? value : null;\n}\n\n/**\n * Coerce an untrusted `secondaryMapViews` array into valid\n * {@link SecondaryMapView} records, dropping entries without a usable id and\n * de-duplicating by id. Returns null when none are valid.\n */\nexport function normalizeSecondaryMapViews(value: unknown): SecondaryMapView[] | null {\n if (!Array.isArray(value)) return null;\n const views: SecondaryMapView[] = [];\n const seen = new Set<string>();\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<SecondaryMapView>;\n const id = normalizeString(candidate.id).trim();\n if (!id || seen.has(id)) continue;\n seen.add(id);\n const label = normalizeString(candidate.label);\n // Only the known engine ids survive; an absent/unknown value is omitted so\n // the pane defaults to the 2D map (back-compat with pre-globe projects).\n /* eslint-disable local/no-renderer-kind-checks -- validates a renderer name */\n const viewKind =\n candidate.viewKind === \"cesium\" ||\n candidate.viewKind === \"maplibre\" ||\n candidate.viewKind === \"mapbox\" ||\n candidate.viewKind === \"arcgis\"\n ? candidate.viewKind\n : undefined;\n /* eslint-enable local/no-renderer-kind-checks */\n views.push({\n id,\n view: normalizeMapViewState(candidate.view),\n ...(label ? { label } : {}),\n ...(viewKind ? { viewKind } : {}),\n layerVisibility: normalizeLayerVisibility(candidate.layerVisibility),\n });\n }\n return views.length > 0 ? views : null;\n}\n\n/** Coerce an untrusted per-layer visibility map into `Record<string, boolean>`. */\nfunction normalizeLayerVisibility(value: unknown): Record<string, boolean> {\n if (!value || typeof value !== \"object\") return {};\n const result: Record<string, boolean> = {};\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n if (typeof raw === \"boolean\") result[key] = raw;\n }\n return result;\n}\n\n/**\n * Reconcile a parsed grid layout with its secondary panes so the store invariant\n * holds: `secondaryMapViews.length === rows * cols - 1`. Surplus panes are\n * dropped; missing panes are filled by cloning the primary map. A null/absent\n * layout (or a 1x1 grid) collapses to the single-map default.\n */\nexport function resolveMapGrid(\n layout: MapGridLayout | null,\n secondaryViews: SecondaryMapView[] | null,\n primary: { mapView: MapViewState },\n): { mapLayout: MapGridLayout; secondaryMapViews: SecondaryMapView[] } {\n if (!layout) {\n return { mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT }, secondaryMapViews: [] };\n }\n const desired = layout.rows * layout.cols - 1;\n let views = secondaryViews ?? [];\n if (views.length > desired) {\n views = views.slice(0, desired);\n } else if (views.length < desired) {\n const seen = new Set(views.map((v) => v.id));\n const additions: SecondaryMapView[] = [];\n for (let i = views.length; i < desired; i++) {\n let id = `secondary-${i}`;\n // Append a counter (rather than growing the string) so a crafted file\n // with colliding ids resolves in O(1) per attempt instead of O(n).\n let suffix = 0;\n while (seen.has(id)) id = `secondary-${i}-${++suffix}`;\n seen.add(id);\n additions.push({\n id,\n view: { ...primary.mapView },\n layerVisibility: {},\n });\n }\n views = [...views, ...additions];\n }\n return { mapLayout: layout, secondaryMapViews: views };\n}\n\n/** A 3- or 6-digit hex color, the only widget color format we persist. */\nconst HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n\n/** Upper bound for a persisted histogram bin count, mirroring the chart\n * renderer's clamp (`MAX_HISTOGRAM_BINS` in the desktop app's chart helpers). */\nconst MAX_PERSISTED_BINS = 50;\n\n/** Upper bound for a persisted list-widget row limit, mirroring the widget\n * editor's row-count input (`max={500}` in `WidgetEditorDialog`). */\nconst MAX_PERSISTED_LIST_ROWS = 500;\n\n// Spelled as a Record so adding a member to DashboardWidgetType fails to\n// compile until it is listed here. A plain array accepted a short list\n// silently, and a type missing from it makes normalizeWidgets drop every widget\n// of that type — which is how selector widgets vanished on save and reload.\nconst DASHBOARD_WIDGET_TYPES = Object.keys({\n histogram: true,\n scatter: true,\n bar: true,\n line: true,\n box: true,\n pie: true,\n indicator: true,\n selector: true,\n list: true,\n} satisfies Record<DashboardWidgetType, true>) as readonly DashboardWidgetType[];\nconst DASHBOARD_WIDGET_AGGREGATIONS: readonly DashboardWidgetAggregation[] = [\n \"count\",\n \"sum\",\n \"mean\",\n];\nconst INDICATOR_AGGREGATIONS: readonly IndicatorAggregation[] = [\n \"count\",\n \"sum\",\n \"mean\",\n \"min\",\n \"max\",\n \"median\",\n];\n\n/**\n * Coerce an untrusted (possibly hand-edited) `widgets` array into valid\n * {@link DashboardWidget} records. Drops widgets without a usable id, layer id,\n * or recognized chart type, de-duplicates by id, and keeps only the optional\n * keys that are present and well-typed (the Dashboard panel falls back to\n * sensible defaults for anything missing). Returns `null` when there is nothing\n * worth persisting, so a widget-less project stays free of the key.\n *\n * @param value Raw `widgets` value from the project JSON.\n * @returns Normalized widgets, or `null` when none survive.\n */\nexport function normalizeWidgets(value: unknown): DashboardWidget[] | null {\n if (!Array.isArray(value)) return null;\n const widgets: DashboardWidget[] = [];\n const seen = new Set<string>();\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<DashboardWidget>;\n const id = normalizeString(candidate.id).trim();\n const layerId = normalizeString(candidate.layerId).trim();\n if (!id || !layerId || seen.has(id)) continue;\n const type = candidate.type;\n if (!type || !DASHBOARD_WIDGET_TYPES.includes(type)) continue;\n seen.add(id);\n const widget: DashboardWidget = { id, layerId, type };\n const title = normalizeString(candidate.title).trim();\n if (title) widget.title = title;\n const color = normalizeString(candidate.color).trim();\n if (HEX_COLOR.test(color)) widget.color = color;\n const field = normalizeString(candidate.field).trim();\n if (field) widget.field = field;\n const xField = normalizeString(candidate.xField).trim();\n if (xField) widget.xField = xField;\n const yField = normalizeString(candidate.yField).trim();\n if (yField) widget.yField = yField;\n if (typeof candidate.bins === \"number\" && Number.isFinite(candidate.bins)) {\n // Persist only a sane positive bin count; the histogram renderer clamps to\n // [1, 50], so mirror that here rather than round-tripping 0 or huge values.\n const bins = Math.trunc(candidate.bins);\n if (bins >= 1) widget.bins = Math.min(MAX_PERSISTED_BINS, bins);\n }\n const category = normalizeString(candidate.category).trim();\n if (category) widget.category = category;\n if (\n candidate.aggregation &&\n DASHBOARD_WIDGET_AGGREGATIONS.includes(candidate.aggregation) &&\n // A pie has no \"average\"; the renderer would silently treat mean as sum,\n // so drop it here and let the default (count) stand for hand-edited files.\n !(type === \"pie\" && candidate.aggregation === \"mean\")\n ) {\n widget.aggregation = candidate.aggregation;\n }\n const valueField = normalizeString(candidate.valueField).trim();\n if (valueField) widget.valueField = valueField;\n // Indicator widget fields (issue #1381). Only an indicator reads them, so\n // drop them elsewhere rather than round-tripping dead configuration.\n if (type === \"indicator\") {\n if (\n candidate.indicatorAggregation &&\n INDICATOR_AGGREGATIONS.includes(candidate.indicatorAggregation)\n ) {\n widget.indicatorAggregation = candidate.indicatorAggregation;\n }\n // Prefix/suffix are not trimmed: a leading/trailing space is intentional\n // (e.g. \" ha\" or \"$ \").\n const prefix = normalizeString(candidate.prefix);\n if (prefix) widget.prefix = prefix;\n const suffix = normalizeString(candidate.suffix);\n if (suffix) widget.suffix = suffix;\n }\n // Selector widget fields (issue #1381). Only a selector reads the flag, and\n // false is the default, so persist it only when it is on.\n if (type === \"selector\" && candidate.multiple === true) {\n widget.multiple = true;\n }\n // List widget fields (issue #1381). normalizeWidgets also runs on the save\n // path (projectFromStore), so dropping these would blank a list widget the\n // moment its project is saved — the renderer falls back to \"no data\"\n // without listFields.\n if (type === \"list\") {\n if (Array.isArray(candidate.listFields)) {\n const listFields = candidate.listFields\n .map((entry) => normalizeString(entry).trim())\n .filter((entry) => entry !== \"\");\n if (listFields.length > 0) widget.listFields = listFields;\n }\n const sortBy = normalizeString(candidate.sortBy).trim();\n if (sortBy) widget.sortBy = sortBy;\n if (candidate.sortDir === \"asc\" || candidate.sortDir === \"desc\") {\n widget.sortDir = candidate.sortDir;\n }\n if (typeof candidate.limit === \"number\" && Number.isFinite(candidate.limit)) {\n // Clamp to the editor's range so a hand-edited 0 or 10_000 cannot reach\n // the renderer.\n const limit = Math.trunc(candidate.limit);\n if (limit >= 1) widget.limit = Math.min(MAX_PERSISTED_LIST_ROWS, limit);\n }\n }\n widgets.push(widget);\n }\n return widgets.length > 0 ? widgets : null;\n}\n\n/**\n * Clamp an untrusted dashboard column count into the supported range, falling\n * back to the default for a missing or non-finite value.\n *\n * @param value Raw `dashboardColumns` value from the project JSON.\n * @returns An integer column count within [MIN, MAX].\n */\nexport function normalizeDashboardColumns(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n return DEFAULT_DASHBOARD_COLUMNS;\n }\n return Math.max(MIN_DASHBOARD_COLUMNS, Math.min(MAX_DASHBOARD_COLUMNS, Math.trunc(value)));\n}\n\n/**\n * Coerce an untrusted `interaction` block into a {@link ProjectInteraction}.\n *\n * Layer ids are kept even when no such layer exists: the block is replayed\n * against whatever layers the project has when it opens, and the loader drops\n * the ones it cannot find. Control names are kept as written for the same\n * reason; the app ignores names it does not know.\n *\n * @param value - Raw `interaction` value from the project JSON or the store.\n * @returns The normalized block, or null when it carries nothing to apply.\n */\nexport function normalizeProjectInteraction(value: unknown): ProjectInteraction | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const raw = value as { identify?: unknown; controls?: unknown };\n const result: ProjectInteraction = {};\n if (raw.identify === null) {\n result.identify = null;\n } else if (typeof raw.identify === \"string\" && raw.identify) {\n result.identify = raw.identify;\n } else if (Array.isArray(raw.identify)) {\n const ids = [\n ...new Set(raw.identify.filter((id): id is string => typeof id === \"string\" && !!id)),\n ];\n if (ids.length > 0) result.identify = ids;\n }\n if (raw.controls && typeof raw.controls === \"object\" && !Array.isArray(raw.controls)) {\n const controls: Record<string, boolean> = {};\n for (const [name, shown] of Object.entries(raw.controls)) {\n if (name && typeof shown === \"boolean\") controls[name] = shown;\n }\n if (Object.keys(controls).length > 0) result.controls = controls;\n }\n return \"identify\" in result || result.controls ? result : null;\n}\n\nfunction normalizeProjectPreferences(preferences: unknown): ProjectPreferences {\n if (!preferences || typeof preferences !== \"object\") {\n return DEFAULT_PROJECT_PREFERENCES;\n }\n\n const candidate = preferences as Partial<ProjectPreferences>;\n const map = candidate.map ?? {};\n // Every MapPreferences field is normalized explicitly below, so the map\n // object is not spread in: that would forward unknown keys from a\n // hand-edited project file straight into app state.\n return {\n map: {\n ...DEFAULT_PROJECT_PREFERENCES.map,\n bounds: normalizeBounds((map as Partial<ProjectPreferences[\"map\"]>).bounds),\n minZoom: normalizeNumber(\n (map as Partial<ProjectPreferences[\"map\"]>).minZoom,\n DEFAULT_PROJECT_PREFERENCES.map.minZoom,\n ),\n maxZoom: normalizeNumber(\n (map as Partial<ProjectPreferences[\"map\"]>).maxZoom,\n DEFAULT_PROJECT_PREFERENCES.map.maxZoom,\n ),\n maxPitch: normalizeNumber(\n (map as Partial<ProjectPreferences[\"map\"]>).maxPitch,\n DEFAULT_PROJECT_PREFERENCES.map.maxPitch,\n ),\n restrictBounds: Boolean((map as Partial<ProjectPreferences[\"map\"]>).restrictBounds),\n renderWorldCopies: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).renderWorldCopies,\n true,\n ),\n projection:\n (map as Partial<ProjectPreferences[\"map\"]>).projection === \"mercator\"\n ? \"mercator\"\n : \"globe\",\n // Coerce unknown/missing bodies to Earth so measurements never break.\n ellipsoidId: getEllipsoid((map as Partial<ProjectPreferences[\"map\"]>).ellipsoidId).id,\n scaleUnit: normalizeScaleUnit((map as Partial<ProjectPreferences[\"map\"]>).scaleUnit),\n // Absent in every project written before #1813, and the default is off,\n // so an older project opens without the elevation lookup enabled.\n showPointerElevation: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).showPointerElevation,\n DEFAULT_PROJECT_PREFERENCES.map.showPointerElevation,\n ),\n // Missing means follow the saved project basemap. Do not reapply the\n // new-project Streets default after a user has selected a shared style.\n mapboxStyleUrl:\n normalizeString((map as Partial<ProjectPreferences[\"map\"]>).mapboxStyleUrl) || undefined,\n arcgisBasemap:\n normalizeString((map as Partial<ProjectPreferences[\"map\"]>).arcgisBasemap) || undefined,\n // Missing means follow the saved project basemap, as it does for\n // `mapboxStyleUrl` above: a project written before this field existed\n // chose nothing, and reapplying the new-project default would repaint\n // its globe with Ion imagery the next time it opened. New projects get\n // the default from `DEFAULT_PROJECT_PREFERENCES` and save it explicitly.\n cesiumBasemap: normalizeCesiumBasemap(\n (map as Partial<ProjectPreferences[\"map\"]>).cesiumBasemap,\n ),\n // Older projects omit this field and continue to open with terrain off.\n terrainEnabled: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).terrainEnabled,\n DEFAULT_PROJECT_PREFERENCES.map.terrainEnabled,\n ),\n // Kept as a free string here; the app coerces an unknown notation to\n // decimal degrees when it renders, so a hand-edited project cannot break\n // the readout.\n coordinateFormat:\n typeof (map as Partial<ProjectPreferences[\"map\"]>).coordinateFormat === \"string\"\n ? ((map as Partial<ProjectPreferences[\"map\"]>).coordinateFormat as string)\n : DEFAULT_PROJECT_PREFERENCES.map.coordinateFormat,\n // Absent in projects written before the EPSG readout existed; only a\n // positive integer survives, so a hand-edited value cannot reach proj4.\n coordinateEpsgCode: normalizeEpsgCode(\n (map as Partial<ProjectPreferences[\"map\"]>).coordinateEpsgCode,\n ),\n // Older projects omit this field and keep fitting to newly added data.\n zoomToNewLayers: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).zoomToNewLayers,\n DEFAULT_PROJECT_PREFERENCES.map.zoomToNewLayers,\n ),\n },\n environmentVariables: Array.isArray(candidate.environmentVariables)\n ? candidate.environmentVariables\n .map(normalizeEnvironmentVariable)\n .filter((variable): variable is RuntimeEnvironmentVariable => Boolean(variable))\n : [],\n geocoding: normalizeGeocodingPreferences(candidate.geocoding),\n };\n}\n\n/**\n * Keep a stored EPSG code only when it is a positive integer.\n *\n * Args:\n * value: The stored value, of unknown shape.\n *\n * Returns:\n * The code, or undefined for anything else.\n */\nfunction normalizeEpsgCode(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\nfunction normalizeGeocodingPreferences(geocoding: unknown): ProjectPreferences[\"geocoding\"] {\n if (!geocoding || typeof geocoding !== \"object\") {\n return { ...DEFAULT_PROJECT_PREFERENCES.geocoding, apiKeys: {} };\n }\n const candidate = geocoding as Partial<ProjectPreferences[\"geocoding\"]>;\n const apiKeys: Record<string, string> = {};\n if (candidate.apiKeys && typeof candidate.apiKeys === \"object\") {\n for (const [key, value] of Object.entries(candidate.apiKeys)) {\n const normalizedKey = key.trim();\n if (normalizedKey && typeof value === \"string\") {\n apiKeys[normalizedKey] = value;\n }\n }\n }\n return {\n providerId:\n typeof candidate.providerId === \"string\" && candidate.providerId.trim()\n ? candidate.providerId.trim()\n : DEFAULT_PROJECT_PREFERENCES.geocoding.providerId,\n apiKeys,\n forwardEndpoint:\n typeof candidate.forwardEndpoint === \"string\" && candidate.forwardEndpoint.trim()\n ? candidate.forwardEndpoint.trim()\n : undefined,\n reverseEndpoint:\n typeof candidate.reverseEndpoint === \"string\" && candidate.reverseEndpoint.trim()\n ? candidate.reverseEndpoint.trim()\n : undefined,\n email:\n typeof candidate.email === \"string\" && candidate.email.trim()\n ? candidate.email.trim()\n : undefined,\n };\n}\n\n/** Coerce an unknown value to a supported scale unit, defaulting to metric. */\nfunction normalizeScaleUnit(value: unknown): MapScaleUnit {\n return value === \"imperial\" || value === \"nautical\" ? value : \"metric\";\n}\n\nfunction normalizeBounds(bounds: unknown): ProjectPreferences[\"map\"][\"bounds\"] {\n if (\n Array.isArray(bounds) &&\n bounds.length === 4 &&\n bounds.every((value) => Number.isFinite(value))\n ) {\n // Clamp to valid lng/lat ranges so the stored bounds match what the map\n // controller applies, then keep the ordering check so an empty or\n // inverted region falls back to the default instead of being persisted.\n const west = clampCoordinate(Number(bounds[0]), -180, 180);\n const south = clampCoordinate(Number(bounds[1]), -85, 85);\n const east = clampCoordinate(Number(bounds[2]), -180, 180);\n const north = clampCoordinate(Number(bounds[3]), -85, 85);\n if (west < east && south < north) {\n return [west, south, east, north];\n }\n }\n\n return DEFAULT_PROJECT_PREFERENCES.map.bounds;\n}\n\nfunction normalizeNumber(value: unknown, fallback: number): number {\n return Number.isFinite(value) ? Number(value) : fallback;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction clampCoordinate(value: number, min: number, max: number): number {\n return clamp(value, min, max);\n}\n\nfunction normalizeBoolean(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nconst ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nfunction normalizeEnvironmentVariable(variable: unknown): RuntimeEnvironmentVariable | null {\n if (!variable || typeof variable !== \"object\") return null;\n const candidate = variable as Partial<RuntimeEnvironmentVariable>;\n const key = typeof candidate.key === \"string\" ? candidate.key.trim() : \"\";\n if (!key || !ENVIRONMENT_VARIABLE_NAME_PATTERN.test(key)) return null;\n\n return {\n key,\n value: typeof candidate.value === \"string\" ? candidate.value : \"\",\n enabled: normalizeBoolean(candidate.enabled, true),\n ...(candidate.secret === false ? { secret: false } : {}),\n };\n}\n\nconst PROJECT_PLUGIN_CONTROL_POSITIONS = new Set<ProjectPluginControlPosition>([\n \"top-left\",\n \"top-right\",\n \"bottom-left\",\n \"bottom-right\",\n]);\n\nfunction normalizeProjectPlugins(plugins: unknown): ProjectPluginState | null {\n if (!plugins || typeof plugins !== \"object\") return null;\n\n const candidate = plugins as Partial<ProjectPluginState>;\n const manifestUrls = Array.isArray(candidate.manifestUrls)\n ? uniqueStrings(candidate.manifestUrls).filter(isAllowedPluginManifestUrl)\n : [];\n const activePluginIds = Array.isArray(candidate.activePluginIds)\n ? uniqueStrings(candidate.activePluginIds)\n : [];\n const mapControlPositions: Record<string, ProjectPluginControlPosition> = {};\n const settings: Record<string, unknown> = {};\n\n if (candidate.mapControlPositions && typeof candidate.mapControlPositions === \"object\") {\n for (const [pluginId, position] of Object.entries(candidate.mapControlPositions)) {\n if (\n typeof pluginId === \"string\" &&\n pluginId.trim() &&\n PROJECT_PLUGIN_CONTROL_POSITIONS.has(position as ProjectPluginControlPosition)\n ) {\n mapControlPositions[pluginId.trim()] = position as ProjectPluginControlPosition;\n }\n }\n }\n\n if (candidate.settings && typeof candidate.settings === \"object\") {\n for (const [pluginId, value] of Object.entries(candidate.settings)) {\n if (typeof pluginId === \"string\" && pluginId.trim() && isJsonCompatible(value)) {\n settings[pluginId.trim()] = value;\n }\n }\n }\n\n return {\n manifestUrls,\n activePluginIds,\n mapControlPositions,\n settings,\n };\n}\n\n// Plugin manifest URLs lead to fetched and executed code, so both the\n// Settings dialog and project-file loading enforce the same scheme rule:\n// HTTPS, or HTTP on a loopback host for local development.\nexport function isAllowedPluginManifestUrl(url: string): boolean {\n try {\n const { protocol, hostname } = new URL(url);\n return (\n protocol === \"https:\" ||\n (protocol === \"http:\" && [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(hostname))\n );\n } catch {\n return false;\n }\n}\n\nfunction uniqueStrings(values: unknown[]): string[] {\n const seen = new Set<string>();\n const normalized: string[] = [];\n\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const id = value.trim();\n if (!id || seen.has(id)) continue;\n seen.add(id);\n normalized.push(id);\n }\n\n return normalized;\n}\n\nfunction isJsonCompatible(value: unknown): boolean {\n if (value === null) return true;\n\n switch (typeof value) {\n case \"string\":\n case \"boolean\":\n return true;\n case \"number\":\n return Number.isFinite(value);\n case \"object\":\n if (Array.isArray(value)) return value.every(isJsonCompatible);\n if (!isPlainObject(value)) return false;\n return Object.values(value).every(isJsonCompatible);\n default:\n return false;\n }\n}\n\nfunction isPlainObject(value: object): boolean {\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** Browser byte URLs belong to the live session, never a saved project. */\nfunction withoutLocalRasterBytes(layer: GeoLibreLayer): GeoLibreLayer {\n if (layer.metadata?.localBytesUrl === undefined) return layer;\n const { localBytesUrl: _localBytesUrl, ...metadata } = layer.metadata;\n return { ...layer, metadata };\n}\n\n/**\n * Drop a Zarr layer's request headers. They authenticate the store (a bearer\n * token, an API key), so they are credentials: the Zarr adds keep them in a\n * session-only map the renderer reads (`registerZarrHeaders` in\n * @geolibre/map), but a project saved before that change carries them on\n * `source` (opengeos/GeoLibre#2643). Not applied on parse, so such a project\n * still authenticates for the session it is opened in.\n *\n * @param layer - Any layer.\n * @returns The layer without `source.headers` when it is a Zarr layer, else\n * the same object.\n */\nfunction withoutZarrHeaders(layer: GeoLibreLayer): GeoLibreLayer {\n if (layer.type !== \"zarr\" || layer.source?.headers === undefined) return layer;\n const { headers: _headers, ...source } = layer.source;\n return { ...layer, source };\n}\n\n/**\n * Strip the session-only state every serializer must leave out, whether or not\n * the project came through `projectFromStore`.\n *\n * @param layer - Any layer.\n * @returns The layer as it may be written to a project file.\n */\nfunction portableLayer(layer: GeoLibreLayer): GeoLibreLayer {\n return withoutZarrHeaders(withoutLocalRasterBytes(layer));\n}\n\nfunction normalizeLayer(layer: GeoLibreLayer): GeoLibreLayer {\n layer = withoutLocalRasterBytes(layer);\n // `capabilities` is split off the spread rather than overwritten: a raw value\n // that normalizes to nothing (`{}`, an array, a string, an object with no\n // boolean flag) must not survive into the normalized layer and be written\n // back out on the next save.\n const {\n capabilities: rawCapabilities,\n filterExpression: rawFilterExpression,\n descriptiveMetadata: rawDescriptiveMetadata,\n ...rest\n } = layer;\n const capabilities = normalizeLayerCapabilities(rawCapabilities);\n // Same split for the user-authored catalog metadata: a block that cleans to\n // nothing (every field blank) is dropped rather than round-tripped.\n const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);\n const filterExpression =\n Array.isArray(rawFilterExpression) &&\n rawFilterExpression.length > 0 &&\n validateMapExpression(JSON.stringify(rawFilterExpression), { expectedType: \"boolean\" }).ok\n ? rawFilterExpression\n : undefined;\n return {\n ...rest,\n style: { ...DEFAULT_LAYER_STYLE, ...layer.style },\n visible: layer.visible ?? true,\n opacity: layer.opacity ?? 1,\n metadata: layer.metadata ?? {},\n source: layer.source ?? {},\n ...(capabilities ? { capabilities } : {}),\n ...(filterExpression ? { filterExpression } : {}),\n ...(descriptiveMetadata ? { descriptiveMetadata } : {}),\n };\n}\n\nexport function normalizeProjectComments(rawComments: unknown): ProjectComment[] {\n if (!Array.isArray(rawComments)) return [];\n const result: ProjectComment[] = [];\n for (const item of rawComments) {\n if (!item || typeof item !== \"object\") continue;\n const c = item as Record<string, unknown>;\n if (typeof c.id !== \"string\" || !c.id) continue;\n\n if (!c.anchor || typeof c.anchor !== \"object\") continue;\n const anchorObj = c.anchor as Record<string, unknown>;\n let anchor: CommentAnchor;\n if (\n anchorObj.type === \"point\" &&\n Array.isArray(anchorObj.lngLat) &&\n anchorObj.lngLat.length === 2 &&\n typeof anchorObj.lngLat[0] === \"number\" &&\n typeof anchorObj.lngLat[1] === \"number\"\n ) {\n anchor = { type: \"point\", lngLat: [anchorObj.lngLat[0], anchorObj.lngLat[1]] };\n } else if (\n anchorObj.type === \"feature\" &&\n typeof anchorObj.layerId === \"string\" &&\n (typeof anchorObj.featureId === \"string\" || typeof anchorObj.featureId === \"number\")\n ) {\n const featLngLat =\n Array.isArray(anchorObj.lngLat) &&\n anchorObj.lngLat.length === 2 &&\n typeof anchorObj.lngLat[0] === \"number\" &&\n typeof anchorObj.lngLat[1] === \"number\"\n ? ([anchorObj.lngLat[0], anchorObj.lngLat[1]] as [number, number])\n : undefined;\n anchor = {\n type: \"feature\",\n layerId: anchorObj.layerId,\n featureId: anchorObj.featureId,\n ...(featLngLat ? { lngLat: featLngLat } : {}),\n };\n } else {\n continue;\n }\n\n const authorObj =\n c.author && typeof c.author === \"object\" ? (c.author as Record<string, unknown>) : {};\n const HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n const author: CommentAuthor = {\n name:\n typeof authorObj.name === \"string\" && authorObj.name.trim()\n ? authorObj.name.trim()\n : \"Anonymous\",\n color:\n typeof authorObj.color === \"string\" && HEX.test(authorObj.color.trim())\n ? authorObj.color.trim()\n : \"#3b82f6\",\n };\n\n const replies: CommentReply[] = [];\n if (Array.isArray(c.replies)) {\n for (const rItem of c.replies) {\n if (!rItem || typeof rItem !== \"object\") continue;\n const r = rItem as Record<string, unknown>;\n if (typeof r.id !== \"string\" || !r.id) continue;\n const rAuthorObj =\n r.author && typeof r.author === \"object\" ? (r.author as Record<string, unknown>) : {};\n replies.push({\n id: r.id,\n author: {\n name:\n typeof rAuthorObj.name === \"string\" && rAuthorObj.name.trim()\n ? rAuthorObj.name.trim()\n : \"Anonymous\",\n color:\n typeof rAuthorObj.color === \"string\" && HEX.test(rAuthorObj.color.trim())\n ? rAuthorObj.color.trim()\n : \"#3b82f6\",\n },\n body: typeof r.body === \"string\" ? r.body : \"\",\n createdAt: typeof r.createdAt === \"string\" ? r.createdAt : new Date().toISOString(),\n });\n }\n }\n\n result.push({\n id: c.id,\n anchor,\n author,\n body: typeof c.body === \"string\" ? c.body : \"\",\n createdAt: typeof c.createdAt === \"string\" ? c.createdAt : new Date().toISOString(),\n resolved: Boolean(c.resolved),\n replies,\n });\n }\n return result;\n}\n\nexport function projectFromStore(state: {\n projectName: string;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n blankBackgroundColor?: string | null;\n layers: GeoLibreLayer[];\n selectedLayerId?: string | null;\n layerGroups?: LayerGroup[];\n preferences: ProjectPreferences;\n plugins?: ProjectPluginState | null;\n legend?: LegendConfig | null;\n printLayout?: PrintLayoutConfig | null;\n storymap?: StoryMap | null;\n models?: ProcessingModel[] | null;\n processingHistory?: ProcessingRun[] | null;\n widgets?: DashboardWidget[] | null;\n dashboardColumns?: number;\n mapLayout?: MapGridLayout;\n secondaryMapViews?: SecondaryMapView[];\n primaryMapLabel?: string;\n primaryRenderer?: MapRendererKind;\n /** Project-scoped Style Manager entries (the store's `projectStyleLibrary`). */\n styleLibrary?: StyleLibraryEntry[] | null;\n comments?: ProjectComment[] | null;\n /** Startup interaction state loaded with the project, written back as-is. */\n interaction?: ProjectInteraction | null;\n metadata: Record<string, unknown>;\n}): GeoLibreProject {\n const styles: Record<string, LayerStyle> = {};\n for (const layer of state.layers) {\n styles[layer.id] = layer.style;\n }\n const plugins = normalizeProjectPlugins(state.plugins);\n const legend = normalizeLegendConfig(state.legend);\n // Persist the composer only once it differs from the defaults, so a project\n // that never opened Print Layout keeps its previous byte-for-byte shape.\n const printLayout = normalizePrintLayoutConfig(state.printLayout);\n const storymap = normalizeStoryMap(state.storymap);\n const models = normalizeModels(state.models);\n const processingHistory = normalizeProcessingHistory(state.processingHistory);\n const widgets = normalizeWidgets(state.widgets);\n const comments = normalizeProjectComments(state.comments);\n const interaction = normalizeProjectInteraction(state.interaction);\n // Persist a non-default column count only; a default-layout dashboard (or a\n // widget-less project) stays free of the key for legacy readers.\n const dashboardColumns =\n state.dashboardColumns === undefined\n ? DEFAULT_DASHBOARD_COLUMNS\n : normalizeDashboardColumns(state.dashboardColumns);\n // Persist every group (including empty folders, which the UI supports). The\n // key is spread only when non-empty so legacy readers that don't recognise it\n // are unaffected; normalizeLayerGroups round-trips them back on load.\n const layerGroups = state.layerGroups ?? [];\n // Persist the grid only when it is more than a single pane; default single-map\n // projects stay byte-identical and unaffected by this feature. The reconcile\n // keeps `secondaryMapViews` exactly `rows * cols - 1` long even if state drifted.\n const { mapLayout, secondaryMapViews } = resolveMapGrid(\n normalizeMapLayout(state.mapLayout),\n normalizeSecondaryMapViews(state.secondaryMapViews),\n { mapView: state.mapView },\n );\n const persistGrid = mapLayout.rows * mapLayout.cols > 1;\n const styleLibrary = normalizeStyleLibraryEntries(state.styleLibrary);\n const selectedLayerId =\n state.selectedLayerId === null\n ? null\n : typeof state.selectedLayerId === \"string\" &&\n state.layers.some((layer) => layer.id === state.selectedLayerId)\n ? state.selectedLayerId\n : undefined;\n return {\n version: PROJECT_VERSION,\n name: state.projectName,\n mapView: state.mapView,\n basemapStyleUrl: state.basemapStyleUrl,\n basemapVisible: state.basemapVisible,\n basemapOpacity: state.basemapOpacity,\n ...(state.blankBackgroundColor ? { blankBackgroundColor: state.blankBackgroundColor } : {}),\n layers: state.layers.map(prepareLayerForSave),\n ...(selectedLayerId !== undefined ? { selectedLayerId } : {}),\n ...(layerGroups.length > 0 ? { layerGroups } : {}),\n styles,\n preferences: state.preferences,\n ...(plugins ? { plugins } : {}),\n ...(legend ? { legend } : {}),\n ...(printLayout && !isDefaultPrintLayout(printLayout) ? { printLayout } : {}),\n ...(storymap ? { storymap } : {}),\n ...(models ? { models } : {}),\n ...(processingHistory ? { processingHistory } : {}),\n ...(widgets ? { widgets } : {}),\n ...(dashboardColumns !== DEFAULT_DASHBOARD_COLUMNS ? { dashboardColumns } : {}),\n ...(persistGrid\n ? {\n mapLayout,\n secondaryMapViews,\n ...(normalizeString(state.primaryMapLabel)\n ? { primaryMapLabel: normalizeString(state.primaryMapLabel) }\n : {}),\n }\n : {}),\n // Written only for the non-default renderer, and independently of the grid:\n // a single-pane Cesium project persists `primaryRenderer` with no\n // `mapLayout`, and a MapLibre project writes neither.\n ...(normalizePrimaryRenderer(state.primaryRenderer)\n ? { primaryRenderer: normalizePrimaryRenderer(state.primaryRenderer)! }\n : {}),\n ...(styleLibrary.length > 0 ? { styleLibrary } : {}),\n ...(comments.length > 0 ? { comments } : {}),\n ...(interaction ? { interaction } : {}),\n metadata: state.metadata,\n };\n}\n\n// An external native layer can drop its persisted `geojson` only if its\n// features can be reconstructed on reopen, i.e. it has a fetchable source URL\n// (the Add Vector Layer / WFS / geojson-url cases). Layers loaded from local\n// files or built in-memory (e.g. by a plugin's drawing/annotation control)\n// have no such URL, so the persisted `geojson` is their ONLY copy and must be\n// kept.\nfunction hasRestorableSourceUrl(layer: GeoLibreLayer): boolean {\n const sourceUrl = layer.source.url;\n const originalUrl = layer.metadata.originalUrl;\n return (\n (typeof sourceUrl === \"string\" && sourceUrl.trim() !== \"\") ||\n (typeof originalUrl === \"string\" && originalUrl.trim() !== \"\")\n );\n}\n\nfunction prepareLayerForSave(layer: GeoLibreLayer): GeoLibreLayer {\n const hasGeometryEdits = layer.metadata.geometryEdited === true;\n layer = portableLayer(layer);\n // WFS feature URLs can contain inline authentication supplied by a plugin.\n // Persist a sanitized reference, and keep the fetched collection embedded so\n // the saved project remains renderable without storing those credentials.\n // The stripped flag is persisted alongside that sanitized reference: once the\n // reference is stored credential-free, a later save of the reopened project\n // cannot tell that the URL ever needed a secret, and would drop the embedded\n // features the reference can no longer re-fetch without it.\n let wfsCredentialsRedacted = layer.metadata.wfsCredentialsRedacted === true;\n if (layer.metadata.sourceKind === \"wfs-getfeature\") {\n const sourceUrl =\n typeof layer.source.url === \"string\"\n ? redactUrlCredentials(layer.source.url)\n : layer.source.url;\n const sourcePath =\n typeof layer.sourcePath === \"string\"\n ? redactUrlCredentials(layer.sourcePath)\n : layer.sourcePath;\n const originalUrl =\n typeof layer.metadata.originalUrl === \"string\"\n ? redactUrlCredentials(layer.metadata.originalUrl)\n : layer.metadata.originalUrl;\n const stripped =\n sourceUrl !== layer.source.url ||\n sourcePath !== layer.sourcePath ||\n originalUrl !== layer.metadata.originalUrl;\n wfsCredentialsRedacted ||= stripped;\n if (stripped) {\n layer = {\n ...layer,\n source: sourceUrl === layer.source.url ? layer.source : { ...layer.source, url: sourceUrl },\n sourcePath,\n metadata: {\n ...layer.metadata,\n ...(originalUrl === layer.metadata.originalUrl ? {} : { originalUrl }),\n wfsCredentialsRedacted: true,\n },\n };\n }\n }\n // This flag describes unsaved changes to the live source, not persisted\n // project state. A reference-only save reloads the original geometries;\n // carrying the flag into that project would warn about nonexistent edits.\n if (layer.metadata.geometryEdited !== undefined) {\n const { geometryEdited: _geometryEdited, ...metadata } = layer.metadata;\n layer = { ...layer, metadata };\n }\n // The live time filter is derived from the Time Slider's current date, so it\n // is transient: strip it before saving so a reopened project never starts\n // with a stale time-window filter hiding most of a layer's features. The\n // binding config in `metadata.timeBinding` persists, and the Time Slider\n // re-applies the filter the next time it activates.\n if (layer.timeFilter !== undefined) {\n const { timeFilter: _timeFilter, ...rest } = layer;\n layer = rest;\n }\n if (layer.embedFilter !== undefined) {\n const { embedFilter: _embedFilter, ...rest } = layer;\n layer = rest;\n }\n // Catalog metadata is written only when it says something: a record a\n // plugin (or an older build) left blank, or with blank fields, is cleaned or\n // dropped so an empty `descriptiveMetadata` block never reaches the file.\n if (layer.descriptiveMetadata !== undefined) {\n const { descriptiveMetadata: rawDescriptiveMetadata, ...rest } = layer;\n const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);\n layer = descriptiveMetadata ? { ...rest, descriptiveMetadata } : rest;\n }\n\n // Some live plugin layers publish a large in-memory row model solely for\n // the Attribute Table and rebuild it from their feed on activation. Keeping\n // those rows in the store makes them queryable; embedding them in every\n // project/autosave would persist stale positions and can cross the history\n // snapshot ceiling.\n if (layer.geojson && layer.metadata.transientGeojson === true) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n // Live CZML feeds likewise rebuild their renderer payload on activation.\n // Persisting thousands of packets in every autosave duplicates the feed,\n // stores stale positions, and can exceed the history snapshot limit.\n if (layer.source.czmlData !== undefined && layer.metadata.transientCzml === true) {\n const { czmlData: _czmlData, ...source } = layer.source;\n layer = { ...layer, source };\n }\n\n // External native layers that restore their features from a source URL keep\n // a `geojson` copy on the map only for the attribute table; it is redundant\n // in a saved project and would only bloat it, so strip it. Layers without a\n // restorable URL (local-file or in-memory) keep their `geojson` because it is\n // the sole copy GeoLibre's restore path (`ensureExternalGeoJsonNativeLayer`)\n // re-renders from.\n //\n // Add Vector Layer (`maplibre-gl-vector`) layers are the exception: they are\n // restored by the control, not from `geojson` — from the file path on desktop\n // or embedded `metadata.embeddedGeoJSON` on the web. Their `geojson` is only\n // the attribute table's copy, so persisting it would silently embed the whole\n // dataset (bypassing the web embed prompt) instead of saving the path. Strip\n // it regardless of a restorable URL.\n const isVectorControlLayer = layer.metadata.sourceKind === \"maplibre-gl-vector\";\n if (\n layer.metadata.externalNativeLayer === true &&\n layer.geojson &&\n (hasRestorableSourceUrl(layer) || isVectorControlLayer)\n ) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n const wfsSourceUrl = layer.source.url;\n let hasHttpWfsSource = false;\n if (typeof wfsSourceUrl === \"string\") {\n try {\n const url = new URL(wfsSourceUrl);\n hasHttpWfsSource = url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n hasHttpWfsSource = false;\n }\n }\n\n // Host-managed WFS records reload through their persisted successful\n // GetFeature URL. Keep unedited feature collections reference-only in saved\n // projects. Edited geometry becomes authoritative embedded data, so omit\n // the request URL in that saved copy; otherwise a later save/reopen could\n // replace the edits with the service response.\n if (\n layer.type === \"geojson\" &&\n layer.metadata.sourceKind === \"wfs-getfeature\" &&\n layer.geojson &&\n hasHttpWfsSource\n ) {\n if (hasGeometryEdits) {\n const { url: _url, ...source } = layer.source;\n layer = { ...layer, source };\n } else if (!wfsCredentialsRedacted) {\n const { geojson: _geojson, ...rest } = layer;\n // Credential-bearing references are deliberately kept with their data.\n layer = rest;\n }\n }\n\n // An Add Vector Layer layer GeoLibre adopted (`maplibre-gl-vector-adopted`,\n // ADOPTED_VECTOR_SOURCE_KIND in @geolibre/plugins) holds its features in\n // `geojson`, but a URL-backed one saves the URL and is re-read through the\n // control on reopen, like the control layer it was. A browser-picked file has\n // no URL and keeps its features, like a drag-and-drop layer.\n if (\n layer.geojson &&\n layer.metadata.sourceKind === \"maplibre-gl-vector-adopted\" &&\n hasRestorableSourceUrl(layer)\n ) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n // A local-file layer the desktop host can re-read from its absolute path on\n // reopen (a drag-dropped or Add Data vector file) does not embed its features\n // either: the path is saved and the data is reloaded from disk. The flag is\n // only set when a real path was captured (desktop), so a web project — which\n // cannot re-read a path — never sets it and keeps the embedded copy.\n if (layer.geojson && layer.metadata.localFileReloadable === true) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n if (layer.type === \"wms\") {\n const tiles = layer.source.tiles;\n if (!Array.isArray(tiles)) return layer;\n const portableTiles = tiles.map((tile) => portableWmsTileUrl(tile));\n return portableTiles.some((tile, index) => tile !== tiles[index])\n ? { ...layer, source: { ...layer.source, tiles: portableTiles } }\n : layer;\n }\n\n if (layer.type !== \"xyz\") return layer;\n\n const originalUrl =\n typeof layer.metadata.originalUrl === \"string\" && layer.metadata.originalUrl.trim()\n ? layer.metadata.originalUrl\n : typeof layer.source.url === \"string\" && layer.source.url.trim()\n ? layer.source.url\n : null;\n if (!originalUrl) return layer;\n\n const metadata = { ...layer.metadata };\n delete metadata.resolvedUrl;\n\n // The collapse below rewinds a resolved short URL (or a desktop protocol URL)\n // back to what the user typed, because those tile URLs are not portable. A\n // TileJSON layer is the exception: `tiles` holds the document's own https\n // templates, which are portable, while its `originalUrl` is the *document*\n // URL and carries no {z}/{x}/{y}. Collapsing onto it would leave the saved\n // layer unable to request a tile until a re-fetch succeeds — and\n // `resolveProjectXyzLayers` keeps the on-disk layer when the document is\n // unreachable, so an offline reopen would strand it. Rewind only `url`.\n const tiles = typeof layer.metadata.tilejsonUrl === \"string\" ? {} : { tiles: [originalUrl] };\n\n return {\n ...layer,\n source: {\n ...layer.source,\n ...tiles,\n url: originalUrl,\n },\n metadata,\n };\n}\n\n/**\n * The plain HTTP(S) WMS template inside the desktop app's `geolibre-wms://`\n * wrapper (its CORS-exempt native fetcher), or the tile unchanged when it is\n * not wrapped or the wrapped URL is not HTTP(S). Shared by project export and\n * the ArcGIS engine's story-export templates.\n *\n * @param tile - A tile template, wrapped or not.\n * @returns The unwrapped template, or `tile` itself.\n */\nexport function portableWmsTileUrl(tile: unknown): unknown {\n // Keep this protocol prefix in sync with WMS_TILE_PROTOCOL in the desktop\n // app, which packages/core cannot import without reversing dependencies.\n if (typeof tile !== \"string\" || !tile.startsWith(\"geolibre-wms://\")) return tile;\n try {\n const url = new URL(tile).searchParams.get(\"url\");\n if (!url) return tile;\n const parsed = new URL(url);\n return parsed.protocol === \"http:\" || parsed.protocol === \"https:\" ? url : tile;\n } catch {\n return tile;\n }\n}\n\n/**\n * The store form of one project layer: its style completed from\n * {@link DEFAULT_LAYER_STYLE} and the project's top-level `styles` entry.\n *\n * Legacy and externally-authored projects can carry a partial top-level style\n * alongside newer fields on the layer itself. Those layer fields are kept,\n * while the top-level copy stays authoritative where it explicitly supplies a\n * value.\n *\n * @param project - The project the layer belongs to (for its `styles` map).\n * @param layer - One of `project.layers`.\n * @returns A new layer record ready for the store.\n */\nexport function hydrateProjectLayer(\n project: Pick<GeoLibreProject, \"styles\">,\n layer: GeoLibreLayer,\n): GeoLibreLayer {\n const topLevel = project.styles?.[layer.id];\n return {\n ...layer,\n style: topLevel\n ? { ...DEFAULT_LAYER_STYLE, ...layer.style, ...topLevel }\n : { ...DEFAULT_LAYER_STYLE, ...layer.style },\n };\n}\n\nexport function applyProjectToStore(project: GeoLibreProject): {\n projectName: string;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n blankBackgroundColor: string | null;\n layers: GeoLibreLayer[];\n layerGroups: LayerGroup[];\n preferences: ProjectPreferences;\n projectPlugins: ProjectPluginState | null;\n legend: LegendConfig;\n printLayout: PrintLayoutConfig;\n storymap: StoryMap | null;\n models: ProcessingModel[];\n processingHistory: ProcessingRun[];\n widgets: DashboardWidget[];\n dashboardColumns: number;\n mapLayout: MapGridLayout;\n secondaryMapViews: SecondaryMapView[];\n primaryMapLabel: string;\n primaryRenderer: MapRendererKind;\n projectStyleLibrary: StyleLibraryEntry[];\n comments: ProjectComment[];\n projectInteraction: ProjectInteraction | null;\n metadata: Record<string, unknown>;\n} {\n const layers = project.layers.map((layer) => hydrateProjectLayer(project, layer));\n // Re-normalize here (even though `parseProject` already did) because\n // `applyProjectToStore` is a public entry point also reached directly by\n // programmatic/newProject loads that never passed through `parseProject`, so\n // this stays a hardening boundary for untrusted group data. The call is\n // idempotent on already-normalized input.\n const layerGroups = normalizeLayerGroups(project.layerGroups);\n const validGroupIds = new Set(layerGroups.map((g) => g.id));\n // Drop dangling groupIds, then restore the contiguity invariant the layer\n // panel relies on, in case the project was hand-edited or produced externally\n // with a group's members interleaved among unrelated layers.\n const normalizedLayers = normalizeGroupContiguity(\n layers.map((layer) =>\n layer.groupId && !validGroupIds.has(layer.groupId) ? { ...layer, groupId: undefined } : layer,\n ),\n );\n const basemapStyleUrl = project.basemapStyleUrl;\n const basemapVisible = project.basemapVisible ?? true;\n const basemapOpacity = project.basemapOpacity ?? 1;\n const blankBackgroundColor = normalizeBlankBackgroundColor(project.blankBackgroundColor);\n // Reconcile the (possibly hand-edited or programmatic) grid so the store's\n // invariant `secondaryMapViews.length === rows * cols - 1` always holds.\n const mapView = normalizeMapViewState(project.mapView);\n const { mapLayout, secondaryMapViews } = resolveMapGrid(\n normalizeMapLayout(project.mapLayout),\n normalizeSecondaryMapViews(project.secondaryMapViews),\n { mapView },\n );\n\n // Scrub cross-references that point at layers not present in the loaded\n // project (orphans from hand-editing or a partial project file).\n const existingLayerIds = new Set(normalizedLayers.map((l) => l.id));\n const widgets = normalizeWidgets(project.widgets) ?? [];\n const comments = normalizeProjectComments(project.comments);\n const legend = normalizeLegendConfig(project.legend) ?? {\n ...DEFAULT_LEGEND_CONFIG,\n };\n\n const allReferencedIds = new Set<string>();\n for (const w of widgets) allReferencedIds.add(w.layerId);\n for (const c of comments) {\n if (c.anchor.type === \"feature\") allReferencedIds.add(c.anchor.layerId);\n }\n for (const id of legend.order) allReferencedIds.add(id);\n for (const key of Object.keys(legend.overrides)) {\n const base = key.includes(\"::\") ? key.slice(0, key.indexOf(\"::\")) : key;\n allReferencedIds.add(base);\n }\n if (legend.customEntries) {\n for (const key of Object.keys(legend.customEntries)) {\n if (!key.startsWith(\"custom:\")) allReferencedIds.add(key);\n }\n }\n\n const orphanIds = new Set([...allReferencedIds].filter((id) => !existingLayerIds.has(id)));\n\n const scrubbedWidgets =\n orphanIds.size > 0 ? scrubWidgetsForRemovedLayers(widgets, orphanIds) : widgets;\n const scrubbedComments =\n orphanIds.size > 0 ? scrubCommentsForRemovedLayers(comments, orphanIds) : comments;\n const scrubbedLegend =\n orphanIds.size > 0 ? scrubLegendForRemovedLayers(legend, orphanIds) : legend;\n // The composer's data/atlas blocks name a layer directly rather than through\n // `allReferencedIds`, so they are scrubbed against the surviving layer set.\n const printLayout = scrubPrintLayoutForLayers(\n normalizePrintLayoutConfig(project.printLayout) ?? createDefaultPrintLayout(),\n existingLayerIds,\n );\n\n return {\n projectName: project.name,\n mapView,\n basemapStyleUrl,\n basemapVisible,\n basemapOpacity,\n blankBackgroundColor,\n layers: normalizedLayers,\n layerGroups,\n preferences: normalizeProjectPreferences(project.preferences),\n projectPlugins: normalizeProjectPlugins(project.plugins),\n legend: scrubbedLegend,\n printLayout,\n storymap: normalizeStoryMap(project.storymap),\n models: normalizeModels(project.models) ?? [],\n processingHistory: normalizeProcessingHistory(project.processingHistory) ?? [],\n widgets: scrubbedWidgets,\n dashboardColumns: normalizeDashboardColumns(project.dashboardColumns),\n mapLayout,\n secondaryMapViews,\n primaryMapLabel: normalizeString(project.primaryMapLabel),\n // An unknown or absent value resolves to the 2D map, so a project written\n // before #2217 (and any hand-edited one) opens on MapLibre as before.\n primaryRenderer: normalizePrimaryRenderer(project.primaryRenderer) ?? DEFAULT_PRIMARY_RENDERER,\n projectStyleLibrary: normalizeStyleLibraryEntries(project.styleLibrary),\n comments: scrubbedComments,\n projectInteraction: normalizeProjectInteraction(project.interaction),\n metadata: project.metadata,\n };\n}\n\n/**\n * Create an unlinked copy of a project, suffixed with \"(copy)\" by default and\n * stripped of share-specific metadata (shareId, shareUrl, etc.).\n */\nexport function detachProjectCopy(\n project: GeoLibreProject,\n options: { nameSuffix?: string } = {},\n): GeoLibreProject {\n const suffix = options.nameSuffix ?? \"(copy)\";\n const rawName = project.name.trim() || DEFAULT_PROJECT_NAME;\n const name = suffix ? (rawName.endsWith(suffix) ? rawName : `${rawName} ${suffix}`) : rawName;\n\n const metadata = { ...(project.metadata ?? {}) };\n for (const key of Object.keys(metadata)) {\n if (/^share/i.test(key)) {\n delete metadata[key];\n }\n }\n\n return {\n ...project,\n id: uuidv4(),\n name,\n metadata,\n };\n}\n\n/**\n * Create a template snapshot of a project. Optionally strips data layers while\n * keeping basemap, layer groups, styles, legend config, preferences, widgets, and\n * print layout.\n */\nexport function createProjectTemplate(\n project: GeoLibreProject,\n options: { name?: string; stripDataLayers?: boolean } = {},\n): GeoLibreProject {\n const detached = detachProjectCopy(project, { nameSuffix: \"\" });\n const name = options.name?.trim() || detached.name;\n const stripDataLayers = options.stripDataLayers !== false;\n\n const layers = stripDataLayers ? [] : detached.layers;\n\n return {\n ...detached,\n name,\n layers,\n metadata: {\n ...detached.metadata,\n isTemplate: true,\n },\n };\n}\n","/**\n * The map workspace: the primary camera, the basemap, project preferences, the\n * multi-map grid and its secondary panes, and which engine draws the primary\n * map. The basemap fields are tracked by undo history; the camera is not.\n */\nimport { v4 as uuidv4 } from \"uuid\";\nimport { createDefaultMapView, normalizeBlankBackgroundColor } from \"../project\";\nimport { DEFAULT_ELLIPSOID_ID } from \"../ellipsoids\";\nimport type { PlanetaryBasemap } from \"../ellipsoids\";\nimport {\n DEFAULT_BASEMAP,\n DEFAULT_MAP_GRID_LAYOUT,\n DEFAULT_PRIMARY_RENDERER,\n DEFAULT_PROJECT_PREFERENCES,\n MAX_MAP_GRID_DIM,\n type MapGridLayout,\n type MapRendererKind,\n type MapViewState,\n type ProjectPreferences,\n type SecondaryMapView,\n} from \"../types\";\nimport type { AppState, SliceCreator } from \"./types\";\n\n/** An explicit background choice replaces the active renderer's override. */\nfunction preferencesForBasemap(state: AppState, ellipsoidId = state.preferences.map.ellipsoidId) {\n // Each engine keeps its own persisted basemap override, so this names them.\n /* eslint-disable local/no-renderer-kind-checks -- per-engine persisted basemap settings */\n const clearMapbox =\n state.primaryRenderer === \"mapbox\" && state.preferences.map.mapboxStyleUrl !== undefined;\n // Any Cesium or ArcGIS pane, not only a primary one: split panes pick the\n // renderer independently, and a pinned globe imagery or Esri style would\n // otherwise ignore the picker.\n const clearCesium =\n state.preferences.map.cesiumBasemap !== \"project\" &&\n (state.primaryRenderer === \"cesium\" ||\n state.secondaryMapViews.some((pane) => pane.viewKind === \"cesium\"));\n const clearArcgis =\n state.preferences.map.arcgisBasemap !== undefined &&\n (state.primaryRenderer === \"arcgis\" ||\n state.secondaryMapViews.some((pane) => pane.viewKind === \"arcgis\"));\n /* eslint-enable local/no-renderer-kind-checks */\n if (\n !clearMapbox &&\n !clearCesium &&\n !clearArcgis &&\n ellipsoidId === state.preferences.map.ellipsoidId\n )\n return state.preferences;\n return {\n ...state.preferences,\n map: {\n ...state.preferences.map,\n ...(clearMapbox ? { mapboxStyleUrl: undefined } : {}),\n ...(clearCesium ? { cesiumBasemap: \"project\" as const } : {}),\n ...(clearArcgis ? { arcgisBasemap: undefined } : {}),\n ellipsoidId,\n },\n };\n}\n\n/** True when two camera states have identical center/zoom/bearing/pitch. */\nfunction sameCamera(a: MapViewState, b: MapViewState): boolean {\n return (\n a.center[0] === b.center[0] &&\n a.center[1] === b.center[1] &&\n a.zoom === b.zoom &&\n a.bearing === b.bearing &&\n a.pitch === b.pitch\n );\n}\n\n/** Clamp a requested grid row/column count into the supported [1, MAX] range. */\nfunction clampGridDim(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.max(1, Math.min(MAX_MAP_GRID_DIM, Math.floor(value)));\n}\n\n/**\n * Pick a grid that holds at least `total` panes within the supported\n * `MAX_MAP_GRID_DIM x MAX_MAP_GRID_DIM` bound, minimizing empty cells first and\n * then preferring a column count close to (and, on ties, no smaller than)\n * `preferredCols` so removing a pane keeps the layout's orientation. Used when\n * collapsing the grid after a pane is removed.\n *\n * Bounding both dimensions matters: a prime `total` (e.g. 5 panes left after\n * removing one from a 2x3 grid) has no gap-free factor pair inside the bound, so\n * the only gap-free options (1x5 / 5x1) would exceed `MAX_MAP_GRID_DIM`. In that\n * case we accept the smallest bounded grid with one empty trailing cell (2x3)\n * rather than returning an out-of-range dimension.\n */\nfunction fitGrid(total: number, preferredCols: number): { rows: number; cols: number } {\n if (total <= 1) return { rows: 1, cols: 1 };\n let best: {\n rows: number;\n cols: number;\n empty: number;\n score: number;\n } | null = null;\n for (let rows = 1; rows <= MAX_MAP_GRID_DIM; rows++) {\n for (let cols = 1; cols <= MAX_MAP_GRID_DIM; cols++) {\n const capacity = rows * cols;\n if (capacity < total) continue;\n const empty = capacity - total;\n const score = Math.abs(cols - preferredCols);\n // Fewest empty cells wins; then the column count closest to\n // preferredCols; then, on a tie, the larger column count (favoring wider,\n // side-by-side layouts over tall ones).\n const better =\n best === null ||\n empty < best.empty ||\n (empty === best.empty &&\n (score < best.score || (score === best.score && cols > best.cols)));\n if (better) best = { rows, cols, empty, score };\n }\n }\n return best ? { rows: best.rows, cols: best.cols } : { rows: 1, cols: 1 };\n}\n\nexport interface MapViewSlice {\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n blankBackgroundColor: string | null;\n preferences: ProjectPreferences;\n /**\n * Multi-map grid layout (issue: split/grid view). A 1x1 grid is the normal\n * single-map workspace. `rows * cols` panes are shown; pane 0 is the primary\n * map driven by `mapView` / `basemap*`, panes 1.. are `secondaryMapViews`.\n */\n mapLayout: MapGridLayout;\n /**\n * Secondary map panes (everything past the primary pane). The store keeps\n * exactly `rows * cols - 1` entries in sync with `mapLayout`.\n */\n secondaryMapViews: SecondaryMapView[];\n /** User-entered label for the primary pane (shown only in multi-map mode). */\n primaryMapLabel: string;\n /**\n * Which engine draws the primary map area (issue #2217): the 2D MapLibre map\n * or the 3D Cesium globe. Both render the same store state, so switching\n * keeps the camera, basemap, layers, groups, visibility, and opacity — it\n * only changes what draws them. Independent of `mapLayout`: switching never\n * adds or removes panes.\n */\n primaryRenderer: MapRendererKind;\n\n setMapView: (view: Partial<MapViewState>, markDirty?: boolean) => void;\n /**\n * Resize the map grid. Clamps `rows`/`cols` into range and grows/shrinks\n * `secondaryMapViews` so it always holds `rows * cols - 1` panes; new panes\n * clone the primary map's current camera and basemap.\n */\n setMapGrid: (rows: number, cols: number) => void;\n /** Toggle synchronized camera across all panes. */\n setSyncView: (syncView: boolean) => void;\n /** Patch one secondary pane's camera by id (no-op if the id is unknown). */\n setSecondaryMapView: (id: string, view: Partial<MapViewState>, markDirty?: boolean) => void;\n /**\n * Override a layer's visibility in one secondary pane (no-op if the pane id is\n * unknown). The override forces the layer visible/hidden in that pane only,\n * independent of the primary map's visibility.\n */\n setSecondaryLayerVisibility: (id: string, layerId: string, visible: boolean) => void;\n /** Set the primary pane's custom label. */\n setPrimaryMapLabel: (label: string) => void;\n /**\n * Switch the primary map area between the 2D map and the 3D globe (no-op if\n * unchanged). Touches nothing else in the store, so the shared camera, layer,\n * and basemap state carries straight across the swap.\n */\n setPrimaryRenderer: (renderer: MapRendererKind) => void;\n /** Set one secondary pane's custom label (no-op if the id is unknown). */\n setSecondaryMapLabel: (id: string, label: string) => void;\n /**\n * Switch one secondary pane between the 2D map and the 3D globe (no-op if the\n * id is unknown or the kind is unchanged).\n */\n setSecondaryViewKind: (id: string, viewKind: NonNullable<SecondaryMapView[\"viewKind\"]>) => void;\n /** Remove one secondary pane and collapse the grid back toward 1x1. */\n removeSecondaryMapView: (id: string) => void;\n setBasemapStyleUrl: (url: string) => void;\n /**\n * Apply a planetary basemap and sync the project's ellipsoid to the body it\n * depicts, so measurements and the globe control use that body's radius. Used\n * by both the basemap picker and the Layers-panel planet switcher.\n */\n applyPlanetaryBasemap: (basemap: PlanetaryBasemap) => void;\n /**\n * Return to Earth: apply `styleUrl` (typically the Earth basemap that was\n * active before a planet was selected, e.g. Liberty) and reset the ellipsoid\n * to Earth. Used when a planet is deselected in the switcher.\n */\n restoreEarthBasemap: (styleUrl: string) => void;\n setBasemapVisible: (visible: boolean) => void;\n setBasemapOpacity: (opacity: number) => void;\n setBlankBackgroundColor: (color: string | null) => void;\n setPreferences: (preferences: ProjectPreferences) => void;\n}\n\nexport const createMapViewSlice: SliceCreator<MapViewSlice> = (set) => ({\n mapView: createDefaultMapView(),\n basemapStyleUrl: DEFAULT_BASEMAP,\n basemapVisible: true,\n basemapOpacity: 1,\n blankBackgroundColor: null,\n preferences: DEFAULT_PROJECT_PREFERENCES,\n mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT },\n secondaryMapViews: [],\n primaryMapLabel: \"\",\n primaryRenderer: DEFAULT_PRIMARY_RENDERER,\n\n setMapView: (view, markDirty = false) =>\n set((s) => ({\n mapView: { ...s.mapView, ...view },\n isDirty: markDirty || s.isDirty,\n })),\n setMapGrid: (rows, cols) =>\n set((s) => {\n const clampedRows = clampGridDim(rows);\n const clampedCols = clampGridDim(cols);\n const desiredSecondary = clampedRows * clampedCols - 1;\n let secondaryMapViews = s.secondaryMapViews;\n if (desiredSecondary < secondaryMapViews.length) {\n secondaryMapViews = secondaryMapViews.slice(0, desiredSecondary);\n } else if (desiredSecondary > secondaryMapViews.length) {\n const additions: SecondaryMapView[] = [];\n for (let i = secondaryMapViews.length; i < desiredSecondary; i++) {\n // New panes start as a clone of the primary map's camera and (by\n // having no overrides) inherit its layer visibility, so the\n // comparison begins from the same view the user is looking at.\n additions.push({\n id: uuidv4(),\n view: { ...s.mapView },\n layerVisibility: {},\n });\n }\n secondaryMapViews = [...secondaryMapViews, ...additions];\n }\n return {\n mapLayout: {\n ...s.mapLayout,\n rows: clampedRows,\n cols: clampedCols,\n },\n secondaryMapViews,\n isDirty: true,\n };\n }),\n setSyncView: (syncView) =>\n set((s) => ({\n mapLayout: { ...s.mapLayout, syncView },\n isDirty: true,\n })),\n setSecondaryMapView: (id, view, markDirty = false) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n const merged = { ...pane.view, ...view };\n // Skip value-identical writes: a programmatic `applyView` (camera\n // sync, initial load) fires \"moveend\" too, so without this guard\n // each pane re-stores the same camera it was just given, churning a\n // new `secondaryMapViews` array and re-rendering every subscriber.\n if (sameCamera(pane.view, merged)) return pane;\n changed = true;\n return { ...pane, view: merged };\n });\n if (!changed) return s;\n return {\n secondaryMapViews,\n isDirty: markDirty || s.isDirty,\n };\n }),\n setSecondaryLayerVisibility: (id, layerId, visible) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n changed = true;\n return {\n ...pane,\n layerVisibility: { ...pane.layerVisibility, [layerId]: visible },\n };\n });\n if (!changed) return s;\n return { secondaryMapViews, isDirty: true };\n }),\n setPrimaryMapLabel: (label) => set({ primaryMapLabel: label, isDirty: true }),\n setPrimaryRenderer: (renderer) =>\n set((s) => (s.primaryRenderer === renderer ? s : { primaryRenderer: renderer, isDirty: true })),\n setSecondaryMapLabel: (id, label) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n changed = true;\n return { ...pane, label };\n });\n if (!changed) return s;\n return { secondaryMapViews, isDirty: true };\n }),\n setSecondaryViewKind: (id, viewKind) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n // Treat an absent viewKind as \"maplibre\" so switching a legacy pane\n // to maplibre is a no-op rather than a churned array.\n if ((pane.viewKind ?? \"maplibre\") === viewKind) return pane;\n changed = true;\n return { ...pane, viewKind };\n });\n if (!changed) return s;\n return { secondaryMapViews, isDirty: true };\n }),\n removeSecondaryMapView: (id) =>\n set((s) => {\n const secondaryMapViews = s.secondaryMapViews.filter((pane) => pane.id !== id);\n if (secondaryMapViews.length === s.secondaryMapViews.length) {\n return s;\n }\n // Collapse to a gap-free grid that fits the remaining panes, keeping\n // the layout's orientation as close as possible to the current one.\n const total = secondaryMapViews.length + 1;\n const { rows, cols } = fitGrid(total, s.mapLayout.cols);\n return {\n secondaryMapViews,\n mapLayout: { ...s.mapLayout, rows, cols },\n isDirty: true,\n };\n }),\n setBasemapStyleUrl: (url) =>\n set((state) => ({\n basemapStyleUrl: url,\n preferences: preferencesForBasemap(state),\n isDirty: true,\n })),\n applyPlanetaryBasemap: (basemap) =>\n set((state) => ({\n basemapStyleUrl: basemap.styleUrl,\n preferences: preferencesForBasemap(state, basemap.ellipsoidId),\n isDirty: true,\n })),\n restoreEarthBasemap: (styleUrl) =>\n set((state) => ({\n basemapStyleUrl: styleUrl,\n preferences: preferencesForBasemap(state, DEFAULT_ELLIPSOID_ID),\n isDirty: true,\n })),\n setBasemapVisible: (visible) => set({ basemapVisible: visible, isDirty: true }),\n setBasemapOpacity: (opacity) => set({ basemapOpacity: opacity, isDirty: true }),\n setBlankBackgroundColor: (color) =>\n set({ blankBackgroundColor: normalizeBlankBackgroundColor(color), isDirty: true }),\n setPreferences: (preferences) => set({ preferences, isDirty: true }),\n});\n","/**\n * Saved processing models (issue #344) and the Processing History log\n * (issue #1292). Both are saved with the project.\n */\nimport { MAX_PROCESSING_HISTORY, type ProcessingModel, type ProcessingRun } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface ProcessingSlice {\n /** Saved processing pipelines (batch/model chaining; issue #344). */\n models: ProcessingModel[];\n /** Recorded processing tool runs, oldest first (Processing History; #1292). */\n processingHistory: ProcessingRun[];\n\n /** Insert a new model or replace an existing one matching by `id`. */\n saveModel: (model: ProcessingModel) => void;\n /** Remove a saved model by id. */\n deleteModel: (id: string) => void;\n\n /** Append a processing run to the history (bounded, de-duped by id; #1292). */\n addProcessingRun: (run: ProcessingRun) => void;\n /** Patch a recorded run by id (no-op if absent), e.g. to add output layers. */\n updateProcessingRun: (id: string, patch: Partial<Omit<ProcessingRun, \"id\">>) => void;\n /** Drop all recorded processing runs. */\n clearProcessingHistory: () => void;\n}\n\nexport const createProcessingSlice: SliceCreator<ProcessingSlice> = (set) => ({\n models: [],\n processingHistory: [],\n\n saveModel: (model) =>\n set((s) => {\n const exists = s.models.some((m) => m.id === model.id);\n const models = exists\n ? s.models.map((m) => (m.id === model.id ? model : m))\n : [...s.models, model];\n return { models, isDirty: true };\n }),\n deleteModel: (id) =>\n set((s) => ({\n models: s.models.filter((m) => m.id !== id),\n isDirty: true,\n })),\n\n addProcessingRun: (run) =>\n set((s) => {\n // Ignore a duplicate id so updateProcessingRun stays unambiguous.\n if (s.processingHistory.some((r) => r.id === run.id)) return s;\n const processingHistory = [...s.processingHistory, run].slice(-MAX_PROCESSING_HISTORY);\n return { processingHistory, isDirty: true };\n }),\n updateProcessingRun: (id, patch) =>\n set((s) => {\n if (!s.processingHistory.some((r) => r.id === id)) return s;\n return {\n processingHistory: s.processingHistory.map((r) =>\n r.id === id ? { ...r, ...patch, id: r.id } : r,\n ),\n isDirty: true,\n };\n }),\n clearProcessingHistory: () =>\n set((s) => (s.processingHistory.length === 0 ? s : { processingHistory: [], isDirty: true })),\n});\n","/**\n * Project content that sits beside the layers: review comments (issue #1518),\n * Dashboard widgets (issue #401) and the story map. All of it is saved with the\n * project; `comments` and `storymap` are also tracked by undo history.\n */\nimport {\n DEFAULT_DASHBOARD_COLUMNS,\n DEFAULT_STORY_MAP,\n MAX_DASHBOARD_COLUMNS,\n MIN_DASHBOARD_COLUMNS,\n type CommentReply,\n type DashboardWidget,\n type ProjectComment,\n type ProjectInteraction,\n type StoryChapter,\n type StoryMap,\n} from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface ProjectContentSlice {\n storymap: StoryMap | null;\n /** Saved Dashboard panel chart widgets (issue #401). */\n widgets: DashboardWidget[];\n /** Number of columns in the Dashboard widget grid. */\n dashboardColumns: number;\n /** Anchored review comments on map points or features (issue #1518). */\n comments: ProjectComment[];\n /**\n * The loaded project's startup `interaction` block (issue #2688). Applied\n * once on load and written back unchanged on save; live Identify and control\n * changes do not edit it.\n */\n projectInteraction: ProjectInteraction | null;\n\n /** Append a new dashboard widget. */\n addWidget: (widget: DashboardWidget) => void;\n /** Patch an existing dashboard widget by id (no-op if absent). Merges, so an\n * omitted key keeps its current value; use replaceWidget to clear one. */\n updateWidget: (id: string, patch: Partial<Omit<DashboardWidget, \"id\">>) => void;\n /** Swap an existing dashboard widget for a complete new record, keeping its\n * id and position (no-op if absent). Unlike updateWidget this does not merge,\n * so fields the caller omits are cleared — what the widget editor needs to\n * persist an emptied title, color, prefix, or suffix. */\n replaceWidget: (id: string, widget: Omit<DashboardWidget, \"id\">) => void;\n /** Remove a dashboard widget by id. */\n removeWidget: (id: string) => void;\n /** Move a widget to a new index, clamped into range, preserving the rest. */\n moveWidget: (id: string, toIndex: number) => void;\n /** Set the Dashboard widget-grid column count (clamped into range). */\n setDashboardColumns: (columns: number) => void;\n\n setStorymap: (storymap: StoryMap | null) => void;\n updateStorymapSettings: (patch: Partial<Omit<StoryMap, \"chapters\">>) => void;\n addStoryChapter: (chapter: StoryChapter, atIndex?: number) => void;\n updateStoryChapter: (id: string, patch: Partial<StoryChapter>) => void;\n removeStoryChapter: (id: string) => void;\n moveStoryChapter: (id: string, targetIndex: number) => void;\n addComment: (comment: ProjectComment) => void;\n replyToComment: (commentId: string, reply: CommentReply) => void;\n toggleResolveComment: (commentId: string, resolved?: boolean) => void;\n deleteComment: (commentId: string) => void;\n setComments: (comments: ProjectComment[]) => void;\n}\n\nexport const createProjectContentSlice: SliceCreator<ProjectContentSlice> = (set) => ({\n storymap: null,\n widgets: [],\n dashboardColumns: DEFAULT_DASHBOARD_COLUMNS,\n comments: [],\n projectInteraction: null,\n\n addComment: (comment) =>\n set((s) => {\n // Ignore a duplicate id: the WebSocket relay echoes our own\n // comment-mutation back to the sender, and a reconnect can replay\n // recent history, so de-dupe defensively (mirrors addCollaborationChat).\n if (s.comments.some((c) => c.id === comment.id)) return s;\n return { comments: [...s.comments, comment], isDirty: true };\n }),\n replyToComment: (commentId, reply) =>\n set((s) => {\n let appended = false;\n const nextComments = s.comments.map((c) => {\n if (c.id !== commentId) return c;\n if (c.replies.some((r) => r.id === reply.id)) return c;\n appended = true;\n return { ...c, replies: [...c.replies, reply] };\n });\n if (!appended) return s;\n return { comments: nextComments, isDirty: true };\n }),\n toggleResolveComment: (commentId, resolved) =>\n set((s) => ({\n comments: s.comments.map((c) =>\n c.id === commentId\n ? { ...c, resolved: resolved !== undefined ? resolved : !c.resolved }\n : c,\n ),\n isDirty: true,\n })),\n deleteComment: (commentId) =>\n set((s) => ({\n comments: s.comments.filter((c) => c.id !== commentId),\n isDirty: true,\n })),\n setComments: (comments) => set({ comments, isDirty: true }),\n\n addWidget: (widget) =>\n set((s) => {\n // Ignore a duplicate id so updateWidget/removeWidget stay unambiguous\n // and a later entry isn't silently dropped when normalized on save.\n if (s.widgets.some((w) => w.id === widget.id)) return s;\n return { widgets: [...s.widgets, widget], isDirty: true };\n }),\n updateWidget: (id, patch) =>\n set((s) => {\n const exists = s.widgets.some((w) => w.id === id);\n if (!exists) return s;\n return {\n widgets: s.widgets.map((w) => (w.id === id ? { ...w, ...patch, id: w.id } : w)),\n isDirty: true,\n };\n }),\n replaceWidget: (id, widget) =>\n set((s) => {\n if (!s.widgets.some((w) => w.id === id)) return s;\n return {\n widgets: s.widgets.map((w) => (w.id === id ? { ...widget, id } : w)),\n isDirty: true,\n };\n }),\n removeWidget: (id) =>\n set((s) => ({\n widgets: s.widgets.filter((w) => w.id !== id),\n isDirty: true,\n })),\n moveWidget: (id, toIndex) =>\n set((s) => {\n const from = s.widgets.findIndex((w) => w.id === id);\n if (from < 0) return s;\n const target = Math.max(0, Math.min(s.widgets.length - 1, toIndex));\n if (target === from) return s;\n const widgets = [...s.widgets];\n const [moved] = widgets.splice(from, 1);\n widgets.splice(target, 0, moved);\n return { widgets, isDirty: true };\n }),\n setDashboardColumns: (columns) =>\n set((s) => {\n // Guard non-finite input so the clamp can't yield NaN columns.\n if (!Number.isFinite(columns)) return s;\n return {\n dashboardColumns: Math.max(\n MIN_DASHBOARD_COLUMNS,\n Math.min(MAX_DASHBOARD_COLUMNS, Math.trunc(columns)),\n ),\n isDirty: true,\n };\n }),\n\n setStorymap: (storymap) => set({ storymap, isDirty: true }),\n updateStorymapSettings: (patch) =>\n set((s) => ({\n storymap: { ...(s.storymap ?? DEFAULT_STORY_MAP), ...patch },\n isDirty: true,\n })),\n addStoryChapter: (chapter, atIndex) =>\n set((s) => {\n const base = s.storymap ?? DEFAULT_STORY_MAP;\n const chapters = [...base.chapters];\n const index =\n atIndex === undefined ? chapters.length : Math.min(Math.max(atIndex, 0), chapters.length);\n chapters.splice(index, 0, chapter);\n return { storymap: { ...base, chapters }, isDirty: true };\n }),\n updateStoryChapter: (id, patch) =>\n set((s) => {\n if (!s.storymap) return s;\n return {\n storymap: {\n ...s.storymap,\n chapters: s.storymap.chapters.map((chapter) =>\n chapter.id === id ? { ...chapter, ...patch } : chapter,\n ),\n },\n isDirty: true,\n };\n }),\n removeStoryChapter: (id) =>\n set((s) => {\n if (!s.storymap) return s;\n return {\n storymap: {\n ...s.storymap,\n chapters: s.storymap.chapters.filter((chapter) => chapter.id !== id),\n },\n isDirty: true,\n };\n }),\n moveStoryChapter: (id, targetIndex) =>\n set((s) => {\n if (!s.storymap) return s;\n const current = s.storymap.chapters.findIndex((chapter) => chapter.id === id);\n if (current < 0) return s;\n const chapters = [...s.storymap.chapters];\n const [chapter] = chapters.splice(current, 1);\n if (!chapter) return s;\n const next = Math.min(Math.max(targetIndex, 0), chapters.length);\n chapters.splice(next, 0, chapter);\n if (chapters.every((item, i) => item.id === s.storymap?.chapters[i]?.id)) {\n return s;\n }\n return { storymap: { ...s.storymap, chapters }, isDirty: true };\n }),\n});\n","/**\n * Runtime-settable window (ms) used to coalesce rapid history-producing changes\n * (e.g. a continuous opacity/style slider drag) into a single undo entry. Tests\n * set this to 0 for deterministic one-entry-per-action behavior.\n */\nlet historyCoalesceMs = 400;\n\nexport function setHistoryCoalesceMs(ms: number): void {\n historyCoalesceMs = ms;\n}\n\nexport function getHistoryCoalesceMs(): number {\n return historyCoalesceMs;\n}\n\n/**\n * Soft budget (in total feature count) for the geometry/attribute payload\n * retained across undo snapshots. Each in-app edit of a vector layer pushes a\n * new snapshot that holds the layer's full `geojson`; without a bound, editing\n * a large layer many times pins several copies of its feature set in memory\n * (issue #341). When the distinct features across snapshots exceed this budget,\n * the oldest snapshots are dropped (see {@link trimHistoryBySize}).\n *\n * Feature count is a cheap proxy for payload size; it avoids serializing\n * geometry on every edit. The default is generous enough that ordinary\n * multi-step editing of small/medium layers keeps the full history depth, while\n * very large layers trade history depth for a bounded footprint.\n */\nlet maxHistoryFeatureCount = 500_000;\n\nexport function setMaxHistoryFeatureCount(count: number): void {\n if (!Number.isFinite(count) || count < 0) {\n throw new RangeError(\n `maxHistoryFeatureCount must be a non-negative finite number, got ${count}`,\n );\n }\n maxHistoryFeatureCount = count;\n}\n\nexport function getMaxHistoryFeatureCount(): number {\n return maxHistoryFeatureCount;\n}\n\n/** Structural shape of a partialized undo snapshot, for size accounting. */\ninterface HistorySnapshot {\n layers?: {\n geojson?: { features?: unknown[] } | null;\n source?: { url?: unknown } | null;\n }[];\n}\n\n/**\n * Bytes of inline payload charged as one feature-equivalent, so a `data:` URL\n * competes for the same budget as geometry. A rough vector feature costs a few\n * hundred bytes of live JS objects, which is the order this matches.\n */\nconst BYTES_PER_FEATURE_EQUIVALENT = 200;\n\n/**\n * The size a snapshot adds to the budget, skipping any payload already in\n * `seen` so payloads shared across snapshots (unchanged layers keep the same\n * reference) are counted once. Mutates `seen` with each newly counted payload.\n *\n * Two payload kinds count. Vector `geojson` contributes its feature count. A\n * layer whose `source.url` is an inline `data:` URL contributes its length as\n * feature-equivalents: a NetCDF grid baked to pixels holds a whole PNG there,\n * and re-symbolizing it writes a fresh multi-megabyte string on every change, so\n * without this the pruner sees a \"free\" snapshot and history grows unbounded.\n */\nfunction distinctFeatureCount(snapshot: HistorySnapshot, seen: Set<unknown>): number {\n let count = 0;\n for (const layer of snapshot.layers ?? []) {\n const geojson = layer?.geojson;\n // Dedup the reference first so a payload shared across snapshots is visited\n // once even when its `features` is missing/malformed (it then contributes 0).\n if (geojson && !seen.has(geojson)) {\n seen.add(geojson);\n if (Array.isArray(geojson.features)) count += geojson.features.length;\n }\n const url = layer?.source?.url;\n // Strings have no stable identity to dedup by reference, so `seen` holds the\n // value itself; an unchanged layer repeats the same URL across snapshots and\n // must be charged once, exactly as a shared feature payload is.\n if (typeof url === \"string\" && url.startsWith(\"data:\") && !seen.has(url)) {\n seen.add(url);\n count += Math.ceil(url.length / BYTES_PER_FEATURE_EQUIVALENT);\n }\n }\n return count;\n}\n\n/**\n * Bound the memory held by undo history by dropping the oldest snapshots whose\n * combined feature payload exceeds `maxFeatures`.\n *\n * Walks newest-to-oldest and keeps the most recent snapshots whose cumulative\n * distinct feature count fits the budget. The single newest snapshot is always\n * kept regardless of size, so one-step undo of even a huge edit still works.\n * Feature sets shared by reference across snapshots (unchanged layers) are\n * counted once, so retaining many snapshots of small layers stays cheap.\n *\n * Returns the original array when nothing needs trimming, or a trimmed slice\n * (oldest entries removed) otherwise. Pure: never mutates its input.\n */\nexport function trimHistoryBySize<T extends HistorySnapshot>(\n pastStates: T[],\n maxFeatures: number,\n): T[] {\n if (!Number.isFinite(maxFeatures) || maxFeatures < 0) {\n throw new RangeError(`maxFeatures must be a non-negative finite number, got ${maxFeatures}`);\n }\n if (pastStates.length <= 1) return pastStates;\n const seen = new Set<unknown>();\n const lastIndex = pastStates.length - 1;\n // The newest snapshot is always retained, even if it alone exceeds the budget.\n let total = distinctFeatureCount(pastStates[lastIndex], seen);\n let keepFrom = lastIndex;\n for (let i = lastIndex - 1; i >= 0; i--) {\n const added = distinctFeatureCount(pastStates[i], seen);\n if (total + added > maxFeatures) break;\n total += added;\n keepFrom = i;\n }\n return keepFrom === 0 ? pastStates : pastStates.slice(keepFrom);\n}\n\n/** A debounced function with a `cancel` to clear its in-flight burst window. */\nexport type DebouncedFn<A extends unknown[]> = ((...args: A) => void) & {\n /** Clear the active burst timer so the next call fires on its leading edge. */\n cancel: () => void;\n};\n\n/**\n * Leading-edge debounce. Fires `fn` immediately on the first call of a burst,\n * then suppresses further calls until `getWait()` ms of quiet have elapsed.\n * When the wait is <= 0, every call is passed straight through (used in tests).\n *\n * Used as zundo's `handleSet`: `fn` is zundo's \"save previous state to history\"\n * function, so firing only on the leading edge records the pre-burst state once.\n * `cancel()` resets the window so that clearing history mid-burst doesn't\n * suppress the next edit's save.\n */\nexport function leadingDebounce<A extends unknown[]>(\n fn: (...args: A) => void,\n getWait: () => number,\n): DebouncedFn<A> {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const wrapped = (...args: A) => {\n const wait = getWait();\n if (wait <= 0) {\n fn(...args);\n return;\n }\n const atBurstStart = timer === null;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n }, wait);\n if (atBurstStart) fn(...args);\n };\n wrapped.cancel = () => {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n };\n return wrapped;\n}\n","/**\n * Undo/redo for the app store. zundo's `temporal` middleware tracks only the\n * project-data fields that {@link partializeHistory} lists (layers, groups,\n * basemap, story map, comments); every other field, including the whole `ui`\n * dialog sub-state, selection and the camera, is outside the history, so\n * changing it never records an undo step and undo/redo never reverts it.\n *\n * The functions here act on the store bound by {@link bindHistoryStore}\n * rather than importing it, so the slices (whose `newProject`/`loadProject`\n * call {@link clearHistory}) can depend on this module without a cycle.\n */\nimport { shallow } from \"zustand/shallow\";\nimport type { StoreApi } from \"zustand\";\nimport type { TemporalState, ZundoOptions } from \"zundo\";\nimport { DEFAULT_ELLIPSOID_ID, getPlanetaryBasemapByStyleUrl } from \"../ellipsoids\";\nimport {\n getHistoryCoalesceMs,\n getMaxHistoryFeatureCount,\n leadingDebounce,\n trimHistoryBySize,\n} from \"../history\";\nimport type { GeoLibreProject, LayerGroup } from \"../types\";\nimport type { AppState } from \"./types\";\n\n/** The fields undo/redo snapshots, restores and compares. */\nexport type HistoryState = Pick<\n AppState,\n | \"layers\"\n | \"layerGroups\"\n | \"basemapStyleUrl\"\n | \"basemapVisible\"\n | \"basemapOpacity\"\n | \"blankBackgroundColor\"\n | \"storymap\"\n | \"comments\"\n>;\n\n/** The parts of the bound store the history functions use. */\ninterface HistoryStore {\n getState: () => AppState;\n setState: (partial: Partial<AppState>) => void;\n temporal: StoreApi<TemporalState<HistoryState>>;\n}\n\nlet historyStore: HistoryStore | null = null;\n\n/** Point the history functions at the app store; called once by `store.ts`. */\nexport function bindHistoryStore(store: HistoryStore): void {\n historyStore = store;\n}\n\nfunction boundStore(): HistoryStore {\n if (!historyStore) throw new Error(\"The app store has not been created yet.\");\n return historyStore;\n}\n\n/**\n * Compare two `layerGroups` arrays for undo-history purposes, ignoring the\n * `collapsed` flag so expand/collapse (a UI-panel preference) never records a\n * history entry. Every other field — order, name, visibility, opacity — is\n * still compared, so real edits are tracked.\n */\nfunction layerGroupsEqualForHistory(a: LayerGroup[], b: LayerGroup[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n const x = a[i];\n const y = b[i];\n if (x === y) continue;\n if (x.id !== y.id || x.name !== y.name || x.visible !== y.visible || x.opacity !== y.opacity) {\n return false;\n }\n }\n return true;\n}\n\n/** Cancels the active history coalesce window (assigned by zundo's handleSet). */\nlet cancelHistoryCoalesce: () => void = () => {};\n\ninterface ProjectRestoreHistoryEntry {\n before: GeoLibreProject;\n beforePath: string | null;\n after: GeoLibreProject;\n afterPath: string | null;\n}\n\nlet projectRestoreUndo: ProjectRestoreHistoryEntry | null = null;\nlet projectRestoreRedo: ProjectRestoreHistoryEntry | null = null;\nlet applyingProjectRestoreHistory = false;\nconst projectRestoreHistoryListeners = new Set<() => void>();\n\nfunction notifyProjectRestoreHistory(): void {\n projectRestoreHistoryListeners.forEach((listener) => listener());\n}\n\nexport function subscribeProjectRestoreHistory(listener: () => void): () => void {\n projectRestoreHistoryListeners.add(listener);\n return () => projectRestoreHistoryListeners.delete(listener);\n}\n\nexport function canUndoProjectRestore(): boolean {\n return projectRestoreUndo !== null;\n}\n\nexport function canRedoProjectRestore(): boolean {\n return projectRestoreRedo !== null;\n}\n\n/**\n * Register a whole-project restore as one undoable operation. The regular\n * temporal history intentionally tracks only editing fields, while restoring a\n * history snapshot also changes project metadata, camera, plugins, and other\n * serialized state, so it needs a full canonical project pair.\n */\nexport function registerProjectRestoreHistory(\n before: GeoLibreProject,\n beforePath: string | null,\n after: GeoLibreProject,\n afterPath: string | null = null,\n): void {\n projectRestoreUndo = { before, beforePath, after, afterPath };\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\n}\n\n/**\n * Drop the oldest undo snapshots once their combined feature payload exceeds the\n * configured budget, bounding the memory held by history when a large vector\n * layer is edited repeatedly (issue #341). Called after a snapshot is appended.\n * Operates on the live temporal store directly; this never touches the main\n * store, so it does not itself record a history entry.\n */\nfunction pruneHistoryBySize(): void {\n const temporalStore = boundStore().temporal;\n const { pastStates } = temporalStore.getState();\n const trimmed = trimHistoryBySize(pastStates, getMaxHistoryFeatureCount());\n if (trimmed.length !== pastStates.length) {\n temporalStore.setState({ pastStates: trimmed });\n }\n}\n\n/**\n * Only these fields participate in undo/redo; everything else (selection,\n * ui flags, mapView/camera, pointerCoords, project metadata, isDirty, ...)\n * is excluded, so changing them never creates a history entry.\n */\nexport function partializeHistory(s: AppState): HistoryState {\n return {\n layers: s.layers,\n layerGroups: s.layerGroups,\n basemapStyleUrl: s.basemapStyleUrl,\n basemapVisible: s.basemapVisible,\n basemapOpacity: s.basemapOpacity,\n blankBackgroundColor: s.blankBackgroundColor,\n storymap: s.storymap,\n comments: s.comments,\n };\n}\n\n/** zundo options for the app store's undo history. */\nexport function createHistoryOptions(): ZundoOptions<AppState, HistoryState> {\n return {\n partialize: partializeHistory,\n // Records a history entry only when the tracked slice really changed.\n // Basemap fields compare with ===; `layers` is compared element-by-element\n // (Object.is per element) via shallow. Every mutating action creates new\n // layer/group objects, so real changes differ; two distinct empty arrays\n // compare equal, so resetting them (e.g. newProject) records nothing.\n // `storymap` is compared by reference: every authoring action creates a\n // new object, so real edits differ while an unchanged null stays equal.\n // `layerGroups` is compared ignoring `collapsed`, which is a UI preference\n // excluded from undo (see toggleLayerGroupCollapsed).\n // `comments` is compared shallowly by reference.\n equality: (a, b) =>\n a.basemapStyleUrl === b.basemapStyleUrl &&\n a.basemapVisible === b.basemapVisible &&\n a.basemapOpacity === b.basemapOpacity &&\n a.blankBackgroundColor === b.blankBackgroundColor &&\n a.storymap === b.storymap &&\n shallow(a.layers, b.layers) &&\n shallow(a.comments, b.comments) &&\n layerGroupsEqualForHistory(a.layerGroups, b.layerGroups),\n limit: 100,\n // Group rapid bursts (slider drags) into one entry; window is 0 in tests.\n // Keep the debounced wrapper so clearHistory can reset an in-flight burst.\n handleSet: (baseHandleSet) => {\n const debounced = leadingDebounce(baseHandleSet, getHistoryCoalesceMs);\n cancelHistoryCoalesce = debounced.cancel;\n // Trim history back under the feature-payload budget so editing large\n // layers can't pin unbounded copies of their feature sets in memory\n // (issue #341). Only the leading-edge call actually pushes a snapshot;\n // burst-suppressed calls leave `pastStates` untouched, so skip the scan\n // unless a new snapshot was appended.\n return (...args: Parameters<typeof debounced>) => {\n const before = boundStore().temporal.getState().pastStates;\n debounced(...args);\n if (boundStore().temporal.getState().pastStates !== before) {\n if (!applyingProjectRestoreHistory && projectRestoreRedo) {\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\n }\n pruneHistoryBySize();\n }\n };\n },\n };\n}\n\n/**\n * After an undo/redo restores the tracked slice, mark the project dirty and\n * drop a `selectedLayerId` that no longer points at an existing layer (selection\n * is intentionally not tracked in history, so it can dangle after a restore).\n */\nfunction finishHistoryStep(previousBasemapStyleUrl: string): void {\n const s = boundStore().getState();\n const selectionDangling =\n s.selectedLayerId !== null && !s.layers.some((layer) => layer.id === s.selectedLayerId);\n // The basemap is in the undo history but the ellipsoid preference is not, so a\n // step that restores a *different* basemap can leave the two out of sync (e.g.\n // undoing a switch to Mars would keep the Mars radius under an Earth basemap).\n // Re-derive the ellipsoid from the restored basemap's body — Earth for a\n // non-planetary basemap — but only when this step actually changed the\n // basemap. Steps that leave the basemap untouched must not touch the ellipsoid,\n // which the user can set independently of the basemap in Settings.\n const restoredEllipsoidId =\n getPlanetaryBasemapByStyleUrl(s.basemapStyleUrl)?.ellipsoidId ?? DEFAULT_ELLIPSOID_ID;\n const ellipsoidPatch =\n s.basemapStyleUrl !== previousBasemapStyleUrl &&\n s.preferences.map.ellipsoidId !== restoredEllipsoidId\n ? {\n preferences: {\n ...s.preferences,\n map: { ...s.preferences.map, ellipsoidId: restoredEllipsoidId },\n },\n }\n : {};\n boundStore().setState(\n selectionDangling\n ? {\n isDirty: true,\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n ...ellipsoidPatch,\n }\n : { isDirty: true, ...ellipsoidPatch },\n );\n // The setState above must not leave a coalesce window open for the next edit.\n cancelHistoryCoalesce();\n}\n\n/**\n * Step the layer/basemap history back one entry and mark the project dirty.\n * zundo restores the partialized slice via the store's set; the resulting new\n * `layers`/basemap refs drive MapCanvas's existing effects, so the map\n * reconciles through MapController.syncLayers (never mutated directly here).\n */\nexport function undo(): void {\n const temporal = boundStore().temporal.getState();\n if (temporal.pastStates.length === 0 && projectRestoreUndo) {\n const entry = projectRestoreUndo;\n applyingProjectRestoreHistory = true;\n try {\n boundStore().getState().loadProject(entry.before, entry.beforePath, {\n rememberRecent: false,\n presenting: false,\n });\n projectRestoreUndo = null;\n boundStore().setState({ isDirty: true });\n projectRestoreRedo = entry;\n notifyProjectRestoreHistory();\n } catch (error) {\n console.error(\"Could not undo the project snapshot restore.\", error);\n } finally {\n applyingProjectRestoreHistory = false;\n }\n return;\n }\n if (temporal.pastStates.length === 0) return; // nothing to undo; stay clean\n cancelHistoryCoalesce(); // break any in-flight burst so the next edit records\n const previousBasemapStyleUrl = boundStore().getState().basemapStyleUrl;\n temporal.undo();\n finishHistoryStep(previousBasemapStyleUrl);\n}\n\n/** Step the history forward one entry and mark the project dirty. */\nexport function redo(): void {\n const temporal = boundStore().temporal.getState();\n if (temporal.futureStates.length === 0 && projectRestoreRedo) {\n const entry = projectRestoreRedo;\n applyingProjectRestoreHistory = true;\n try {\n boundStore().getState().loadProject(entry.after, entry.afterPath, {\n rememberRecent: false,\n presenting: false,\n });\n projectRestoreRedo = null;\n boundStore().setState({ isDirty: true });\n projectRestoreUndo = entry;\n notifyProjectRestoreHistory();\n } catch (error) {\n console.error(\"Could not redo the project snapshot restore.\", error);\n } finally {\n applyingProjectRestoreHistory = false;\n }\n return;\n }\n if (temporal.futureStates.length === 0) return; // nothing to redo; stay clean\n cancelHistoryCoalesce(); // break any in-flight burst so the next edit records\n const previousBasemapStyleUrl = boundStore().getState().basemapStyleUrl;\n temporal.redo();\n finishHistoryStep(previousBasemapStyleUrl);\n}\n\n/** Empty both the undo and redo stacks (e.g. on new/loaded project). */\nexport function clearHistory(): void {\n cancelHistoryCoalesce(); // reset any in-flight burst so the next edit records\n boundStore().temporal.getState().clear();\n if (!applyingProjectRestoreHistory) {\n projectRestoreUndo = null;\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\n }\n}\n","/**\n * Project identity and lifecycle: name, path, dirty flag, recent projects, and\n * the project-level settings that have no richer home (plugins, legend, print\n * layout, metadata). `newProject` and `loadProject` replace the whole document\n * (every slice's project fields) and clear undo history.\n */\nimport {\n applyProjectToStore,\n type CreateProjectOptions,\n createEmptyProject,\n DEFAULT_PROJECT_NAME,\n} from \"../project\";\nimport {\n createDefaultPrintLayout,\n printLayoutConfigsEqual,\n type PrintLayoutConfig,\n} from \"../print-layout-config\";\nimport { reapplyLayerJoins } from \"../joins\";\nimport {\n DEFAULT_LEGEND_CONFIG,\n type GeoLibreProject,\n type LegendConfig,\n type ProjectPluginState,\n type RecentProjectEntry,\n} from \"../types\";\nimport { resolveIdentifyTarget } from \"./session-slice\";\nimport type { SliceCreator } from \"./types\";\nimport { clearHistory } from \"./undo-history\";\n\nconst MAX_RECENT_PROJECTS = 10;\n\n/** Derive a human-friendly display name from a file path or URL. */\nexport function projectPathLabel(path: string): string {\n return path.split(/[/\\\\]/).pop() || path;\n}\n\nfunction normalizeRecentProjects(projects: RecentProjectEntry[]): RecentProjectEntry[] {\n const seen = new Set<string>();\n const normalized: RecentProjectEntry[] = [];\n\n for (const project of projects) {\n const path = project.path.trim();\n if (!path || seen.has(path)) continue;\n\n const name = project.name.trim() || projectPathLabel(path);\n normalized.push({\n path,\n name,\n openedAt: project.openedAt || new Date().toISOString(),\n });\n seen.add(path);\n }\n\n return normalized.slice(0, MAX_RECENT_PROJECTS);\n}\n\nexport interface ProjectSlice {\n projectName: string;\n projectPath: string | null;\n projectGeneration: number;\n isDirty: boolean;\n projectPlugins: ProjectPluginState | null;\n legend: LegendConfig;\n /** Print Layout composer settings for the open project (discussion #1992). */\n printLayout: PrintLayoutConfig;\n metadata: Record<string, unknown>;\n recentProjects: RecentProjectEntry[];\n\n setLegend: (legend: LegendConfig) => void;\n /**\n * Replace the Print Layout composer settings. A config equal to the current\n * one is ignored, so re-opening the composer (or a project load seeding the\n * dialog) never marks the project dirty.\n */\n setPrintLayout: (printLayout: PrintLayoutConfig) => void;\n setProjectPlugins: (projectPlugins: ProjectPluginState | null, shouldMarkDirty?: boolean) => void;\n newProject: (options?: CreateProjectOptions & { name?: string }) => void;\n loadProject: (\n project: GeoLibreProject,\n path?: string | null,\n options?: { rememberRecent?: boolean; presenting?: boolean },\n ) => void;\n setProjectPath: (path: string | null) => void;\n setProjectName: (name: string) => void;\n setRecentProjects: (projects: RecentProjectEntry[]) => void;\n rememberRecentProject: (entry: RecentProjectEntry) => void;\n forgetRecentProject: (path: string) => void;\n clearRecentProjects: () => void;\n markSaved: () => void;\n}\n\nexport const createProjectSlice: SliceCreator<ProjectSlice> = (set, get) => ({\n projectName: DEFAULT_PROJECT_NAME,\n projectPath: null,\n projectGeneration: 0,\n isDirty: false,\n projectPlugins: null,\n legend: { ...DEFAULT_LEGEND_CONFIG },\n printLayout: createDefaultPrintLayout(),\n metadata: {},\n recentProjects: [],\n\n setLegend: (legend) => set({ legend, isDirty: true }),\n\n setPrintLayout: (printLayout) =>\n set((s) =>\n printLayoutConfigsEqual(s.printLayout, printLayout) ? s : { printLayout, isDirty: true },\n ),\n // When shouldMarkDirty is false the existing dirty flag is preserved rather\n // than set; it cannot clear the flag (only markSaved() does that).\n setProjectPlugins: (projectPlugins, shouldMarkDirty = true) =>\n set((s) => ({\n projectPlugins,\n isDirty: shouldMarkDirty || s.isDirty,\n })),\n\n setProjectPath: (path) => set({ projectPath: path }),\n setProjectName: (name) => set({ projectName: name, isDirty: true }),\n setRecentProjects: (projects) => set({ recentProjects: normalizeRecentProjects(projects) }),\n rememberRecentProject: (entry) =>\n set((s) => ({\n recentProjects: normalizeRecentProjects([entry, ...s.recentProjects]),\n })),\n forgetRecentProject: (path) => {\n // Compare with separators normalized so a backslash/forward-slash mismatch\n // on Windows does not leave a stale entry behind.\n const normalized = path.replace(/\\\\/g, \"/\");\n set((s) => ({\n recentProjects: s.recentProjects.filter(\n (project) => project.path.replace(/\\\\/g, \"/\") !== normalized,\n ),\n }));\n },\n clearRecentProjects: () => set({ recentProjects: [] }),\n markSaved: () => set({ isDirty: false }),\n\n newProject: (options = {}) => {\n const project = createEmptyProject(options.name, options);\n const applied = applyProjectToStore(project);\n set((s) => ({\n ...applied,\n hoverTooltipsEnabled: true,\n projectPath: null,\n projectGeneration: s.projectGeneration + 1,\n isDirty: false,\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n identifyLayerId: null,\n identifyLayerIds: null,\n // The copied style names a layer from the previous project, so a\n // paste in the new one would apply an orphaned entry.\n copiedLayerStyle: null,\n pointerCoords: null,\n pointerElevation: null,\n cameraAltitude: null,\n attributeFilter: \"\",\n // Don't carry an active story presentation into a different project.\n ui: {\n ...s.ui,\n storymapPresenting: false,\n storymapReturnToEditor: false,\n storymapLayerOpacity: {},\n storymapPanelOpen: false,\n storymapComposingId: null,\n // An open selection dialog (and its preselected layer id) belongs\n // to the previous project's layers.\n selectByExpressionOpen: false,\n selectByExpressionLayerId: null,\n selectByLocationOpen: false,\n selectByLocationLayerId: null,\n loadEditorFeaturesOpen: false,\n loadEditorFeaturesLayerId: null,\n // A pending assistant-requested Model Builder load names a model in\n // the previous project's `savedModels`.\n modelBuilderRequestedModelId: null,\n },\n }));\n clearHistory();\n },\n\n loadProject: (project, path = null, options = {}) => {\n const applied = applyProjectToStore(project);\n // Re-resolve persistent attribute joins against the loaded layer set,\n // so joined columns reflect the join tables as saved (and a stale\n // saved copy of the joined output self-heals).\n applied.layers = reapplyLayerJoins(applied.layers);\n // A project that ships a story map opens straight into the presentation\n // so the reader sees the story, not the editor. Projects without a story\n // (or with an empty one) open normally. Callers that open a project for\n // authoring rather than viewing can pass `presenting: false` to override.\n const presentStory = options.presenting ?? (applied.storymap?.chapters.length ?? 0) > 0;\n const selectedLayerId =\n project.selectedLayerId === null\n ? null\n : typeof project.selectedLayerId === \"string\" &&\n applied.layers.some((layer) => layer.id === project.selectedLayerId)\n ? project.selectedLayerId\n : (applied.layers[0]?.id ?? null);\n set((s) => ({\n ...applied,\n hoverTooltipsEnabled: true,\n projectPath: path,\n projectGeneration: s.projectGeneration + 1,\n isDirty: false,\n selectedLayerId,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n // A project that saved an Identify target (issue #2688) opens with it\n // armed; any other load disarms Identify, since the previous target\n // named a layer of the project being replaced.\n ...resolveIdentifyTarget(\n applied.projectInteraction?.identify,\n applied.layers.map((layer) => layer.id),\n ),\n // The copied style names a layer from the previous project, so a\n // paste in the loaded one would apply an orphaned entry.\n copiedLayerStyle: null,\n // Ephemeral readouts describe the previous project's map. The\n // elevation and altitude especially: a project that switches to a\n // planetary body would otherwise keep showing Earth-scaled values\n // until the next hover or camera move.\n pointerCoords: null,\n pointerElevation: null,\n cameraAltitude: null,\n // Present a bundled story on load; otherwise drop any presentation\n // carried over from the previous project.\n ui: {\n ...s.ui,\n storymapPresenting: presentStory,\n // A bundled story auto-presents for viewing, so exiting it should\n // not pop open the editor (#918).\n storymapReturnToEditor: false,\n storymapLayerOpacity: {},\n storymapPanelOpen: false,\n storymapComposingId: null,\n // An open selection dialog (and its preselected layer id) belongs\n // to the previous project's layers.\n selectByExpressionOpen: false,\n selectByExpressionLayerId: null,\n selectByLocationOpen: false,\n selectByLocationLayerId: null,\n loadEditorFeaturesOpen: false,\n loadEditorFeaturesLayerId: null,\n // A pending assistant-requested Model Builder load names a model in\n // the previous project's `savedModels`.\n modelBuilderRequestedModelId: null,\n },\n }));\n clearHistory();\n if (path && options.rememberRecent !== false) {\n get().rememberRecentProject({\n path,\n name: project.name,\n openedAt: new Date().toISOString(),\n });\n }\n },\n});\n","/**\n * UI-only state: which dialogs and panels are open, and the one-shot requests\n * they consume. Lives in the `ui` sub-state, which the undo history never\n * tracks (see `undo-history.ts`), so opening or closing a dialog never records\n * an undo step and undo/redo never toggles one.\n */\nimport type { ProcessingRerunRequest } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport type ConversionToolKind =\n | \"vector-to-vector\"\n | \"vector-to-geoparquet\"\n | \"vector-to-flatgeobuf\"\n | \"vector-to-shapefile\"\n | \"vector-to-geopackage\"\n | \"csv-to-geoparquet\"\n | \"vector-to-pmtiles\"\n | \"raster-to-pmtiles\"\n | \"raster-to-cog\";\n\n/**\n * Identifiers of the vector processing tools. Kept in sync by hand with the\n * `id` fields of `VECTOR_TOOLS` in `@geolibre/processing` (`vector-tools.ts`);\n * deriving the type there would create a core -> processing circular import.\n */\nexport type VectorToolKind =\n | \"buffer\"\n | \"centroids\"\n | \"convex-hull\"\n | \"dissolve\"\n | \"bounding-box\"\n | \"simplify\"\n | \"clip\"\n | \"intersection\"\n | \"difference\"\n | \"union\"\n | \"spatial-join\"\n | \"attribute-join\"\n | \"select-by-value\"\n | \"select-by-location\"\n | \"random-extract\"\n | \"reproject\"\n | \"explode\"\n | \"aggregate\"\n | \"smooth\"\n | \"extract-vertices\"\n | \"points-along-geometry\"\n | \"grid\"\n | \"voronoi\"\n | \"cell-sectors\"\n | \"dggs-grid\"\n | \"dggs-bin\"\n | \"dggs-compact\"\n | \"trajectory-speed\"\n | \"detect-stops\"\n | \"space-time-proximity\"\n | \"decode-polyline\"\n | \"encode-polyline\"\n | \"merge-layers\"\n | \"check-validity\"\n | \"fix-geometries\"\n | \"check-topology-rules\"\n | \"fix-topology\";\n\n/** Identifiers of the network-analysis tools (`NETWORK_TOOLS` ids). */\nexport type NetworkToolKind = \"isochrone\" | \"od-matrix\" | \"sequential-route\";\n\n/** Identifiers of the spatial-statistics tools (`STATISTICS_TOOLS` ids). */\nexport type StatisticsToolKind =\n | \"global-morans-i\"\n | \"local-morans-i\"\n | \"getis-ord-gi\"\n | \"average-nearest-neighbor\"\n | \"kernel-density\"\n | \"emerging-hot-spot\"\n | \"composite-score\";\n\n/**\n * Identifiers of the raster processing tools. Kept in sync by hand with the\n * `id` fields of `RASTER_TOOLS` in `@geolibre/processing` (`raster-tools.ts`);\n * deriving the type there would create a core -> processing circular import.\n */\nexport type RasterToolKind =\n | \"hillshade\"\n | \"slope\"\n | \"aspect\"\n | \"reproject\"\n | \"resample\"\n | \"clip-extent\"\n | \"clip-mask\"\n | \"polygonize\"\n | \"contour\"\n | \"interpolate\"\n | \"zonal\"\n | \"raster-calc\"\n | \"spectral-index\"\n | \"reclassify\"\n | \"mosaic\"\n | \"focal\";\n\nexport interface UiSlice {\n ui: {\n processingOpen: boolean;\n /**\n * Tool id to preselect when the Whitebox toolbox dialog opens, set when the\n * user picks a specific tool from the Processing menu's category submenus.\n * Consumed and cleared by ProcessingDialog. Null means \"no preselection\".\n */\n processingInitialTool: string | null;\n conversionOpen: ConversionToolKind | null;\n vectorToolOpen: VectorToolKind | null;\n networkToolOpen: NetworkToolKind | null;\n statisticsToolOpen: StatisticsToolKind | null;\n rasterToolOpen: RasterToolKind | null;\n segmentationOpen: boolean;\n objectDetectionOpen: boolean;\n segmentEverythingOpen: boolean;\n geocodeOpen: boolean;\n sqlWorkspaceOpen: boolean;\n loadEditorFeaturesOpen: boolean;\n // Store layer preselected in the \"Load Features into Editor\" dialog when it\n // is opened from a layer's context menu, or null when opened without a target.\n loadEditorFeaturesLayerId: string | null;\n pythonConsoleOpen: boolean;\n notebookOpen: boolean;\n assistantOpen: boolean;\n attributeTableOpen: boolean;\n /**\n * Pending \"open the attribute table in edit mode\" request for this layer,\n * written by Identify's Edit attributes action (#2932). The table consumes\n * and clears it. Null when idle.\n */\n attributeTableEditLayerId: string | null;\n /** Whether the Raster Attribute Table bottom panel is open (issue #1307). */\n rasterAttributeTableOpen: boolean;\n dashboardOpen: boolean;\n storymapPanelOpen: boolean;\n storymapPresenting: boolean;\n // True when the active presentation was launched from the editor, so exiting\n // it reopens the Story Map editor instead of dropping to the bare map\n // (#918). Auto-presented projects (opened for viewing) leave this false.\n storymapReturnToEditor: boolean;\n /**\n * Layer opacities the active story presentation has applied so far, keyed\n * by store layer id. Playback fades layers by writing MapLibre paint\n * properties directly (never the persisted `layers[].opacity`), so this is\n * how renderers outside MapLibre's paint model (deck.gl diagrams, 3D\n * Z-value geometry) and the on-map Legend follow a chapter's fades. Empty\n * while not presenting; never saved with the project.\n */\n storymapLayerOpacity: Record<string, number>;\n // Id of the chapter currently being composed on the live map. When set, the\n // Story Map dialog is hidden so the user can pan/zoom/tilt the real map and\n // save the resulting camera back into this chapter (issue #775).\n storymapComposingId: string | null;\n /** The Batch tools dialog (run one tool across many layers). */\n batchToolsOpen: boolean;\n /** The Model Builder canvas panel (author a processing graph). */\n modelBuilderOpen: boolean;\n /** One-shot request for Model Builder to load a saved model. */\n modelBuilderRequestedModelId: string | null;\n /** Style Manager dialog visibility (issue #1294). */\n styleManagerOpen: boolean;\n /** Processing History panel visibility (#1292). */\n processingHistoryOpen: boolean;\n /** Select by Expression dialog visibility (#1314). */\n selectByExpressionOpen: boolean;\n // Layer preselected in the Select by Expression dialog when it is opened\n // from a layer's context menu, or null when opened without a target.\n // Deliberately not selectLayer(): that would clear the live selection the\n // dialog's add/remove/intersect modes need to combine with.\n selectByExpressionLayerId: string | null;\n /** Select by Location dialog visibility (#1314). */\n selectByLocationOpen: boolean;\n /** Same contract as `selectByExpressionLayerId`, for Select by Location. */\n selectByLocationLayerId: string | null;\n /**\n * Pending \"re-run from History\" request. Written by the History panel just\n * before it opens the target processing dialog; consumed and cleared by\n * that dialog once it has pre-filled its parameter form. Null when idle.\n */\n processingRerun: ProcessingRerunRequest | null;\n zoomToSelectedFeature: boolean;\n // Live-collaboration dialog visibility. Lifted into the store (rather than\n // local toolbar state) so the on-canvas session-status badge can reopen the\n // Collaborate dialog from outside the toolbar's component tree (#754).\n collaborateDialogOpen: boolean;\n };\n\n setProcessingOpen: (open: boolean) => void;\n setProcessingInitialTool: (toolId: string | null) => void;\n setConversionOpen: (kind: ConversionToolKind | null) => void;\n setVectorToolOpen: (kind: VectorToolKind | null) => void;\n setNetworkToolOpen: (kind: NetworkToolKind | null) => void;\n setStatisticsToolOpen: (kind: StatisticsToolKind | null) => void;\n setRasterToolOpen: (kind: RasterToolKind | null) => void;\n setSegmentationOpen: (open: boolean) => void;\n setObjectDetectionOpen: (open: boolean) => void;\n setSegmentEverythingOpen: (open: boolean) => void;\n setGeocodeOpen: (open: boolean) => void;\n setSqlWorkspaceOpen: (open: boolean) => void;\n setLoadEditorFeaturesOpen: (open: boolean, layerId?: string | null) => void;\n setPythonConsoleOpen: (open: boolean) => void;\n setNotebookOpen: (open: boolean) => void;\n setAssistantOpen: (open: boolean) => void;\n setAttributeTableOpen: (open: boolean) => void;\n /**\n * Ask the attribute table to open in edit mode on `layerId` (opening the\n * table too), or clear a pending request with `null`.\n */\n requestAttributeTableEdit: (layerId: string | null) => void;\n setRasterAttributeTableOpen: (open: boolean) => void;\n setDashboardOpen: (open: boolean) => void;\n setStorymapPanelOpen: (open: boolean) => void;\n setStorymapPresenting: (presenting: boolean, returnToEditor?: boolean) => void;\n /**\n * Record the layer opacities a story chapter applied, merging into the\n * presentation's running map (see `ui.storymapLayerOpacity`).\n */\n setStorymapLayerOpacity: (changes: Record<string, number>) => void;\n setStorymapComposing: (chapterId: string | null) => void;\n setBatchToolsOpen: (open: boolean) => void;\n setModelBuilderOpen: (open: boolean) => void;\n setModelBuilderRequestedModelId: (id: string | null) => void;\n setProcessingHistoryOpen: (open: boolean) => void;\n /** Open/close Select by Expression, optionally preselecting a target layer. */\n setSelectByExpressionOpen: (open: boolean, layerId?: string | null) => void;\n /** Open/close Select by Location, optionally preselecting a target layer. */\n setSelectByLocationOpen: (open: boolean, layerId?: string | null) => void;\n setProcessingRerun: (request: ProcessingRerunRequest | null) => void;\n setCollaborateDialogOpen: (open: boolean) => void;\n setZoomToSelectedFeature: (enabled: boolean) => void;\n setStyleManagerOpen: (open: boolean) => void;\n}\n\nexport const createUiSlice: SliceCreator<UiSlice> = (set) => ({\n ui: {\n processingOpen: false,\n processingInitialTool: null,\n conversionOpen: null,\n vectorToolOpen: null,\n networkToolOpen: null,\n statisticsToolOpen: null,\n rasterToolOpen: null,\n segmentationOpen: false,\n objectDetectionOpen: false,\n segmentEverythingOpen: false,\n geocodeOpen: false,\n sqlWorkspaceOpen: false,\n loadEditorFeaturesOpen: false,\n loadEditorFeaturesLayerId: null,\n pythonConsoleOpen: false,\n notebookOpen: false,\n assistantOpen: false,\n attributeTableOpen: false,\n attributeTableEditLayerId: null,\n rasterAttributeTableOpen: false,\n dashboardOpen: false,\n storymapPanelOpen: false,\n storymapPresenting: false,\n storymapReturnToEditor: false,\n storymapLayerOpacity: {},\n storymapComposingId: null,\n batchToolsOpen: false,\n modelBuilderOpen: false,\n modelBuilderRequestedModelId: null,\n styleManagerOpen: false,\n processingHistoryOpen: false,\n selectByExpressionOpen: false,\n selectByExpressionLayerId: null,\n selectByLocationOpen: false,\n selectByLocationLayerId: null,\n processingRerun: null,\n zoomToSelectedFeature: false,\n collaborateDialogOpen: false,\n },\n\n setProcessingOpen: (open) => set((s) => ({ ui: { ...s.ui, processingOpen: open } })),\n setProcessingInitialTool: (toolId) =>\n set((s) => ({ ui: { ...s.ui, processingInitialTool: toolId } })),\n setConversionOpen: (kind) => set((s) => ({ ui: { ...s.ui, conversionOpen: kind } })),\n setVectorToolOpen: (kind) =>\n set((s) => ({\n ui: {\n ...s.ui,\n // Pre-DGGS projects / callers may still pass h3-grid / h3-bin-points.\n vectorToolOpen:\n (kind as string | null) === \"h3-grid\"\n ? \"dggs-grid\"\n : (kind as string | null) === \"h3-bin-points\"\n ? \"dggs-bin\"\n : kind,\n },\n })),\n setNetworkToolOpen: (kind) => set((s) => ({ ui: { ...s.ui, networkToolOpen: kind } })),\n setStatisticsToolOpen: (kind) => set((s) => ({ ui: { ...s.ui, statisticsToolOpen: kind } })),\n setRasterToolOpen: (kind) => set((s) => ({ ui: { ...s.ui, rasterToolOpen: kind } })),\n setSegmentationOpen: (open) => set((s) => ({ ui: { ...s.ui, segmentationOpen: open } })),\n setObjectDetectionOpen: (open) => set((s) => ({ ui: { ...s.ui, objectDetectionOpen: open } })),\n setSegmentEverythingOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, segmentEverythingOpen: open } })),\n setGeocodeOpen: (open) => set((s) => ({ ui: { ...s.ui, geocodeOpen: open } })),\n setSqlWorkspaceOpen: (open) => set((s) => ({ ui: { ...s.ui, sqlWorkspaceOpen: open } })),\n setLoadEditorFeaturesOpen: (open, layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n loadEditorFeaturesOpen: open,\n loadEditorFeaturesLayerId: open ? (layerId ?? null) : null,\n },\n })),\n setPythonConsoleOpen: (open) => set((s) => ({ ui: { ...s.ui, pythonConsoleOpen: open } })),\n setNotebookOpen: (open) => set((s) => ({ ui: { ...s.ui, notebookOpen: open } })),\n setAssistantOpen: (open) => set((s) => ({ ui: { ...s.ui, assistantOpen: open } })),\n setAttributeTableOpen: (open) => set((s) => ({ ui: { ...s.ui, attributeTableOpen: open } })),\n requestAttributeTableEdit: (layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n attributeTableEditLayerId: layerId,\n attributeTableOpen: layerId === null ? s.ui.attributeTableOpen : true,\n },\n })),\n setRasterAttributeTableOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, rasterAttributeTableOpen: open } })),\n setDashboardOpen: (open) => set((s) => ({ ui: { ...s.ui, dashboardOpen: open } })),\n setStorymapPanelOpen: (open) =>\n set((s) => ({\n ui: {\n ...s.ui,\n storymapPanelOpen: open,\n // Opening the editor must leave compose mode, or the menu item could\n // re-open the dialog while the compose bar is still active over a\n // now-hidden map (#775). Closing (entering compose) leaves it as-is.\n ...(open ? { storymapComposingId: null } : {}),\n },\n })),\n setStorymapPresenting: (presenting, returnToEditor = false) =>\n set((s) => ({\n ui: {\n ...s.ui,\n storymapPresenting: presenting,\n // Track whether exiting should reopen the editor; only meaningful\n // while presenting, so it clears once the presentation ends (#918).\n storymapReturnToEditor: presenting ? returnToEditor : false,\n // A presentation starts from (and leaves behind) a clean slate; the\n // fades it applies are replayed from chapter 0 on the next run.\n storymapLayerOpacity: {},\n },\n })),\n setStorymapLayerOpacity: (changes) =>\n set((s) => {\n const next = { ...s.ui.storymapLayerOpacity };\n let changed = false;\n for (const [layerId, opacity] of Object.entries(changes)) {\n const clamped = Math.min(1, Math.max(0, opacity));\n if (next[layerId] === clamped) continue;\n next[layerId] = clamped;\n changed = true;\n }\n // Return the current state untouched when nothing moved: Zustand only\n // skips the listener broadcast for the same state reference, and a\n // chapter re-entering the same opacities would otherwise rebuild\n // every store subscriber's view.\n return changed ? { ui: { ...s.ui, storymapLayerOpacity: next } } : s;\n }),\n setStorymapComposing: (chapterId) =>\n set((s) => ({ ui: { ...s.ui, storymapComposingId: chapterId } })),\n setBatchToolsOpen: (open) => set((s) => ({ ui: { ...s.ui, batchToolsOpen: open } })),\n setModelBuilderOpen: (open) => set((s) => ({ ui: { ...s.ui, modelBuilderOpen: open } })),\n setModelBuilderRequestedModelId: (id) =>\n set((s) => ({ ui: { ...s.ui, modelBuilderRequestedModelId: id } })),\n setProcessingHistoryOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, processingHistoryOpen: open } })),\n setProcessingRerun: (request) => set((s) => ({ ui: { ...s.ui, processingRerun: request } })),\n setSelectByExpressionOpen: (open, layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n selectByExpressionOpen: open,\n selectByExpressionLayerId: open ? (layerId ?? null) : null,\n },\n })),\n setSelectByLocationOpen: (open, layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n selectByLocationOpen: open,\n selectByLocationLayerId: open ? (layerId ?? null) : null,\n },\n })),\n setCollaborateDialogOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, collaborateDialogOpen: open } })),\n setZoomToSelectedFeature: (enabled) =>\n set((s) => ({ ui: { ...s.ui, zoomToSelectedFeature: enabled } })),\n\n setStyleManagerOpen: (open) => set((s) => ({ ui: { ...s.ui, styleManagerOpen: open } })),\n});\n","/**\n * The app store: one Zustand store wrapped in zundo's undo history, composed\n * from the slices under `./store/` (zustand's slices pattern). The split is\n * internal — the hook, the state shape and every export are unchanged, and\n * consumers keep importing from here (or `@geolibre/core`).\n *\n * Slices: project (identity and lifecycle), map-view (camera, basemap, grid),\n * layers, layer-groups, project-content (comments, dashboard, story map),\n * processing (models, run history), libraries (style / layer / template),\n * session (selection and live readouts), collaboration, capabilities, and ui\n * (dialog and panel flags). Undo history lives in `./store/undo-history.ts`.\n */\nimport { create } from \"zustand\";\nimport { temporal } from \"zundo\";\nimport { appPrivilegeReason } from \"./capabilities\";\nimport { setActiveEllipsoidId } from \"./ellipsoids\";\nimport type { AppPrivilege } from \"./types\";\nimport { createCapabilitiesSlice } from \"./store/capabilities-slice\";\nimport { createCollaborationSlice } from \"./store/collaboration-slice\";\nimport { createLayerGroupsSlice } from \"./store/layer-groups-slice\";\nimport { createLayersSlice } from \"./store/layers-slice\";\nimport { createLibrariesSlice } from \"./store/libraries-slice\";\nimport { createMapViewSlice } from \"./store/map-view-slice\";\nimport { createProcessingSlice } from \"./store/processing-slice\";\nimport { createProjectContentSlice } from \"./store/project-content-slice\";\nimport { createProjectSlice } from \"./store/project-slice\";\nimport { createSessionSlice } from \"./store/session-slice\";\nimport { createUiSlice } from \"./store/ui-slice\";\nimport type { AppState } from \"./store/types\";\nimport { bindHistoryStore, createHistoryOptions } from \"./store/undo-history\";\n\nexport type { AppState } from \"./store/types\";\nexport type {\n ConversionToolKind,\n NetworkToolKind,\n RasterToolKind,\n StatisticsToolKind,\n VectorToolKind,\n} from \"./store/ui-slice\";\nexport {\n type GpsStatusFix,\n IDENTIFY_ALL_LAYERS_ID,\n type IdentifyState,\n identifyAllIncludes,\n identifyStateWithoutLayers,\n resolveIdentifyTarget,\n} from \"./store/session-slice\";\nexport { DEFAULT_COLLABORATION_STATE } from \"./store/collaboration-slice\";\nexport { projectPathLabel } from \"./store/project-slice\";\nexport {\n canRedoProjectRestore,\n canUndoProjectRestore,\n clearHistory,\n redo,\n registerProjectRestoreHistory,\n subscribeProjectRestoreHistory,\n undo,\n} from \"./store/undo-history\";\n\nexport const useAppStore = create<AppState>()(\n temporal(\n (set, get) => ({\n ...createProjectSlice(set, get),\n ...createMapViewSlice(set, get),\n ...createLayersSlice(set, get),\n ...createLayerGroupsSlice(set, get),\n ...createProjectContentSlice(set, get),\n ...createProcessingSlice(set, get),\n ...createLibrariesSlice(set, get),\n ...createSessionSlice(set, get),\n ...createCollaborationSlice(set, get),\n ...createCapabilitiesSlice(set, get),\n ...createUiSlice(set, get),\n }),\n createHistoryOptions(),\n ),\n);\n\nbindHistoryStore(useAppStore);\n\n// Mirror the project's ellipsoid into the module-level singleton the\n// measurement helpers read. One subscription covers every path that changes\n// preferences (setPreferences, load/new project, undo/redo) without threading\n// the value through each call site. The subscription runs on every store\n// mutation (e.g. setPointerCoords on each mousemove), so guard on the id to skip\n// the redundant work for a value that changes at most once per session.\nlet lastEllipsoidId = useAppStore.getState().preferences.map.ellipsoidId;\nsetActiveEllipsoidId(lastEllipsoidId);\nuseAppStore.subscribe((state) => {\n const id = state.preferences.map.ellipsoidId;\n if (id === lastEllipsoidId) return;\n lastEllipsoidId = id;\n setActiveEllipsoidId(id);\n});\n\n/**\n * Whether the map should fit to data the user just added, per the project's\n * Map Preferences. Add-data paths consult this before an automatic fit; an\n * explicit \"Zoom to layer\" request ignores it.\n *\n * @returns `true` unless the user turned off zooming to new layers.\n */\nexport function shouldZoomToNewLayers(): boolean {\n return useAppStore.getState().preferences.map.zoomToNewLayers !== false;\n}\n\n/**\n * React hook for consuming application capability state for a specific privilege.\n *\n * @param privilege - The privilege to check.\n * @returns `{ granted: boolean, reason?: string }`\n */\nexport function useAppCapability(privilege: AppPrivilege): { granted: boolean; reason?: string } {\n const capabilities = useAppStore((state) => state.capabilities);\n return {\n granted: capabilities.privileges.includes(privilege),\n reason: appPrivilegeReason(capabilities, privilege),\n };\n}\n","import { useAppStore } from \"./store\";\nimport { applySelectionMode, type SelectionMode } from \"./selection\";\n\n/**\n * Store-touching selection helpers. Kept out of `selection.ts` so that module\n * stays a pure, directly testable set of functions over id arrays.\n */\n\n/**\n * Applies a matched id set to the live selection under the given mode and\n * returns the resulting selection size. Combines with the current selection\n * only when the target layer already holds it (ids are per-layer, so a\n * cross-layer combine would mix unrelated features). Otherwise `current` is\n * empty: \"new\"/\"add\" start a fresh selection on the target layer, while\n * \"remove\"/\"intersect\" necessarily yield an empty one — the dialogs guard\n * this by forcing mode \"new\" when the target layer does not hold the\n * selection (SelectionModeField's disableCombineModes). `selectLayer` runs\n * before `selectFeatures` because it clears the selection as a side effect.\n *\n * Lives in `@geolibre/core` rather than the app so the layer-panel dialogs and\n * the map's drawing gestures (`@geolibre/map`) share one copy of this rule\n * instead of two that can drift apart.\n */\nexport function applyMatchedSelection(\n targetLayerId: string,\n matchedIds: string[],\n mode: SelectionMode,\n): number {\n const store = useAppStore.getState();\n const current = store.selectedLayerId === targetLayerId ? store.selectedFeatureIds : [];\n const next = applySelectionMode(current, matchedIds, mode);\n if (store.selectedLayerId !== targetLayerId) store.selectLayer(targetLayerId);\n store.selectFeatures(next);\n return next.length;\n}\n","/**\n * Shared scale-bar unit maths.\n *\n * The on-screen scale bar (`PlanetaryScaleControl` in `@geolibre/map`) and the\n * Print Layout scale bar (`apps/geolibre-desktop/src/lib/print-layout.ts`) both\n * need to pick a \"nice\" round distance in the user's chosen unit system\n * (metric / imperial / nautical). Keeping that logic in one place here means the\n * two bars round a given ground span to the same value and share the same unit\n * constants, so a change to one can never silently diverge from the other.\n */\n\nimport type { MapScaleUnit } from \"./types\";\n\n/** Conversion constants shared by every scale-bar denomination. */\nexport const FEET_PER_METER = 3.2808398950131235; // 1 / 0.3048\nexport const METERS_PER_FOOT = 0.3048;\nexport const FEET_PER_MILE = 5280;\nexport const METERS_PER_MILE = 1609.344; // 5280 ft\nexport const METERS_PER_NAUTICAL_MILE = 1852;\n\n/**\n * The largest `1 / 2 / 3 / 5 / 10 × 10ⁿ` number that does not exceed `num`, so\n * the bar lands on a readable round value. Uses log10 for the magnitude (unlike\n * MapLibre's digit-count trick, which returns 1 for any `0 < num < 1` and so\n * rounds *up* — producing a bar wider than the max width for sub-unit spans).\n * Returns 0 for a non-positive input so callers can clamp or fall back.\n */\nexport function getRoundNum(num: number): number {\n if (!(num > 0)) return 0;\n const pow10 = Math.pow(10, Math.floor(Math.log10(num)));\n const d = num / pow10;\n const nice = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1;\n return pow10 * nice;\n}\n\n/** Trim trailing-zero noise from a rounded value (e.g. 0.5, 2, 300). */\nexport function formatRoundNum(value: number): string {\n return Number.parseFloat(value.toPrecision(12)).toString();\n}\n\n/**\n * The denomination a scale bar rounds and labels distances with for a ground\n * span of `maxMeters` in the requested unit system: km/m for metric, mi/ft for\n * imperial, and nautical miles for nautical. The unit crosses to the larger\n * denomination once the span exceeds one of it. `metersPerUnit` is the size of\n * one unit of that denomination in metres, so a caller can round the span in\n * unit space and convert the result back to metres (and thus pixels).\n */\nexport function scaleDenomination(\n maxMeters: number,\n unit: MapScaleUnit,\n): { metersPerUnit: number; label: string } {\n if (unit === \"imperial\") {\n const feet = maxMeters * FEET_PER_METER;\n return feet >= FEET_PER_MILE\n ? { metersPerUnit: METERS_PER_MILE, label: \"mi\" }\n : { metersPerUnit: METERS_PER_FOOT, label: \"ft\" };\n }\n if (unit === \"nautical\") {\n return { metersPerUnit: METERS_PER_NAUTICAL_MILE, label: \"nmi\" };\n }\n return maxMeters >= 1000\n ? { metersPerUnit: 1000, label: \"km\" }\n : { metersPerUnit: 1, label: \"m\" };\n}\n\n/**\n * Convert a ground distance in metres into the span (already divided into the\n * bar's denomination) and its label — the shape the on-screen control consumes.\n * Derived from {@link scaleDenomination} so the two never diverge.\n */\nexport function scaleSpan(maxMeters: number, unit: MapScaleUnit): { span: number; label: string } {\n const { metersPerUnit, label } = scaleDenomination(maxMeters, unit);\n return { span: maxMeters / metersPerUnit, label };\n}\n","/**\n * Shared ground-elevation resolution.\n *\n * Several places in the app need \"how high is the ground at this coordinate\":\n * the Elevation Profile plugin (along a drawn line), the Measure tool's\n * terrain-aware readout (along a measured geometry), and the status bar (under\n * the cursor). They all resolve it the same way, from two sources tried in\n * order:\n *\n * 1. The map's own terrain (`map.queryTerrainElevation`) when 3D terrain is\n * enabled — instant and offline, but only where DEM tiles are loaded.\n * MapLibre returns elevations multiplied by the terrain exaggeration, so\n * values are divided back to true meters.\n * 2. The keyless Open-Meteo elevation API — works without terrain enabled and\n * anywhere on Earth, but costs a network round-trip.\n *\n * This module lives in `@geolibre/core` rather than beside the Elevation\n * Profile plugin because `@geolibre/map` (which owns the pointer readout)\n * cannot import from `@geolibre/plugins` without a dependency cycle — plugins\n * already depends on map. Core is the only package all three can share.\n *\n * Source 2 is Earth-only: Open-Meteo has no data for the planetary basemaps, so\n * callers must not reach for it when the active body is not Earth.\n */\n\n/** A coordinate as `[longitude, latitude]` in degrees. */\nexport type LngLat = [number, number];\n\n/** Open-Meteo accepts at most 100 coordinates per elevation request. */\nexport const MAX_POINTS_PER_REQUEST = 100;\n\n/** Abort an elevation request that has not responded within this window. */\nexport const ELEVATION_REQUEST_TIMEOUT_MS = 15000;\n\nconst ENDPOINT = \"https://api.open-meteo.com/v1/elevation\";\n\n/** Error thrown when an elevation request cannot be completed or parsed. */\nexport class ElevationFetchError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ElevationFetchError\";\n }\n}\n\n/** A `fetch`-compatible function, so tests can inject a stub. */\nexport type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;\n\ninterface ElevationResponse {\n elevation?: number[];\n}\n\n/**\n * Normalize longitude into [-180, 180] degrees (handles unwrapped coordinates\n * from antimeridian cross-overs and world-copy pannings).\n */\nexport function normalizeLongitude(lng: number): number {\n if (!Number.isFinite(lng)) return 0;\n return ((((lng + 180) % 360) + 360) % 360) - 180;\n}\n\n/**\n * Clamp latitude into [-90, 90] degrees.\n */\nexport function clampLatitude(lat: number): number {\n if (!Number.isFinite(lat)) return 0;\n return Math.min(90, Math.max(-90, lat));\n}\n\n/**\n * Fetch elevations (in meters) for an ordered list of coordinates.\n *\n * @param points - Coordinates as `[lng, lat]`, at most {@link MAX_POINTS_PER_REQUEST}\n * @param fetchImpl - Optional `fetch` implementation; defaults to the global `fetch`\n * @returns The elevation in meters for each input coordinate, in the same order\n * @throws {ElevationFetchError} On too many points, a non-finite coordinate, a\n * network error, a non-2xx response, a malformed body, or a length mismatch\n */\nexport async function fetchElevations(points: LngLat[], fetchImpl?: FetchLike): Promise<number[]> {\n if (points.length === 0) return [];\n if (points.length > MAX_POINTS_PER_REQUEST) {\n throw new ElevationFetchError(\n `Too many points: ${points.length} (max ${MAX_POINTS_PER_REQUEST}).`,\n );\n }\n // Normalizing would turn NaN/Infinity into 0 and query (0, 0) as if it were\n // the requested point, so reject them instead of returning a wrong elevation.\n if (points.some(([lng, lat]) => !Number.isFinite(lng) || !Number.isFinite(lat))) {\n throw new ElevationFetchError(\"Invalid coordinate: longitude and latitude must be finite.\");\n }\n\n const doFetch: FetchLike = fetchImpl ?? ((url, init) => fetch(url, init));\n const latitudes = points.map((p) => clampLatitude(p[1]).toFixed(6)).join(\",\");\n const longitudes = points.map((p) => normalizeLongitude(p[0]).toFixed(6)).join(\",\");\n const url = `${ENDPOINT}?latitude=${latitudes}&longitude=${longitudes}`;\n\n // A default fetch never times out, so a hung request would leave the control's\n // busy state stuck with no recovery. Abort after ELEVATION_REQUEST_TIMEOUT_MS\n // and surface it as a normal fetch error the caller already handles.\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), ELEVATION_REQUEST_TIMEOUT_MS);\n let response: Response;\n try {\n response = await doFetch(url, { signal: controller.signal });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw new ElevationFetchError(\"Elevation request timed out.\");\n }\n const detail = error instanceof Error ? error.message : \"unknown error\";\n throw new ElevationFetchError(`Could not reach the elevation service: ${detail}`);\n } finally {\n clearTimeout(timeoutId);\n }\n\n if (!response.ok) {\n throw new ElevationFetchError(`Elevation request failed (HTTP ${response.status}).`);\n }\n\n let data: ElevationResponse;\n try {\n data = (await response.json()) as ElevationResponse;\n } catch {\n throw new ElevationFetchError(\"Could not parse the elevation response.\");\n }\n\n if (!data || !Array.isArray(data.elevation)) {\n throw new ElevationFetchError(\"Malformed elevation response.\");\n }\n if (data.elevation.length !== points.length) {\n throw new ElevationFetchError(\n `Expected ${points.length} elevations but received ${data.elevation.length}.`,\n );\n }\n\n return data.elevation;\n}\n\n/** The slice of the MapLibre map the terrain sampler needs (stubbed in tests). */\nexport interface TerrainMapLike {\n getTerrain?: () => { exaggeration?: number } | null | undefined;\n queryTerrainElevation?: (lngLat: LngLat) => number | null;\n}\n\n/**\n * Sample elevations from the map's enabled 3D terrain, in true meters\n * (MapLibre's `queryTerrainElevation` bakes the exaggeration in, so it is\n * divided back out). Returns null when terrain is not enabled.\n */\nexport function sampleMapTerrain(\n map: TerrainMapLike | null | undefined,\n points: LngLat[],\n): (number | null)[] | null {\n if (!map?.getTerrain || !map.queryTerrainElevation) return null;\n const terrain = map.getTerrain();\n if (!terrain) return null;\n const exaggeration =\n typeof terrain.exaggeration === \"number\" && terrain.exaggeration > 0 ? terrain.exaggeration : 1;\n return points.map((point) => {\n const elevation = map.queryTerrainElevation!(point);\n return typeof elevation === \"number\" && Number.isFinite(elevation)\n ? elevation / exaggeration\n : null;\n });\n}\n\n/**\n * Sample a single point's elevation from enabled 3D terrain, in true meters.\n * Returns null when terrain is off or the DEM tile under the point has not\n * loaded — the caller decides whether that is worth a network round-trip.\n */\nexport function sampleMapTerrainPoint(\n map: TerrainMapLike | null | undefined,\n point: LngLat,\n): number | null {\n return sampleMapTerrain(map, [point])?.[0] ?? null;\n}\n\n/**\n * Sample elevations from the Open-Meteo API, chunked to its 100-point request\n * limit. A failed chunk yields nulls for its points rather than throwing, so a\n * flaky network degrades the readout instead of breaking the caller.\n */\nexport async function sampleRemoteElevations(\n points: LngLat[],\n fetchImpl?: FetchLike,\n): Promise<(number | null)[]> {\n const chunks: LngLat[][] = [];\n for (let i = 0; i < points.length; i += MAX_POINTS_PER_REQUEST) {\n chunks.push(points.slice(i, i + MAX_POINTS_PER_REQUEST));\n }\n // The chunks are independent, so fire them concurrently; Promise.all\n // preserves their order for reassembly.\n const chunkResults = await Promise.all(\n chunks.map(async (chunk) => {\n try {\n const elevations = await fetchElevations(chunk, fetchImpl);\n return elevations.map((elevation) => (Number.isFinite(elevation) ? elevation : null));\n } catch {\n return chunk.map(() => null);\n }\n }),\n );\n return chunkResults.flat();\n}\n\n// --- Pointer elevation resolver --------------------------------------------\n\n/** Round a coordinate to ~11 m so nearby hovers share one cache entry. */\nfunction cacheKey(point: LngLat): string {\n return `${point[0].toFixed(4)},${point[1].toFixed(4)}`;\n}\n\n/** How long the pointer must sit still before a remote lookup is worth it. */\nexport const POINTER_ELEVATION_DEBOUNCE_MS = 500;\n\n/** Remote results kept per resolver; bounded so a long session cannot grow it without limit. */\nexport const POINTER_ELEVATION_CACHE_LIMIT = 500;\n\nexport interface PointerElevationResolverOptions {\n /** The live map, for the terrain sample. May return null before init. */\n getMap: () => TerrainMapLike | null | undefined;\n /**\n * Whether the readout is switched on (Controls -> Elevation). Read per call\n * rather than captured, and rechecked before publishing a remote result, so\n * turning it off mid-request neither shows a late value nor leaves the last\n * one on screen. Defaults to always-on for callers that do not gate it.\n */\n isEnabled?: () => boolean;\n /**\n * Whether the *remote* Open-Meteo fallback may run. The terrain path sends\n * nothing anywhere, so it is deliberately not gated by this — a user who has\n * declined the network lookup still gets a live readout wherever 3D terrain\n * is on. Defaults to allowed for callers that do not gate it.\n */\n canUseRemote?: () => boolean;\n /** Whether the active body is Earth — Open-Meteo has no data for anything else. */\n isEarth: () => boolean;\n /** Called with the resolved elevation in true metres, or null when unknown. */\n emit: (elevation: number | null) => void;\n fetchImpl?: FetchLike;\n debounceMs?: number;\n}\n\nexport interface PointerElevationResolver {\n /** Report a new pointer position, or null when the pointer leaves the map. */\n update: (point: LngLat | null) => void;\n /**\n * Drop any pending *and* in-flight lookup without tearing the resolver down.\n * Used when the map's context changes under it — loading another project\n * resets the readout, and a response for the previous project must not\n * repaint it.\n */\n invalidate: () => void;\n /** Cancel any pending lookup. */\n dispose: () => void;\n}\n\n/**\n * Resolves the ground elevation under the pointer for the status bar.\n *\n * Two paths, deliberately asymmetric:\n *\n * - **Terrain enabled** — `queryTerrainElevation` is synchronous and free, so\n * it runs on every move and the readout tracks the cursor live.\n * - **Terrain disabled** — falls back to Open-Meteo, but only after the\n * pointer has been still for {@link POINTER_ELEVATION_DEBOUNCE_MS}, and only\n * on Earth. Results are cached per ~11 m cell, so sweeping back over ground\n * already looked up costs nothing. Without both guards a hover readout would\n * fire a request per mousemove at a free, keyless public service.\n *\n * While a remote lookup is outstanding the readout reads null rather than the\n * previous point's value: showing a stale height next to fresh coordinates\n * would be worse than showing nothing.\n */\nexport function createPointerElevationResolver(\n options: PointerElevationResolverOptions,\n): PointerElevationResolver {\n const { getMap, isEarth, emit, fetchImpl } = options;\n const isEnabled = options.isEnabled ?? (() => true);\n const canUseRemote = options.canUseRemote ?? (() => true);\n const debounceMs = options.debounceMs ?? POINTER_ELEVATION_DEBOUNCE_MS;\n const cache = new Map<string, number | null>();\n let timer: ReturnType<typeof setTimeout> | null = null;\n // Bumped on every update so a slow response for an abandoned point is dropped\n // rather than overwriting the readout for wherever the pointer is now.\n let generation = 0;\n\n const cancelPending = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const update = (point: LngLat | null) => {\n generation += 1;\n cancelPending();\n\n if (!point || !isEnabled()) {\n emit(null);\n return;\n }\n\n const fromTerrain = sampleMapTerrainPoint(getMap(), point);\n if (fromTerrain !== null) {\n emit(fromTerrain);\n return;\n }\n\n // Open-Meteo has no data off Earth, and the remote path needs consent.\n if (!isEarth() || !canUseRemote()) {\n emit(null);\n return;\n }\n\n const key = cacheKey(point);\n if (cache.has(key)) {\n const cached = cache.get(key) ?? null;\n // Re-insert to move this entry to the end: Map iterates in insertion\n // order, so the first key is always the least recently used.\n cache.delete(key);\n cache.set(key, cached);\n emit(cached);\n return;\n }\n\n emit(null);\n const requested = generation;\n timer = setTimeout(() => {\n timer = null;\n void (async () => {\n const [elevation] = await sampleRemoteElevations([point], fetchImpl);\n const value = elevation ?? null;\n // `sampleRemoteElevations` turns a fetch failure into a null, which is\n // indistinguishable here from \"no data for this cell\". Caching that\n // would blackhole the readout for this ~11 m cell for the rest of the\n // session, so only successes are remembered and a transient network\n // blip gets another chance on the next hover.\n if (value !== null) {\n // Evict just the oldest entry. Clearing the whole cache at the cap\n // would make every 500th distinct cell trigger a burst of re-fetches\n // for cells that may still be under active hovering.\n if (cache.size >= POINTER_ELEVATION_CACHE_LIMIT) {\n const oldest = cache.keys().next();\n if (!oldest.done) cache.delete(oldest.value);\n }\n cache.set(key, value);\n }\n // The pointer moved, left, or the resolver was disposed while the\n // request was in flight. Also recheck the body: a switch to a planetary\n // basemap mid-request must not publish an Earth elevation over it.\n if (requested !== generation || !isEarth() || !isEnabled() || !canUseRemote()) return;\n emit(value);\n })();\n }, debounceMs);\n };\n\n // Bumping the generation is what actually invalidates an *in-flight*\n // request; cancelPending alone only clears a timer that has not fired yet, so\n // a fetch already in progress would still emit into a torn-down map.\n const invalidate = (): void => {\n generation += 1;\n cancelPending();\n };\n\n return { update, invalidate, dispose: invalidate };\n}\n","/**\n * Camera altitude (\"Eye alt\") for the status bar — issue #1816.\n *\n * MapLibre's `transform.getCameraAltitude()` returns the camera height above\n * sea level in metres, which is exactly what Google Earth Pro's `Eye alt`\n * reports. But it derives that height from the Web-Mercator scale, which is\n * built on Earth's radius — so on a Moon / Mars / Mercury basemap it is wrong by\n * the ratio of that body's circumference to Earth's, the same way MapLibre's\n * built-in ScaleControl is (see `PlanetaryScaleControl`, which fixes the\n * horizontal case).\n *\n * {@link scaleAltitudeToActiveBody} applies the matching correction so the\n * readout is right on every body, using the same active-ellipsoid singleton the\n * scale bar and the measurement tools read.\n */\n\nimport { getActiveBodyRadiusRatio } from \"./ellipsoids\";\nimport { scaleDenomination } from \"./scale-units\";\nimport type { MapScaleUnit } from \"./types\";\n\n/**\n * Convert a MapLibre camera altitude (computed against Earth's radius) into the\n * active body's true altitude. A no-op on Earth.\n *\n * @param earthAltitudeMeters Altitude in metres as MapLibre reports it\n * @returns Altitude in metres above the active body's datum, or null when the\n * input is not a usable number (no map, degenerate transform, globe edge case)\n */\nexport function scaleAltitudeToActiveBody(earthAltitudeMeters: number | null): number | null {\n if (earthAltitudeMeters === null || !Number.isFinite(earthAltitudeMeters)) return null;\n return earthAltitudeMeters * getActiveBodyRadiusRatio();\n}\n\n/**\n * Format a camera altitude for the status bar.\n *\n * Altitude spans metres (standing on a ridge) to tens of thousands of\n * kilometres (whole-globe view), so it borrows the scale bar's denomination\n * switching rather than printing \"12,742,000 m\". Metric crosses to km past\n * 1 km, imperial to miles past a mile, and nautical stays in nautical miles —\n * the same thresholds {@link scaleDenomination} gives the scale bar, so the two\n * always agree about which unit this view is being described in.\n */\nexport function formatCameraAltitude(meters: number, unit: MapScaleUnit): string {\n const { metersPerUnit, label } = scaleDenomination(Math.abs(meters), unit);\n const value = meters / metersPerUnit;\n // Sub-unit and single-digit values need a decimal to say anything; past that\n // the extra digit is noise on a number that moves with every zoom step.\n const digits = Math.abs(value) < 10 ? 1 : 0;\n return `${value.toLocaleString(undefined, {\n minimumFractionDigits: digits,\n maximumFractionDigits: digits,\n })} ${label}`;\n}\n","/**\n * Structured differ for two {@link GeoLibreProject} documents, used to show\n * what changed between autosave snapshots (GeoLibre#2858, \"Project snapshot\n * diff\").\n *\n * The result is a summary rather than a JSON patch: layers are matched by id\n * and reported as added, removed, moved or changed; per-layer changes are\n * grouped (style, labels, filter, source, other) with truncated before/after\n * previews; embedded GeoJSON is summarized as counts of added, removed and\n * modified features. Everything is pure and synchronous.\n *\n * Embedded GeoJSON can be large, so each feature is hashed at most once per\n * object identity (memoized in a `WeakMap`), and layers whose `geojson`\n * reference is shared between the two projects are skipped outright.\n */\nimport type { FeatureCollection } from \"geojson\";\nimport type { GeoLibreLayer, GeoLibreProject, ProjectPluginState } from \"./types\";\n\n/** One changed value, with bounded human-readable previews of each side. */\nexport interface ProjectValueChange {\n /** Dotted key path relative to the section, e.g. `fillColor` or `labels.field`. */\n path: string;\n /** Preview of the earlier value; `undefined` when the key was absent. */\n before?: string;\n /** Preview of the later value; `undefined` when the key is now absent. */\n after?: string;\n}\n\n/** Counts of embedded GeoJSON feature changes in one layer. */\nexport interface FeatureChangeSummary {\n added: number;\n removed: number;\n modified: number;\n unchanged: number;\n /**\n * How features were matched: `\"id\"` when every feature carries a unique\n * `id`, `\"geometry\"` when none does (matched by geometry hash), `\"mixed\"`\n * otherwise.\n */\n matchedBy: \"id\" | \"geometry\" | \"mixed\";\n}\n\n/** Changes to one layer between the two projects. */\nexport interface LayerDiff {\n id: string;\n /** Layer name in the later project (or the earlier one when removed). */\n name: string;\n /** Layer type in the later project (or the earlier one when removed). */\n type: string;\n status: \"added\" | \"removed\" | \"changed\";\n /** Set when the layer was renamed. */\n renamed?: { before: string; after: string };\n /**\n * Set when the layer changed position relative to the layers present in\n * both projects (bottom-to-top indexes in each project's `layers`).\n */\n moved?: { before: number; after: number };\n visibility?: { before: boolean; after: boolean };\n opacity?: { before: number; after: number };\n /** Style keys other than labels. */\n style: ProjectValueChange[];\n /** Label style keys (`style.labels.*`), without the `labels.` prefix. */\n labels: ProjectValueChange[];\n /** `filterExpression` and `quickFilters`. */\n filter: ProjectValueChange[];\n /** `type`, `source.*` and `sourcePath`. */\n source: ProjectValueChange[];\n /** Any other persisted layer field (popup, joins, metadata.*, ...). */\n other: ProjectValueChange[];\n /** Embedded GeoJSON feature changes; absent when the data is unchanged. */\n features?: FeatureChangeSummary;\n /** Feature count for an added or removed layer with embedded GeoJSON. */\n featureCount?: number;\n}\n\n/** Changes to one plugin's project state. */\nexport interface PluginDiff {\n id: string;\n /** `added`/`removed` mean the plugin was activated/deactivated. */\n status: \"added\" | \"removed\" | \"changed\";\n changes: ProjectValueChange[];\n}\n\n/** The full structured difference between two projects. */\nexport interface ProjectDiff {\n layers: {\n added: LayerDiff[];\n removed: LayerDiff[];\n changed: LayerDiff[];\n /** True when any layer present in both projects changed position. */\n reordered: boolean;\n };\n camera: ProjectValueChange[];\n basemap: ProjectValueChange[];\n /** Map projection (`preferences.map.projection`). */\n projection: ProjectValueChange[];\n /** Other project preferences (`preferences.*` minus the projection). */\n preferences: ProjectValueChange[];\n plugins: PluginDiff[];\n /** Plugin manifest URLs added to or removed from the project. */\n pluginManifests: { added: string[]; removed: string[] };\n /** Project name (`name`), `metadata.description` and other metadata keys. */\n metadata: ProjectValueChange[];\n /** Other top-level sections that changed (`legend`, `storymap`, ...). */\n sections: string[];\n /** Total number of reported changes; 0 means the projects are equivalent. */\n changeCount: number;\n}\n\n/** Options for {@link diffProjects}. */\nexport interface ProjectDiffOptions {\n /** Maximum characters of a before/after preview (default 80). */\n maxPreviewLength?: number;\n}\n\nconst DEFAULT_PREVIEW_LENGTH = 80;\nconst CAMERA_EPSILON = 1e-6;\n\n/** Layer fields reported in their own group, never under `other`. */\nconst LAYER_GROUPED_KEYS = new Set([\n \"id\",\n \"name\",\n \"type\",\n \"visible\",\n \"opacity\",\n \"style\",\n \"geojson\",\n \"filterExpression\",\n \"quickFilters\",\n \"source\",\n \"sourcePath\",\n // Derived from the layer order on save; reordering is reported separately.\n \"beforeId\",\n]);\n\n/** Top-level project fields reported in their own group, never as a section. */\nconst PROJECT_GROUPED_KEYS = new Set([\n \"id\",\n \"version\",\n \"name\",\n \"mapView\",\n \"basemapStyleUrl\",\n \"basemapVisible\",\n \"basemapOpacity\",\n \"blankBackgroundColor\",\n \"primaryRenderer\",\n \"layers\",\n \"styles\",\n \"preferences\",\n \"plugins\",\n \"metadata\",\n // UI selection, not content.\n \"selectedLayerId\",\n]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Deep structural equality for JSON-like values. A key whose value is\n * `undefined` is treated as absent, so an in-memory record compares equal to\n * its JSON round trip.\n *\n * @param a - First value.\n * @param b - Second value.\n * @returns True when the two values are structurally equal.\n */\nexport function jsonValuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a === \"number\" && typeof b === \"number\") return Number.isNaN(a) && Number.isNaN(b);\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (!jsonValuesEqual(a[i], b[i])) return false;\n return true;\n }\n if (isPlainObject(a)) {\n if (!isPlainObject(b)) return false;\n for (const key of Object.keys(a)) {\n if (a[key] === undefined) continue;\n if (!jsonValuesEqual(a[key], b[key])) return false;\n }\n for (const key of Object.keys(b)) {\n if (b[key] !== undefined && a[key] === undefined) return false;\n }\n return true;\n }\n return false;\n}\n\nfunction isBlank(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return isPlainObject(value) && Object.values(value).every((item) => item === undefined);\n}\n\n/**\n * Equality for reporting: like {@link jsonValuesEqual}, but an empty array,\n * an empty object, `null` and an absent value are interchangeable, since the\n * save path omits empty sections that an in-memory project still carries.\n */\nfunction sameForReport(a: unknown, b: unknown): boolean {\n return jsonValuesEqual(a, b) || (isBlank(a) && isBlank(b));\n}\n\n/**\n * Render a bounded JSON-ish preview of a value without serializing more of it\n * than the preview can show, so a large array or object costs no more than\n * `maxLength` characters of work.\n *\n * @param value - The value to preview.\n * @param maxLength - Maximum preview length; longer output ends with `…`.\n * @returns The preview, or `undefined` for `undefined`.\n */\nexport function previewValue(\n value: unknown,\n maxLength = DEFAULT_PREVIEW_LENGTH,\n): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value === \"string\") {\n return value.length > maxLength ? `${value.slice(0, Math.max(0, maxLength - 1))}…` : value;\n }\n let out = \"\";\n let truncated = false;\n const emit = (text: string): boolean => {\n if (out.length + text.length > maxLength) {\n out += text.slice(0, Math.max(0, maxLength - out.length));\n truncated = true;\n return false;\n }\n out += text;\n return true;\n };\n const walk = (v: unknown): boolean => {\n if (v === null || typeof v !== \"object\") {\n if (typeof v === \"number\" && !Number.isFinite(v)) return emit(\"null\");\n // A long string can never fit, so quote only the part that could show\n // instead of serializing a nested blob (a data URI, say) in full.\n if (typeof v === \"string\" && v.length > maxLength) {\n return emit(JSON.stringify(v.slice(0, maxLength)));\n }\n const text = typeof v === \"bigint\" ? v.toString() : JSON.stringify(v);\n return emit(text ?? \"null\");\n }\n if (Array.isArray(v)) {\n if (!emit(\"[\")) return false;\n for (let i = 0; i < v.length; i++) {\n if (i > 0 && !emit(\",\")) return false;\n if (!walk(v[i] === undefined ? null : v[i])) return false;\n }\n return emit(\"]\");\n }\n if (!emit(\"{\")) return false;\n let first = true;\n for (const [key, item] of Object.entries(v as Record<string, unknown>)) {\n if (item === undefined) continue;\n if (!first && !emit(\",\")) return false;\n first = false;\n if (!emit(`${JSON.stringify(key)}:`)) return false;\n if (!walk(item)) return false;\n }\n return emit(\"}\");\n };\n walk(value);\n if (truncated) out = `${out.slice(0, Math.max(0, maxLength - 1))}…`;\n return out;\n}\n\n/**\n * Diff two records key by key. Nested plain objects are descended into up to\n * `depth` levels (paths joined with `.`); arrays and deeper values are\n * compared whole.\n */\nfunction diffRecords(\n before: Record<string, unknown> | undefined,\n after: Record<string, unknown> | undefined,\n options: { prefix?: string; depth?: number; skip?: Set<string>; maxLength: number },\n out: ProjectValueChange[],\n): void {\n const a = before ?? {};\n const b = after ?? {};\n const prefix = options.prefix ?? \"\";\n const depth = options.depth ?? 1;\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n for (const key of keys) {\n if (options.skip?.has(key)) continue;\n const x = a[key];\n const y = b[key];\n if (sameForReport(x, y)) continue;\n if (depth > 0 && isPlainObject(x) && isPlainObject(y)) {\n diffRecords(\n x,\n y,\n { prefix: `${prefix}${key}.`, depth: depth - 1, maxLength: options.maxLength },\n out,\n );\n continue;\n }\n out.push({\n path: `${prefix}${key}`,\n before: previewValue(x, options.maxLength),\n after: previewValue(y, options.maxLength),\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Feature hashing\n// ---------------------------------------------------------------------------\n\ninterface FeatureHash {\n /** Hash of the geometry alone. */\n geometry: string;\n /** Hash of geometry + properties (+ any other member). */\n full: string;\n}\n\nconst featureHashCache = new WeakMap<object, FeatureHash>();\n\n/** cyrb53: a fast, well-distributed 53-bit string hash. */\nfunction hashString(text: string, seed = 0): string {\n let h1 = 0xdeadbeef ^ seed;\n let h2 = 0x41c6ce57 ^ seed;\n for (let i = 0; i < text.length; i++) {\n const ch = text.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);\n}\n\nfunction hashFeature(feature: unknown): FeatureHash {\n if (typeof feature !== \"object\" || feature === null) {\n const text = JSON.stringify(feature ?? null);\n const h = hashString(text);\n return { geometry: h, full: h };\n }\n const cached = featureHashCache.get(feature);\n if (cached) return cached;\n const { geometry, properties, id: _id, ...rest } = feature as Record<string, unknown>;\n const geometryHash = hashString(JSON.stringify(geometry ?? null));\n const propertiesHash = hashString(JSON.stringify(properties ?? null), 1);\n const restKeys = Object.keys(rest).filter((key) => key !== \"type\" && rest[key] !== undefined);\n const restHash = restKeys.length > 0 ? hashString(JSON.stringify(rest), 2) : \"\";\n const result = { geometry: geometryHash, full: `${geometryHash}:${propertiesHash}:${restHash}` };\n featureHashCache.set(feature, result);\n return result;\n}\n\nfunction featureKey(feature: unknown): string | null {\n if (typeof feature !== \"object\" || feature === null) return null;\n const id = (feature as { id?: unknown }).id;\n if (typeof id === \"string\") return `s:${id}`;\n if (typeof id === \"number\" && Number.isFinite(id)) return `n:${id}`;\n return null;\n}\n\ninterface IndexedFeatures {\n byId: Map<string, string>;\n /** Geometry hash → full hashes of the id-less features with that geometry. */\n byGeometry: Map<string, string[]>;\n keyed: number;\n unkeyed: number;\n}\n\nfunction indexFeatures(features: readonly unknown[]): IndexedFeatures {\n const byId = new Map<string, string>();\n const byGeometry = new Map<string, string[]>();\n let keyed = 0;\n let unkeyed = 0;\n for (const feature of features) {\n const hash = hashFeature(feature);\n const key = featureKey(feature);\n if (key !== null && !byId.has(key)) {\n byId.set(key, hash.full);\n keyed++;\n continue;\n }\n // No id, or a duplicate id: fall back to geometry matching.\n const list = byGeometry.get(hash.geometry);\n if (list) list.push(hash.full);\n else byGeometry.set(hash.geometry, [hash.full]);\n unkeyed++;\n }\n return { byId, byGeometry, keyed, unkeyed };\n}\n\n/**\n * Count the features added, removed and modified between two feature\n * collections. Features are matched by `id` when they carry one; id-less\n * features are matched by geometry hash, so a feature whose properties\n * changed but whose geometry did not counts as modified, while a moved\n * id-less feature counts as one removal plus one addition.\n *\n * @param before - Earlier collection (absent counts as empty).\n * @param after - Later collection (absent counts as empty).\n * @returns The change counts.\n */\nexport function diffFeatureCollections(\n before: FeatureCollection | null | undefined,\n after: FeatureCollection | null | undefined,\n): FeatureChangeSummary {\n const a = indexFeatures(Array.isArray(before?.features) ? before.features : []);\n const b = indexFeatures(Array.isArray(after?.features) ? after.features : []);\n let added = 0;\n let removed = 0;\n let modified = 0;\n let unchanged = 0;\n\n for (const [key, hash] of a.byId) {\n const other = b.byId.get(key);\n if (other === undefined) removed++;\n else if (other === hash) unchanged++;\n else modified++;\n }\n for (const key of b.byId.keys()) if (!a.byId.has(key)) added++;\n\n const geometryKeys = new Set([...a.byGeometry.keys(), ...b.byGeometry.keys()]);\n for (const geometry of geometryKeys) {\n const left = a.byGeometry.get(geometry) ?? [];\n const right = b.byGeometry.get(geometry) ?? [];\n // Pair identical features first, then pair the rest as modified.\n const counts = new Map<string, number>();\n for (const full of left) counts.set(full, (counts.get(full) ?? 0) + 1);\n let same = 0;\n for (const full of right) {\n const n = counts.get(full) ?? 0;\n if (n > 0) {\n counts.set(full, n - 1);\n same++;\n }\n }\n const leftRest = left.length - same;\n const rightRest = right.length - same;\n const paired = Math.min(leftRest, rightRest);\n unchanged += same;\n modified += paired;\n removed += leftRest - paired;\n added += rightRest - paired;\n }\n\n const keyed = a.keyed + b.keyed;\n const unkeyed = a.unkeyed + b.unkeyed;\n const matchedBy = unkeyed === 0 ? \"id\" : keyed === 0 ? \"geometry\" : \"mixed\";\n return { added, removed, modified, unchanged, matchedBy };\n}\n\n// ---------------------------------------------------------------------------\n// Layers\n// ---------------------------------------------------------------------------\n\nfunction effectiveStyle(\n project: GeoLibreProject,\n layer: GeoLibreLayer,\n): Record<string, unknown> | undefined {\n const top = project.styles?.[layer.id];\n if (!top) return layer.style as unknown as Record<string, unknown> | undefined;\n return { ...(layer.style ?? {}), ...top } as unknown as Record<string, unknown>;\n}\n\nfunction featureCountOf(layer: GeoLibreLayer): number | undefined {\n return Array.isArray(layer.geojson?.features) ? layer.geojson.features.length : undefined;\n}\n\nfunction emptyLayerDiff(layer: GeoLibreLayer, status: LayerDiff[\"status\"]): LayerDiff {\n return {\n id: layer.id,\n name: layer.name,\n type: layer.type,\n status,\n style: [],\n labels: [],\n filter: [],\n source: [],\n other: [],\n };\n}\n\n/**\n * Ids (of layers present in both orders) that moved: everything outside a\n * longest increasing subsequence of their later positions, so dragging one\n * layer reports that layer rather than every layer it passed.\n */\nfunction movedLayerIds(beforeIds: string[], afterIndex: Map<string, number>): Set<string> {\n const common = beforeIds.filter((id) => afterIndex.has(id));\n const seq = common.map((id) => afterIndex.get(id)!);\n // Patience-sorting LIS with predecessor links.\n const tails: number[] = [];\n const prev = new Array<number>(seq.length).fill(-1);\n for (let i = 0; i < seq.length; i++) {\n let lo = 0;\n let hi = tails.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (seq[tails[mid]] < seq[i]) lo = mid + 1;\n else hi = mid;\n }\n if (lo > 0) prev[i] = tails[lo - 1];\n tails[lo] = i;\n }\n const stable = new Set<string>();\n for (let i = tails.length ? tails[tails.length - 1] : -1; i >= 0; i = prev[i]) {\n stable.add(common[i]);\n }\n return new Set(common.filter((id) => !stable.has(id)));\n}\n\nfunction diffLayer(\n beforeProject: GeoLibreProject,\n afterProject: GeoLibreProject,\n a: GeoLibreLayer,\n b: GeoLibreLayer,\n maxLength: number,\n): LayerDiff {\n const diff = emptyLayerDiff(b, \"changed\");\n if (a.name !== b.name) diff.renamed = { before: a.name, after: b.name };\n if (Boolean(a.visible) !== Boolean(b.visible)) {\n diff.visibility = { before: Boolean(a.visible), after: Boolean(b.visible) };\n }\n if (!jsonValuesEqual(a.opacity, b.opacity))\n diff.opacity = { before: a.opacity, after: b.opacity };\n\n const styleA = effectiveStyle(beforeProject, a);\n const styleB = effectiveStyle(afterProject, b);\n if (!jsonValuesEqual(styleA, styleB)) {\n diffRecords(styleA, styleB, { skip: new Set([\"labels\"]), maxLength }, diff.style);\n const labelsA = styleA?.labels;\n const labelsB = styleB?.labels;\n if (!jsonValuesEqual(labelsA, labelsB)) {\n diffRecords(\n isPlainObject(labelsA) ? labelsA : undefined,\n isPlainObject(labelsB) ? labelsB : undefined,\n { maxLength },\n diff.labels,\n );\n }\n }\n\n diffRecords(\n { filterExpression: a.filterExpression, quickFilters: a.quickFilters },\n { filterExpression: b.filterExpression, quickFilters: b.quickFilters },\n { depth: 0, maxLength },\n diff.filter,\n );\n\n if (a.type !== b.type) {\n diff.source.push({ path: \"type\", before: a.type, after: b.type });\n }\n if (!jsonValuesEqual(a.source, b.source)) {\n diffRecords(a.source, b.source, { prefix: \"source.\", depth: 0, maxLength }, diff.source);\n }\n if (a.sourcePath !== b.sourcePath) {\n diff.source.push({\n path: \"sourcePath\",\n before: previewValue(a.sourcePath, maxLength),\n after: previewValue(b.sourcePath, maxLength),\n });\n }\n\n diffRecords(\n a as unknown as Record<string, unknown>,\n b as unknown as Record<string, unknown>,\n { skip: LAYER_GROUPED_KEYS, depth: 1, maxLength },\n diff.other,\n );\n\n if (a.geojson !== b.geojson && (a.geojson || b.geojson)) {\n const features = diffFeatureCollections(a.geojson, b.geojson);\n if (features.added || features.removed || features.modified) diff.features = features;\n }\n return diff;\n}\n\nfunction layerHasChanges(diff: LayerDiff): boolean {\n return Boolean(\n diff.renamed ||\n diff.moved ||\n diff.visibility ||\n diff.opacity ||\n diff.features ||\n diff.style.length ||\n diff.labels.length ||\n diff.filter.length ||\n diff.source.length ||\n diff.other.length,\n );\n}\n\nfunction layerChangeCount(diff: LayerDiff): number {\n return (\n (diff.renamed ? 1 : 0) +\n (diff.moved ? 1 : 0) +\n (diff.visibility ? 1 : 0) +\n (diff.opacity ? 1 : 0) +\n (diff.features ? 1 : 0) +\n diff.style.length +\n diff.labels.length +\n diff.filter.length +\n diff.source.length +\n diff.other.length\n );\n}\n\n// ---------------------------------------------------------------------------\n// Plugins\n// ---------------------------------------------------------------------------\n\nfunction diffPlugins(\n before: ProjectPluginState | undefined,\n after: ProjectPluginState | undefined,\n maxLength: number,\n): { plugins: PluginDiff[]; manifests: { added: string[]; removed: string[] } } {\n const activeA = new Set(before?.activePluginIds ?? []);\n const activeB = new Set(after?.activePluginIds ?? []);\n const settingsA = before?.settings ?? {};\n const settingsB = after?.settings ?? {};\n const positionsA = before?.mapControlPositions ?? {};\n const positionsB = after?.mapControlPositions ?? {};\n const ids = new Set([\n ...activeA,\n ...activeB,\n ...Object.keys(settingsA),\n ...Object.keys(settingsB),\n ...Object.keys(positionsA),\n ...Object.keys(positionsB),\n ]);\n const plugins: PluginDiff[] = [];\n for (const id of [...ids].sort()) {\n const changes: ProjectValueChange[] = [];\n const wasActive = activeA.has(id);\n const isActive = activeB.has(id);\n if (!jsonValuesEqual(positionsA[id], positionsB[id])) {\n changes.push({\n path: \"position\",\n before: previewValue(positionsA[id], maxLength),\n after: previewValue(positionsB[id], maxLength),\n });\n }\n const sa = settingsA[id];\n const sb = settingsB[id];\n if (!jsonValuesEqual(sa, sb)) {\n if (isPlainObject(sa) || isPlainObject(sb)) {\n diffRecords(\n isPlainObject(sa) ? sa : undefined,\n isPlainObject(sb) ? sb : undefined,\n { prefix: \"settings.\", depth: 0, maxLength },\n changes,\n );\n } else {\n changes.push({\n path: \"settings\",\n before: previewValue(sa, maxLength),\n after: previewValue(sb, maxLength),\n });\n }\n }\n if (wasActive !== isActive) {\n plugins.push({ id, status: isActive ? \"added\" : \"removed\", changes });\n } else if (changes.length > 0) {\n plugins.push({ id, status: \"changed\", changes });\n }\n }\n const urlsA = new Set(before?.manifestUrls ?? []);\n const urlsB = new Set(after?.manifestUrls ?? []);\n return {\n plugins,\n manifests: {\n added: [...urlsB].filter((url) => !urlsA.has(url)),\n removed: [...urlsA].filter((url) => !urlsB.has(url)),\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Project\n// ---------------------------------------------------------------------------\n\nfunction numbersDiffer(a: unknown, b: unknown): boolean {\n if (typeof a === \"number\" && typeof b === \"number\") return Math.abs(a - b) > CAMERA_EPSILON;\n return !jsonValuesEqual(a, b);\n}\n\n/**\n * Compute a structured summary of what changed from `before` to `after`.\n *\n * Layers are matched by id. Embedded GeoJSON is compared per feature (by id,\n * falling back to geometry hash) and each feature is hashed once per object,\n * so diffing the same parsed project against several others reuses its\n * hashes; callers keep and pass the same parsed project objects to benefit.\n *\n * @param before - The earlier project (e.g. an autosave snapshot).\n * @param after - The later project (e.g. the current project).\n * @param options - Preview length and other tuning.\n * @returns The grouped change summary.\n */\nexport function diffProjects(\n before: GeoLibreProject,\n after: GeoLibreProject,\n options: ProjectDiffOptions = {},\n): ProjectDiff {\n const maxLength = options.maxPreviewLength ?? DEFAULT_PREVIEW_LENGTH;\n const layersA = Array.isArray(before.layers) ? before.layers : [];\n const layersB = Array.isArray(after.layers) ? after.layers : [];\n const byIdA = new Map(layersA.map((layer) => [layer.id, layer]));\n const byIdB = new Map(layersB.map((layer) => [layer.id, layer]));\n const indexA = new Map(layersA.map((layer, i) => [layer.id, i]));\n const indexB = new Map(layersB.map((layer, i) => [layer.id, i]));\n const moved = movedLayerIds(\n layersA.map((layer) => layer.id),\n indexB,\n );\n\n const added: LayerDiff[] = [];\n const removed: LayerDiff[] = [];\n const changed: LayerDiff[] = [];\n for (const layer of layersB) {\n if (byIdA.has(layer.id)) continue;\n const diff = emptyLayerDiff(layer, \"added\");\n const count = featureCountOf(layer);\n if (count !== undefined) diff.featureCount = count;\n added.push(diff);\n }\n for (const layer of layersA) {\n if (byIdB.has(layer.id)) continue;\n const diff = emptyLayerDiff(layer, \"removed\");\n const count = featureCountOf(layer);\n if (count !== undefined) diff.featureCount = count;\n removed.push(diff);\n }\n // Report changed layers top-most first, matching the Layers panel.\n for (let i = layersB.length - 1; i >= 0; i--) {\n const b = layersB[i];\n const a = byIdA.get(b.id);\n if (!a) continue;\n const identical = a === b && before.styles?.[b.id] === after.styles?.[b.id];\n const diff = identical\n ? emptyLayerDiff(b, \"changed\")\n : diffLayer(before, after, a, b, maxLength);\n if (moved.has(b.id)) diff.moved = { before: indexA.get(b.id)!, after: indexB.get(b.id)! };\n if (layerHasChanges(diff)) changed.push(diff);\n }\n\n const camera: ProjectValueChange[] = [];\n const viewA = before.mapView ?? ({} as GeoLibreProject[\"mapView\"]);\n const viewB = after.mapView ?? ({} as GeoLibreProject[\"mapView\"]);\n const centerA = viewA.center ?? [];\n const centerB = viewB.center ?? [];\n if (numbersDiffer(centerA[0], centerB[0]) || numbersDiffer(centerA[1], centerB[1])) {\n const fmt = (c: unknown[]) =>\n c.length === 2 && c.every((n) => typeof n === \"number\")\n ? `${(c[0] as number).toFixed(5)}, ${(c[1] as number).toFixed(5)}`\n : undefined;\n camera.push({ path: \"center\", before: fmt(centerA), after: fmt(centerB) });\n }\n for (const key of [\"zoom\", \"bearing\", \"pitch\"] as const) {\n if (numbersDiffer(viewA[key], viewB[key])) {\n const fmt = (n: unknown) =>\n typeof n === \"number\" ? String(Math.round(n * 100) / 100) : previewValue(n, maxLength);\n camera.push({ path: key, before: fmt(viewA[key]), after: fmt(viewB[key]) });\n }\n }\n\n const basemap: ProjectValueChange[] = [];\n diffRecords(\n {\n basemapStyleUrl: before.basemapStyleUrl,\n basemapVisible: before.basemapVisible,\n basemapOpacity: before.basemapOpacity,\n blankBackgroundColor: before.blankBackgroundColor ?? undefined,\n primaryRenderer: before.primaryRenderer,\n },\n {\n basemapStyleUrl: after.basemapStyleUrl,\n basemapVisible: after.basemapVisible,\n basemapOpacity: after.basemapOpacity,\n blankBackgroundColor: after.blankBackgroundColor ?? undefined,\n primaryRenderer: after.primaryRenderer,\n },\n { depth: 0, maxLength: Math.max(maxLength, 160) },\n basemap,\n );\n\n const projection: ProjectValueChange[] = [];\n const prefsA = (before.preferences ?? {}) as unknown as Record<string, unknown>;\n const prefsB = (after.preferences ?? {}) as unknown as Record<string, unknown>;\n const mapA = isPlainObject(prefsA.map) ? prefsA.map : {};\n const mapB = isPlainObject(prefsB.map) ? prefsB.map : {};\n if (!jsonValuesEqual(mapA.projection, mapB.projection)) {\n projection.push({\n path: \"projection\",\n before: previewValue(mapA.projection, maxLength),\n after: previewValue(mapB.projection, maxLength),\n });\n }\n const preferences: ProjectValueChange[] = [];\n if (!jsonValuesEqual(prefsA, prefsB)) {\n diffRecords(\n { ...prefsA, map: { ...mapA, projection: undefined } },\n { ...prefsB, map: { ...mapB, projection: undefined } },\n { depth: 1, maxLength },\n preferences,\n );\n }\n\n const { plugins, manifests } = diffPlugins(before.plugins, after.plugins, maxLength);\n\n const metadata: ProjectValueChange[] = [];\n if (before.name !== after.name) {\n metadata.push({\n path: \"name\",\n before: previewValue(before.name, maxLength),\n after: previewValue(after.name, maxLength),\n });\n }\n diffRecords(before.metadata, after.metadata, { depth: 0, maxLength }, metadata);\n // Lead with the description, the field users edit, ahead of other keys.\n metadata.sort((x, y) => metadataRank(x.path) - metadataRank(y.path));\n\n const sections: string[] = [];\n const projectA = before as unknown as Record<string, unknown>;\n const projectB = after as unknown as Record<string, unknown>;\n for (const key of new Set([...Object.keys(projectA), ...Object.keys(projectB)])) {\n if (PROJECT_GROUPED_KEYS.has(key)) continue;\n if (!sameForReport(projectA[key], projectB[key])) sections.push(key);\n }\n sections.sort();\n\n const changeCount =\n added.length +\n removed.length +\n changed.reduce((sum, diff) => sum + layerChangeCount(diff), 0) +\n camera.length +\n basemap.length +\n projection.length +\n preferences.length +\n // A plugin counts each changed setting, like a layer counts each key; a\n // bare enable/disable counts once.\n plugins.reduce((sum, plugin) => sum + Math.max(1, plugin.changes.length), 0) +\n manifests.added.length +\n manifests.removed.length +\n metadata.length +\n sections.length;\n\n return {\n layers: { added, removed, changed, reordered: moved.size > 0 },\n camera,\n basemap,\n projection,\n preferences,\n plugins,\n pluginManifests: manifests,\n metadata,\n sections,\n changeCount,\n };\n}\n\nfunction metadataRank(path: string): number {\n if (path === \"name\") return 0;\n if (path === \"description\") return 1;\n return 2;\n}\n","/**\n * Trim a float sample to something readable. A 32-bit raster value decoded to a\n * JS double prints all 17 digits of its binary representation\n * (`48.11851119995117`), which is noise past the sensor's precision — six\n * significant digits is more than any COG carries. Integers are left alone so a\n * classification code is never shown in exponential form.\n *\n * @param value A decoded raster sample.\n * @returns The sample rendered for display.\n */\nexport function formatPixelValue(value: number): string {\n if (!Number.isFinite(value)) return String(value);\n if (Number.isInteger(value)) return String(value);\n return String(Number(value.toPrecision(6)));\n}\n","/**\n * Amazon S3 (and S3-compatible) object URLs: parsing, public HTTPS mapping,\n * SigV4 query-string presigning, and the registry the app uses to plug\n * credentialed signing into every reader.\n *\n * Readers in GeoLibre take a plain HTTPS URL (geotiff.js, cog-tiler-wasm,\n * pmtiles, DuckDB-WASM's HTTP reader, the vector control). A presigned URL is\n * the one form of authenticated S3 access all of them accept without a custom\n * fetch hook, so private objects are read by signing the URL first and handing\n * the reader the result. Layers keep the unsigned source URL (`s3://…` or the\n * plain object URL); the signed one is minted again on every load, so a saved\n * project never carries a signature or a session token.\n */\n\n/** A bucket and object key, with the endpoint the URL named, if any. */\nexport interface S3ObjectLocation {\n bucket: string;\n /** The raw (decoded) object key, without a leading slash. */\n key: string;\n /** The region an AWS HTTPS URL named in its host, when it named one. */\n region?: string;\n}\n\n/** Where and how a bucket is addressed. */\nexport interface S3EndpointConfig {\n /** Signing region, e.g. `us-east-1`. S3-compatible stores often accept `auto`. */\n region: string;\n /**\n * Custom endpoint origin for S3-compatible stores (MinIO, Cloudflare R2,\n * Wasabi, Ceph), e.g. `https://minio.example.com:9000`. Empty for AWS.\n */\n endpoint?: string;\n /** Address the bucket in the path instead of the host name. */\n pathStyle?: boolean;\n}\n\n/** AWS credentials used to sign a request. */\nexport interface S3Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n /** Epoch milliseconds at which temporary credentials stop working. */\n expiresAt?: number;\n}\n\n/** A presigned URL and the moment it stops working. */\nexport interface S3SignedUrl {\n href: string;\n /** Epoch milliseconds. */\n expiresAt: number;\n}\n\n/** SigV4 allows a presigned URL to live at most seven days. */\nexport const S3_MAX_PRESIGN_SECONDS = 7 * 24 * 60 * 60;\n\nconst S3_SCHEME = /^s3a?:\\/\\//i;\n// bucket.s3.amazonaws.com, bucket.s3.us-west-2.amazonaws.com,\n// bucket.s3-us-west-2.amazonaws.com, bucket.s3.dualstack.us-west-2.amazonaws.com\nconst VIRTUAL_HOST = /^(.+)\\.s3(?:[.-](?:dualstack\\.)?([a-z0-9-]+))?\\.amazonaws\\.com(?:\\.cn)?$/i;\n// s3.amazonaws.com, s3.us-west-2.amazonaws.com, s3-us-west-2.amazonaws.com\nconst PATH_HOST = /^s3(?:[.-](?:dualstack\\.)?([a-z0-9-]+))?\\.amazonaws\\.com(?:\\.cn)?$/i;\n\nfunction regionFromHost(value: string | undefined): string | undefined {\n // `s3-external-1` and `s3.dualstack` carry no region of their own.\n if (!value || value === \"external-1\" || value === \"dualstack\") return undefined;\n return value.toLowerCase();\n}\n\n/** Static-website endpoints serve HTML, not the REST API a signature targets. */\nfunction isWebsiteRegion(value: string | undefined): boolean {\n return Boolean(value && /^website/i.test(value));\n}\n\nfunction decodeKey(path: string): string {\n try {\n return decodeURIComponent(path);\n } catch {\n return path;\n }\n}\n\n/**\n * Parses an `s3://bucket/key` URI or an AWS S3 HTTPS object URL\n * (virtual-hosted or path-style, any region) into its bucket and key.\n * Query strings and fragments are ignored, so a presigned URL parses to the\n * object it signs.\n *\n * @param url The URL to parse.\n * @returns The bucket and key, or null when the URL is not an S3 object URL.\n */\nexport function parseS3Url(url: string): S3ObjectLocation | null {\n const trimmed = url.trim();\n if (S3_SCHEME.test(trimmed)) {\n const rest = trimmed.replace(S3_SCHEME, \"\").split(/[?#]/)[0];\n const slash = rest.indexOf(\"/\");\n const bucket = slash === -1 ? rest : rest.slice(0, slash);\n const key = slash === -1 ? \"\" : rest.slice(slash + 1);\n return isSafeBucketName(bucket) ? { bucket, key } : null;\n }\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n return null;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return null;\n const host = parsed.hostname.toLowerCase();\n const path = parsed.pathname.replace(/^\\//, \"\");\n const virtual = host.match(VIRTUAL_HOST);\n if (virtual && !PATH_HOST.test(host)) {\n if (isWebsiteRegion(virtual[2])) return null;\n const region = regionFromHost(virtual[2]);\n return { bucket: virtual[1], key: decodeKey(path), ...(region ? { region } : {}) };\n }\n const pathStyle = host.match(PATH_HOST);\n if (pathStyle) {\n if (isWebsiteRegion(pathStyle[1])) return null;\n const slash = path.indexOf(\"/\");\n const bucket = slash === -1 ? path : path.slice(0, slash);\n if (!bucket) return null;\n const region = regionFromHost(pathStyle[1]);\n return {\n bucket: decodeKey(bucket),\n key: slash === -1 ? \"\" : decodeKey(path.slice(slash + 1)),\n ...(region ? { region } : {}),\n };\n }\n return null;\n}\n\n/**\n * Bucket names are letters, digits, dots, dashes, and underscores (the last\n * two outside AWS's own rules, for S3-compatible stores). Anything else, a\n * backslash or `@` say, could move a virtual-hosted URL's authority to\n * another host before a signature is attached, so it is refused outright.\n */\nconst SAFE_BUCKET_NAME = /^[A-Za-z0-9._-]{1,255}$/;\n\nfunction isSafeBucketName(bucket: string): boolean {\n return SAFE_BUCKET_NAME.test(bucket);\n}\n\n/** Whether `url` is an `s3://` URI (as opposed to an HTTPS object URL). */\nexport function isS3Uri(url: string): boolean {\n return S3_SCHEME.test(url.trim());\n}\n\n/** Formats a location as an `s3://bucket/key` URI. */\nexport function formatS3Uri(location: S3ObjectLocation): string {\n return `s3://${location.bucket}/${location.key}`;\n}\n\n/**\n * URI-encodes a key the way SigV4 canonicalizes an S3 path: every byte except\n * the unreserved set and `/` is percent-encoded, once.\n */\nexport function encodeS3Key(key: string): string {\n return key\n .split(\"/\")\n .map((segment) =>\n encodeURIComponent(segment).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n ),\n )\n .join(\"/\");\n}\n\n/** A custom endpoint's path, without a trailing slash (\"\" for none). */\nfunction endpointPathPrefix(base: URL): string {\n return base.pathname.replace(/\\/+$/, \"\");\n}\n\n/**\n * Builds the HTTPS URL of an object.\n *\n * AWS buckets whose name contains a dot are addressed path-style, since the\n * `*.s3.amazonaws.com` wildcard certificate covers one label only.\n *\n * @param location The bucket and key.\n * @param config The endpoint; without one the global AWS endpoint is used.\n * @returns The unsigned object URL.\n */\nexport function s3ObjectHttpsUrl(\n location: S3ObjectLocation,\n config: Partial<S3EndpointConfig> = {},\n): string {\n if (location.bucket && !isSafeBucketName(location.bucket)) {\n throw new Error(`Invalid S3 bucket name: ${location.bucket}`);\n }\n const key = encodeS3Key(location.key);\n const endpoint = config.endpoint?.trim().replace(/\\/+$/, \"\");\n // No bucket addresses the service itself (ListBuckets).\n if (!location.bucket) {\n if (endpoint) {\n const base = new URL(endpoint.includes(\"://\") ? endpoint : `https://${endpoint}`);\n return `${base.protocol}//${base.host}${endpointPathPrefix(base)}/`;\n }\n const serviceRegion = config.region || location.region;\n return serviceRegion && serviceRegion !== \"us-east-1\"\n ? `https://s3.${serviceRegion}.amazonaws.com/`\n : \"https://s3.amazonaws.com/\";\n }\n if (endpoint) {\n const base = new URL(endpoint.includes(\"://\") ? endpoint : `https://${endpoint}`);\n // An S3-compatible gateway may sit under a path (`https://host/s3`).\n const prefix = endpointPathPrefix(base);\n return config.pathStyle\n ? `${base.protocol}//${base.host}${prefix}/${encodeS3Key(location.bucket)}/${key}`\n : `${base.protocol}//${location.bucket}.${base.host}${prefix}/${key}`;\n }\n const region = config.region || location.region;\n const host = region && region !== \"us-east-1\" ? `s3.${region}.amazonaws.com` : \"s3.amazonaws.com\";\n if (config.pathStyle || location.bucket.includes(\".\")) {\n return `https://${host}/${encodeS3Key(location.bucket)}/${key}`;\n }\n return `https://${location.bucket}.${host}/${key}`;\n}\n\n// ---------------------------------------------------------------------------\n// SigV4 query-string signing\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nasync function sha256Hex(value: string): Promise<string> {\n return toHex(await crypto.subtle.digest(\"SHA-256\", encoder.encode(value)));\n}\n\nasync function hmac(key: ArrayBuffer | Uint8Array, value: string): Promise<ArrayBuffer> {\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n key as BufferSource,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return crypto.subtle.sign(\"HMAC\", cryptoKey, encoder.encode(value));\n}\n\n/** RFC 3986 encoding, as SigV4 requires for query names and values. */\nfunction encodeRfc3986(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n}\n\nfunction amzDate(date: Date): { stamp: string; day: string } {\n const stamp = date\n .toISOString()\n .replace(/[-:]/g, \"\")\n .replace(/\\.\\d{3}/, \"\");\n return { stamp, day: stamp.slice(0, 8) };\n}\n\nexport interface PresignS3Options {\n /** The unsigned HTTPS object URL (see {@link s3ObjectHttpsUrl}). */\n url: string;\n region: string;\n credentials: S3Credentials;\n /** Lifetime in seconds, clamped to 1…{@link S3_MAX_PRESIGN_SECONDS}. */\n expiresIn: number;\n /** HTTP method the URL authorizes. Defaults to GET. */\n method?: string;\n /** Signing time; defaults to now. Exposed for tests. */\n now?: Date;\n}\n\n/**\n * Presigns an S3 request with AWS Signature Version 4 in the query string.\n * Runs on WebCrypto, so it works in the browser, the Tauri webview, and Node.\n *\n * The URL authorizes exactly one method: a GET URL cannot answer a HEAD, so\n * readers that probe with HEAD need a GET range request instead.\n *\n * @param options The object URL, region, credentials, and lifetime.\n * @returns The presigned URL.\n */\nexport async function presignS3Url(options: PresignS3Options): Promise<string> {\n const { credentials, region } = options;\n const method = (options.method ?? \"GET\").toUpperCase();\n const url = new URL(options.url);\n const { stamp, day } = amzDate(options.now ?? new Date());\n const scope = `${day}/${region}/s3/aws4_request`;\n const expiresIn = Math.max(1, Math.min(S3_MAX_PRESIGN_SECONDS, Math.floor(options.expiresIn)));\n\n const params: Array<[string, string]> = [];\n url.searchParams.forEach((value, name) => {\n if (!name.toLowerCase().startsWith(\"x-amz-\")) params.push([name, value]);\n });\n params.push(\n [\"X-Amz-Algorithm\", \"AWS4-HMAC-SHA256\"],\n [\"X-Amz-Credential\", `${credentials.accessKeyId}/${scope}`],\n [\"X-Amz-Date\", stamp],\n [\"X-Amz-Expires\", String(expiresIn)],\n [\"X-Amz-SignedHeaders\", \"host\"],\n );\n if (credentials.sessionToken) params.push([\"X-Amz-Security-Token\", credentials.sessionToken]);\n const canonicalQuery = params\n .map(([name, value]) => [encodeRfc3986(name), encodeRfc3986(value)] as const)\n .sort(([a, av], [b, bv]) => (a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0))\n .map(([name, value]) => `${name}=${value}`)\n .join(\"&\");\n\n // The URL constructor already percent-encoded the path; S3 signs it as sent.\n const canonicalPath = url.pathname || \"/\";\n const canonicalRequest = [\n method,\n canonicalPath,\n canonicalQuery,\n `host:${url.host}\\n`,\n \"host\",\n \"UNSIGNED-PAYLOAD\",\n ].join(\"\\n\");\n const stringToSign = [\"AWS4-HMAC-SHA256\", stamp, scope, await sha256Hex(canonicalRequest)].join(\n \"\\n\",\n );\n\n const dateKey = await hmac(encoder.encode(`AWS4${credentials.secretAccessKey}`), day);\n const regionKey = await hmac(dateKey, region);\n const serviceKey = await hmac(regionKey, \"s3\");\n const signingKey = await hmac(serviceKey, \"aws4_request\");\n const signature = toHex(await hmac(signingKey, stringToSign));\n\n return `${url.origin}${canonicalPath}?${canonicalQuery}&X-Amz-Signature=${signature}`;\n}\n\n// ---------------------------------------------------------------------------\n// Signer registry\n// ---------------------------------------------------------------------------\n\n/** One signed S3 GET: an object read, or a bucket-level call such as ListObjectsV2. */\nexport interface S3PresignRequest {\n bucket: string;\n /** Object key; empty for a bucket-level request. */\n key: string;\n /** Extra query parameters to sign (e.g. `list-type=2`). */\n query?: Record<string, string>;\n /** Region an AWS URL named, used ahead of the connection's default. */\n region?: string;\n /**\n * Sign with this connection instead of the one matched by bucket name (the\n * S3 browser listing a chosen connection's buckets, where `bucket` is empty).\n */\n connectionId?: string;\n}\n\n/** A configured connection, as the S3 browser lists it. No secrets. */\nexport interface S3ConnectionSummary {\n id: string;\n name: string;\n /** Bucket patterns the connection covers; empty means every bucket. */\n buckets: string[];\n}\n\n/**\n * Presigns S3 requests with the app's configured credentials. `presign`\n * resolves to null when no configured connection covers the bucket, in which\n * case the object is read anonymously.\n */\nexport interface S3UrlSigner {\n /** Whether a configured connection covers `bucket`. Must be synchronous. */\n covers(bucket: string): boolean;\n /** Presigns a GET; rejects when the connection's credentials cannot be resolved. */\n presign(request: S3PresignRequest, signal?: AbortSignal): Promise<S3SignedUrl | null>;\n /** The configured connections, for pickers. */\n connections(): S3ConnectionSummary[];\n /**\n * Fetches a presigned bucket-level URL (listing). The desktop app routes it\n * through its native HTTP client, which needs no bucket CORS rule.\n */\n fetchText?(url: string, signal?: AbortSignal): Promise<{ status: number; body: string }>;\n /** The persisted location (`s3://bucket/prefix/`) the S3 browser opens at, or \"\". */\n defaultLocation?(): string;\n /** Persists the S3 browser's default location; \"\" clears it. */\n setDefaultLocation?(location: string): void;\n}\n\nlet activeSigner: S3UrlSigner | null = null;\n/** Presigned URL → the unsigned URL it was minted for. Bounded. */\nconst sourceBySignedHref = new Map<string, string>();\nconst SIGNED_SOURCE_LIMIT = 500;\n\n/** Installs (or with null, removes) the app's S3 signer. */\nexport function registerS3UrlSigner(signer: S3UrlSigner | null): void {\n activeSigner = signer;\n}\n\n/**\n * Whether reading `url` needs the S3 signer: it names an S3 object in a bucket\n * a configured connection covers.\n */\nexport function isCredentialedS3Url(url: string): boolean {\n if (!activeSigner) return false;\n const location = parseS3Url(url);\n return Boolean(location && activeSigner.covers(location.bucket));\n}\n\n/** Records that `signedHref` reads `sourceUrl`, so a layer can keep the latter. */\nfunction rememberSignedSource(signedHref: string, sourceUrl: string): void {\n // Least recently used goes first: deleting before setting moves a URL that\n // is signed (handed out) again to the back of the eviction order.\n sourceBySignedHref.delete(signedHref);\n if (sourceBySignedHref.size >= SIGNED_SOURCE_LIMIT) {\n const oldest = sourceBySignedHref.keys().next().value;\n if (oldest !== undefined) sourceBySignedHref.delete(oldest);\n }\n sourceBySignedHref.set(signedHref, sourceUrl);\n}\n\n/** Removes the SigV4 query parameters (credential, token, signature) from a URL. */\nfunction stripPresignParameters(url: string): string {\n try {\n const parsed = new URL(url);\n for (const name of [...parsed.searchParams.keys()]) {\n if (name.toLowerCase().startsWith(\"x-amz-\")) parsed.searchParams.delete(name);\n }\n return parsed.href;\n } catch {\n return url;\n }\n}\n\n/**\n * The unsigned URL a presigned one was minted for by\n * {@link resolveReadableUrl}, or `url` itself. Layer sync runs every URL a\n * control reports through this, so the store (and a saved project) keeps the\n * `s3://` source rather than an expiring signature.\n */\nexport function unsignedSourceUrl(url: string): string;\nexport function unsignedSourceUrl(url: string | undefined): string | undefined;\nexport function unsignedSourceUrl(url: string | undefined): string | undefined {\n if (url === undefined) return undefined;\n const known = sourceBySignedHref.get(url);\n if (known !== undefined) return known;\n // A presigned URL whose mapping was evicted must still never reach a saved\n // project: fall back to the object it signs, without the signature.\n if (!/[?&]x-amz-signature=/i.test(url)) return url;\n const location = parseS3Url(url);\n // An `s3://` URI cannot carry a key with `?` or `#` (they would be read as a\n // query or fragment), so such keys keep the object URL form.\n return location && !/[?#]/.test(location.key)\n ? formatS3Uri(location)\n : stripPresignParameters(url);\n}\n\n/**\n * Resolves a data URL into one a plain HTTP reader can fetch:\n *\n * - an S3 object in a bucket a configured connection covers → a presigned URL;\n * - any other `s3://` URI → the public HTTPS object URL (anonymous read);\n * - anything else → unchanged.\n *\n * @param url The URL a layer or query names.\n * @param signal Aborts a pending credential lookup.\n * @returns The URL to hand the reader.\n * @throws When the bucket is covered but its credentials cannot be resolved.\n */\nexport async function resolveReadableUrl(url: string, signal?: AbortSignal): Promise<string> {\n const location = parseS3Url(url);\n if (!location) return url;\n if (activeSigner?.covers(location.bucket)) {\n const signed = await activeSigner.presign(\n { bucket: location.bucket, key: location.key, region: location.region },\n signal,\n );\n if (signed) {\n rememberSignedSource(signed.href, url);\n return signed.href;\n }\n }\n return isS3Uri(url) ? s3ObjectHttpsUrl(location) : url;\n}\n\n/** The installed signer, for the S3 browser. */\nexport function getS3UrlSigner(): S3UrlSigner | null {\n return activeSigner;\n}\n\n// ---------------------------------------------------------------------------\n// ListObjectsV2\n// ---------------------------------------------------------------------------\n\n/** One page of a ListObjectsV2 response. */\nexport interface S3ListPage {\n /** Common prefixes (\"folders\") under the requested prefix. */\n prefixes: string[];\n objects: Array<{ key: string; size: number; lastModified?: string }>;\n /** Present when more results follow. */\n nextContinuationToken?: string;\n}\n\nfunction decodeXmlText(value: string): string {\n return value\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\")\n .replace(/&#(\\d+);/g, (_, code: string) => String.fromCodePoint(Number(code)))\n .replace(/&#x([0-9a-f]+);/gi, (_, code: string) => String.fromCodePoint(parseInt(code, 16)))\n .replace(/&amp;/g, \"&\");\n}\n\nfunction xmlTag(block: string, tag: string): string | undefined {\n const match = block.match(new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`));\n return match ? decodeXmlText(match[1]) : undefined;\n}\n\nfunction xmlBlocks(xml: string, tag: string): string[] {\n return Array.from(xml.matchAll(new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`, \"g\")), (m) => m[1]);\n}\n\n/**\n * Parses a ListObjectsV2 XML body. S3's listing XML is flat and predictable,\n * so this reads it with patterns rather than an XML DOM, which keeps it usable\n * in workers and Node tests.\n */\nexport function parseS3ListObjects(xml: string): S3ListPage {\n const prefixes = xmlBlocks(xml, \"CommonPrefixes\")\n .map((block) => xmlTag(block, \"Prefix\"))\n .filter((value): value is string => Boolean(value));\n const objects = xmlBlocks(xml, \"Contents\").flatMap((block) => {\n const key = xmlTag(block, \"Key\");\n if (key === undefined) return [];\n const lastModified = xmlTag(block, \"LastModified\");\n return [\n { key, size: Number(xmlTag(block, \"Size\") ?? 0), ...(lastModified ? { lastModified } : {}) },\n ];\n });\n const truncated = xmlTag(xml, \"IsTruncated\") === \"true\";\n const token = xmlTag(xml, \"NextContinuationToken\");\n return { prefixes, objects, ...(truncated && token ? { nextContinuationToken: token } : {}) };\n}\n\n/** The error code and message of an S3 XML error body, when it is one. */\nexport function parseS3Error(\n xml: string,\n): { code: string; message: string; region?: string } | null {\n const block = xmlBlocks(xml, \"Error\")[0];\n if (block === undefined) return null;\n // A wrong-region signature names the region; an unsigned request to the\n // wrong endpoint (301 PermanentRedirect) names the right endpoint instead.\n const endpoint = xmlTag(block, \"Endpoint\");\n const region =\n xmlTag(block, \"Region\") ?? (endpoint ? parseS3Url(`https://${endpoint}/`)?.region : undefined);\n return {\n code: xmlTag(block, \"Code\") ?? \"Error\",\n message: xmlTag(block, \"Message\") ?? \"\",\n ...(region ? { region } : {}),\n };\n}\n\n/** Bucket names from a ListBuckets (`GET /` on the service) XML body. */\nexport function parseS3BucketList(xml: string): string[] {\n return xmlBlocks(xml, \"Bucket\")\n .map((block) => xmlTag(block, \"Name\"))\n .filter((name): name is string => Boolean(name));\n}\n\n// ---------------------------------------------------------------------------\n// CORS diagnosis\n// ---------------------------------------------------------------------------\n\n/**\n * How browsers word a request they blocked or could not complete, plus the\n * readers that already guess at CORS themselves (maplibre-gl-lidar's message\n * tells the user to use a file picker, which is wrong for a bucket).\n */\nconst NETWORK_FAILURE =\n /failed to fetch|networkerror|load failed|network request failed|err_failed|cross-origin|\\bcors\\b/i;\n\nfunction errorText(error: unknown): string {\n const parts: string[] = [];\n let current: unknown = error;\n for (let depth = 0; current && depth < 4; depth += 1) {\n parts.push(current instanceof Error ? `${current.name}: ${current.message}` : String(current));\n current = current instanceof Error ? (current as Error & { cause?: unknown }).cause : null;\n }\n return parts.join(\" | \");\n}\n\n/** A read the bucket's CORS configuration blocked. */\nexport class S3CorsError extends Error {\n readonly bucket: string;\n readonly origin: string;\n\n constructor(message: string, bucket: string, origin: string, cause: unknown) {\n super(message, { cause });\n this.name = \"S3CorsError\";\n this.bucket = bucket;\n this.origin = origin;\n }\n}\n\n/** Translates `key`, falling back to `defaultValue` (an app API's `translate`). */\nexport type S3ErrorTranslate = (\n key: string,\n defaultValue: string,\n params?: Record<string, string | number>,\n) => string;\n\n/**\n * Explains why reading an S3 object failed when the browser only said\n * \"Failed to fetch\". A browser reports a request the bucket's CORS rules\n * blocked exactly like a network outage, so this repeats the request twice:\n * normally, where success means the failure was transient; then in `no-cors`\n * mode, which CORS cannot block. Only when the first fails and the second gets\n * an answer is the bucket's CORS configuration the cause.\n *\n * @param url The `s3://` URI or object URL that was being read.\n * @param error What the reader threw.\n * @param translate Turns the message into the UI language.\n * @returns An {@link S3CorsError} naming the bucket and the origin to allow,\n * or `error` unchanged when it is not a CORS refusal of an S3 read.\n */\nexport async function explainS3ReadError(\n url: string,\n error: unknown,\n translate?: S3ErrorTranslate,\n): Promise<unknown> {\n const location = parseS3Url(url);\n if (!location || error instanceof S3CorsError) return error;\n if (!NETWORK_FAILURE.test(errorText(error))) return error;\n if (typeof fetch !== \"function\" || typeof window === \"undefined\") return error;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 10_000);\n try {\n const href = await resolveReadableUrl(url);\n const probe = { signal: controller.signal, cache: \"no-store\" as const };\n // A normal (CORS) read first: if that works now, the failure was transient\n // and CORS is not the cause. The body is never needed, so each request is\n // cut off once its headers arrive.\n const corsAllowed = await fetch(href, probe).then(\n () => true,\n () => false,\n );\n if (corsAllowed) return error;\n // Blocked under CORS, yet S3 answers a no-cors request, which CORS cannot\n // block: the bucket's CORS configuration is what refuses this origin.\n await fetch(href, { ...probe, mode: \"no-cors\" });\n } catch {\n // Unreachable under no-cors too: a real network failure, not CORS.\n return error;\n } finally {\n controller.abort();\n clearTimeout(timer);\n }\n const origin = window.location.origin;\n const fallback =\n `The bucket \"${location.bucket}\" does not allow reads from ${origin}: its CORS configuration ` +\n \"is missing or does not list this origin. Add a CORS rule that allows GET and HEAD from this \" +\n \"origin and exposes Content-Length, Content-Range, and ETag (see Settings > Cloud Storage).\";\n const message = translate\n ? translate(\"s3Browser.corsError\", fallback, { bucket: location.bucket, origin })\n : fallback;\n return new S3CorsError(message, location.bucket, origin, error);\n}\n","import type { StoryMap } from \"./types\";\n\nconst SAMPLE_ASSET_BASE = \"https://opengeos.org/maplibre-gl-storymaps/assets\";\n\n/**\n * Build a ready-to-play sample story map.\n *\n * Mirrors the chapters of the `opengeos/maplibre-gl-storymaps` demo (five world\n * cities) so users can explore the feature instantly without authoring content.\n * A fresh object is returned on each call so callers can mutate it freely.\n *\n * @returns A populated {@link StoryMap} with five sample chapters.\n */\nexport function createSampleStoryMap(): StoryMap {\n return {\n title: \"A Tour of Five Cities\",\n subtitle: \"A scrollytelling map experience built with GeoLibre\",\n byline: \"By GeoLibre\",\n footer:\n 'Source: Wikipedia. Built with <a href=\"https://github.com/opengeos/GeoLibre\" target=\"_blank\">GeoLibre</a>, inspired by <a href=\"https://github.com/opengeos/maplibre-gl-storymaps\" target=\"_blank\">MapLibre Storytelling</a>.',\n theme: \"dark\",\n showMarkers: true,\n markerColor: \"#3fb1ce\",\n inset: true,\n insetPosition: \"bottom-left\",\n hideChapterNav: false,\n startSlide: \"none\",\n endSlide: \"none\",\n chapters: [\n {\n id: \"sample-san-francisco\",\n title: \"San Francisco, California\",\n image: `${SAMPLE_ASSET_BASE}/san-francisco.jpg`,\n description:\n \"San Francisco, a hilly city on the tip of a peninsula surrounded by the Pacific Ocean and San Francisco Bay, is known for its year-round fog, iconic Golden Gate Bridge, cable cars and colorful Victorian houses. <br><br>The city is also known for its vibrant tech industry, diverse neighborhoods, and rich cultural scene.\",\n alignment: \"left\",\n hidden: false,\n location: {\n center: [-122.4194, 37.7749],\n zoom: 11,\n pitch: 45,\n bearing: 0,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-new-york\",\n title: \"New York City, New York\",\n image: `${SAMPLE_ASSET_BASE}/new-york.jpg`,\n description:\n \"New York City comprises 5 boroughs sitting where the Hudson River meets the Atlantic Ocean. At its core is Manhattan, a densely populated borough that's among the world's major commercial, financial and cultural centers. <br><br>Its iconic sites include skyscrapers such as the Empire State Building and sprawling Central Park.\",\n alignment: \"right\",\n hidden: false,\n location: {\n center: [-74.006, 40.7128],\n zoom: 11,\n pitch: 60,\n bearing: -43.2,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: true,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-tokyo\",\n title: \"Tokyo, Japan\",\n image: `${SAMPLE_ASSET_BASE}/tokyo.jpg`,\n description:\n \"Tokyo, Japan's busy capital, mixes the ultramodern and the traditional, from neon-lit skyscrapers to historic temples. The opulent Meiji Shinto Shrine is known for its towering gate and surrounding woods. <br><br>The Imperial Palace sits amid large public gardens. The city's many museums offer exhibits ranging from classical art to a reconstructed kabuki theater.\",\n alignment: \"left\",\n hidden: false,\n location: {\n center: [139.6917, 35.6895],\n zoom: 10,\n pitch: 30,\n bearing: 20,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-sydney\",\n title: \"Sydney, Australia\",\n image: `${SAMPLE_ASSET_BASE}/sydney.jpg`,\n description:\n \"Sydney, capital of New South Wales and one of Australia's largest cities, is best known for its harbourfront Sydney Opera House, with a distinctive sail-like design. <br><br>Massive Darling Harbour and the smaller Circular Quay port are hubs of waterside life, with the arched Harbour Bridge and esteemed Royal Botanic Garden nearby.\",\n alignment: \"right\",\n hidden: false,\n location: {\n center: [151.2093, -33.8688],\n zoom: 11,\n pitch: 45,\n bearing: 0,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-cape-town\",\n title: \"Cape Town, South Africa\",\n image: `${SAMPLE_ASSET_BASE}/cape-town.jpg`,\n description:\n \"Cape Town is a port city on South Africa's southwest coast, on a peninsula beneath the imposing Table Mountain. Slowly rotating cable cars climb to the mountain's flat top, from which there are sweeping views of the city, the busy harbor and boats headed for Robben Island, the infamous prison that once held Nelson Mandela. <br><br>You can add as many chapters as you need to tell your story.\",\n alignment: \"left\",\n hidden: false,\n location: {\n center: [18.4241, -33.9249],\n zoom: 10,\n pitch: 50,\n bearing: 30,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n ],\n };\n}\n","/**\n * Helpers for following a story-map presentation's layer fades outside the\n * MapLibre paint model.\n *\n * Story playback fades layers by writing MapLibre paint properties directly\n * (see `MapController.setStoryLayerOpacity`) and records the applied values in\n * `ui.storymapLayerOpacity`. Anything that renders a layer some other way, such\n * as deck.gl feature diagrams and 3D Z-value geometry, or that lists layers\n * for the reader, such as the on-map Legend, reads that record through these\n * helpers so a faded-out layer disappears everywhere at once.\n */\nimport type { GeoLibreLayer, MapViewState, StoryChapterLocation } from \"./types\";\n\n/** The clamped opacity recorded for a layer, or undefined when untouched. */\nfunction recordedStoryOpacity(\n opacities: Record<string, number> | undefined,\n layerId: string,\n): number | undefined {\n const value = opacities?.[layerId];\n if (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * The opacity a story presentation has applied to a layer.\n *\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n * @param layerId Store layer id.\n * @returns The recorded opacity in `[0, 1]`, or `1` when the presentation has\n * not touched the layer (or nothing is presenting).\n */\nexport function storyLayerOpacityFactor(\n opacities: Record<string, number> | undefined,\n layerId: string,\n): number {\n return recordedStoryOpacity(opacities, layerId) ?? 1;\n}\n\n/**\n * A layer whose opacity reflects the story presentation's fade.\n *\n * A chapter's opacity *replaces* the layer's own opacity rather than scaling\n * it, matching `MapController.setStoryLayerOpacity`, which writes the chapter\n * value as the absolute paint opacity; otherwise a layer set to 0.7 in the\n * Layers panel and faded to 0.4 by a chapter would draw its diagrams at 0.28\n * while its MapLibre layers draw at 0.4.\n *\n * Returns the same object when the presentation has not touched the layer (or\n * recorded the opacity it already has), so identity-keyed caches downstream\n * keep hitting.\n *\n * @param layer Store layer.\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n */\nexport function applyStoryLayerOpacity(\n layer: GeoLibreLayer,\n opacities: Record<string, number> | undefined,\n): GeoLibreLayer {\n const opacity = recordedStoryOpacity(opacities, layer.id);\n if (opacity === undefined || opacity === layer.opacity) return layer;\n return { ...layer, opacity };\n}\n\n/**\n * Whether a story presentation has faded a layer fully out.\n *\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n * @param layerId Store layer id.\n */\nexport function isStoryHiddenLayer(\n opacities: Record<string, number> | undefined,\n layerId: string,\n): boolean {\n return storyLayerOpacityFactor(opacities, layerId) === 0;\n}\n\n/**\n * The layers a reader can currently see during a story presentation: the\n * input minus those the active chapter has faded fully out. Outside a\n * presentation the input is returned as-is (same array identity).\n *\n * @param layers Store layers in panel order.\n * @param presenting The store's `ui.storymapPresenting` flag.\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n */\nexport function storyVisibleLayers(\n layers: GeoLibreLayer[],\n presenting: boolean,\n opacities: Record<string, number> | undefined,\n): GeoLibreLayer[] {\n if (!presenting) return layers;\n if (!layers.some((layer) => isStoryHiddenLayer(opacities, layer.id))) return layers;\n return layers.filter((layer) => !isStoryHiddenLayer(opacities, layer.id));\n}\n\n/**\n * Resolve a story chapter location to a full camera view.\n *\n * A location's `pitch` and `bearing` are optional in memory (a project file\n * fills them with 0 on load). An absent value keeps the camera's current one,\n * the same as MapLibre's camera methods do for an omitted option, so engines\n * whose camera API needs every field (Cesium, `MapEngine.applyView`) behave\n * like the MapLibre map.\n *\n * @param location The chapter's camera target.\n * @param current The camera's current view, used for an absent pitch/bearing.\n * @returns The location with `pitch` and `bearing` filled in.\n */\nexport function storyLocationView(\n location: StoryChapterLocation,\n current: Pick<MapViewState, \"pitch\" | \"bearing\">,\n): MapViewState {\n return {\n center: location.center,\n zoom: location.zoom,\n pitch: location.pitch ?? current.pitch,\n bearing: location.bearing ?? current.bearing,\n };\n}\n","import { normalizeStoryMap } from \"./project\";\nimport { DEFAULT_STORY_MAP, type StoryMap } from \"./types\";\n\n/**\n * Serialize a story map to pretty-printed JSON for export.\n *\n * @param storymap The story map to serialize.\n * @returns A JSON string (settings and chapters).\n */\nexport function serializeStoryMapJson(storymap: StoryMap): string {\n return JSON.stringify(storymap, null, 2);\n}\n\n/**\n * Parse a story map from JSON authored outside GeoLibre.\n *\n * Accepts either a bare story map object or a full `.geolibre.json` project with\n * a `storymap` field, then validates it through the same normalizer used when\n * loading projects.\n *\n * @param text JSON text to import.\n * @returns A normalized story map.\n * @throws If the JSON is invalid or contains no usable chapters.\n */\nexport function parseStoryMapJson(text: string): StoryMap {\n const data = JSON.parse(text) as unknown;\n const candidate =\n data && typeof data === \"object\" && \"storymap\" in (data as object)\n ? (data as { storymap: unknown }).storymap\n : data;\n const normalized = normalizeStoryMap(candidate);\n if (!normalized) {\n throw new Error(\"The imported story map has no chapters.\");\n }\n return normalized;\n}\n\n// Flat, spreadsheet-friendly chapter columns. Layer opacity effects are stored\n// as JSON in their own columns so the CSV round-trips, but hand-authors can\n// simply leave them blank.\nconst CSV_HEADERS = [\n \"id\",\n \"title\",\n \"description\",\n \"image\",\n \"alignment\",\n \"hidden\",\n \"lng\",\n \"lat\",\n \"zoom\",\n \"pitch\",\n \"bearing\",\n \"mapAnimation\",\n \"rotateAnimation\",\n \"onChapterEnter\",\n \"onChapterExit\",\n] as const;\n\n/**\n * Serialize a story map's chapters to CSV (one row per chapter).\n *\n * Story-level settings are not represented in CSV; they are kept in the project\n * and preserved on CSV import.\n *\n * @param storymap The story map whose chapters to serialize.\n * @returns A CSV string with a header row.\n */\nexport function serializeStoryMapCsv(storymap: StoryMap): string {\n const rows = [CSV_HEADERS.join(\",\")];\n for (const c of storymap.chapters) {\n rows.push(\n [\n c.id,\n c.title,\n c.description,\n c.image ?? \"\",\n c.alignment,\n String(c.hidden),\n String(c.location.center[0]),\n String(c.location.center[1]),\n String(c.location.zoom),\n // An absent pitch/bearing is an empty cell, not the string \"undefined\".\n String(c.location.pitch ?? \"\"),\n String(c.location.bearing ?? \"\"),\n c.mapAnimation,\n String(c.rotateAnimation),\n JSON.stringify(c.onChapterEnter),\n JSON.stringify(c.onChapterExit),\n ]\n .map(csvEscape)\n .join(\",\"),\n );\n }\n return rows.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * Parse chapters from a CSV authored outside GeoLibre.\n *\n * Columns are matched by header name (case-insensitive) so column order is\n * flexible; `lng`/`lat` set the chapter center. Story-level settings come from\n * `base` (the current story) since CSV only carries chapters; missing ids are\n * generated.\n *\n * @param text CSV text to import.\n * @param base Existing story map whose settings should be preserved.\n * @returns A normalized story map.\n * @throws If the CSV has no data rows or no valid chapters.\n */\nexport function parseStoryMapCsv(text: string, base?: StoryMap | null): StoryMap {\n const rows = parseCsvRows(text);\n if (rows.length < 2) {\n throw new Error(\"The CSV has no chapter rows.\");\n }\n const header = rows[0].map((h) => h.trim().toLowerCase());\n const col = (name: string) => header.indexOf(name.toLowerCase());\n\n const chapters = rows\n .slice(1)\n .filter((row) => row.some((cell) => cell.trim() !== \"\"))\n .map((row, index) => {\n const get = (name: string) => {\n const j = col(name);\n return j >= 0 ? (row[j] ?? \"\") : \"\";\n };\n const num = (name: string, fallback: number) => {\n const value = Number(get(name));\n return Number.isFinite(value) ? value : fallback;\n };\n const bool = (name: string) => /^(true|1|yes)$/i.test(get(name).trim());\n const jsonArray = (name: string) => {\n try {\n const parsed = JSON.parse(get(name) || \"[]\");\n return Array.isArray(parsed) ? parsed : [];\n } catch {\n return [];\n }\n };\n return {\n id: get(\"id\").trim() || `chapter-${index + 1}`,\n title: get(\"title\"),\n description: get(\"description\"),\n image: get(\"image\").trim() || undefined,\n alignment: get(\"alignment\").trim() || \"left\",\n hidden: bool(\"hidden\"),\n location: {\n center: [num(\"lng\", 0), num(\"lat\", 0)],\n zoom: num(\"zoom\", 2),\n pitch: num(\"pitch\", 0),\n bearing: num(\"bearing\", 0),\n },\n mapAnimation: get(\"mapanimation\").trim() || \"flyTo\",\n rotateAnimation: bool(\"rotateanimation\"),\n onChapterEnter: jsonArray(\"onchapterenter\"),\n onChapterExit: jsonArray(\"onchapterexit\"),\n };\n });\n\n const normalized = normalizeStoryMap({\n ...(base ?? DEFAULT_STORY_MAP),\n chapters,\n });\n if (!normalized) {\n throw new Error(\"The CSV contained no valid chapters.\");\n }\n return normalized;\n}\n\n/** Quote a CSV field when it contains a comma, quote, or newline. */\nfunction csvEscape(value: string): string {\n if (/[\",\\n\\r]/.test(value)) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`;\n }\n return value;\n}\n\n/** Parse CSV text into rows of fields, honoring quotes and escaped quotes. */\nfunction parseCsvRows(text: string): string[][] {\n const rows: string[][] = [];\n let row: string[] = [];\n let field = \"\";\n let inQuotes = false;\n // Normalize line endings so \\r\\n and \\r behave like \\n.\n const input = text.replace(/\\r\\n?/g, \"\\n\");\n\n for (let i = 0; i < input.length; i++) {\n const ch = input[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (input[i + 1] === '\"') {\n field += '\"';\n i++;\n } else {\n inQuotes = false;\n }\n } else {\n field += ch;\n }\n } else if (ch === '\"') {\n inQuotes = true;\n } else if (ch === \",\") {\n row.push(field);\n field = \"\";\n } else if (ch === \"\\n\") {\n row.push(field);\n rows.push(row);\n row = [];\n field = \"\";\n } else {\n field += ch;\n }\n }\n // Flush the final field/row unless the input ended with a trailing newline.\n if (field !== \"\" || row.length > 0) {\n row.push(field);\n rows.push(row);\n }\n return rows;\n}\n","/**\n * Narrow subscriptions to the store's `layers` array.\n *\n * `updateLayer` (and every other layer action) replaces `layers` with a new\n * array on each change, so a component that subscribes with\n * `useAppStore((s) => s.layers)` re-renders on every opacity tweak, rename or\n * style edit of *any* layer. The hooks here select only what a component reads\n * and compare it so unrelated layer changes do not re-render it:\n *\n * - {@link useLayer} — one layer record, by id. Layer actions copy only the\n * layer they change and keep every other record's identity, so this is\n * stable while other layers change.\n * - {@link useLayerIds} — the ordered id list, shallow-compared.\n * - {@link useLayerSummaries} — `{ id, name, type, visible, groupId }` per\n * layer, for components that only list layers.\n * - {@link useLayersWhen} — the full array, but only while `active` (a dialog\n * is open, a search has a query); a constant empty array otherwise.\n *\n * The plain `select*` functions are exported so the selection semantics can be\n * tested without React.\n */\nimport { useShallow } from \"zustand/react/shallow\";\nimport { type AppState, useAppStore } from \"./store\";\nimport type { GeoLibreLayer, LayerType } from \"./types\";\n\n/**\n * The fields a layer list needs: enough to draw a row, a `<select>` option or\n * a visibility checkbox. It deliberately leaves out opacity, style, source,\n * metadata and feature data, which change far more often.\n */\nexport interface LayerSummary {\n /** The layer id. */\n readonly id: string;\n /** The display name. */\n readonly name: string;\n /** The layer kind. */\n readonly type: LayerType;\n /** Whether the layer is shown on the primary map. */\n readonly visible: boolean;\n /** The layer panel group the layer belongs to, if any. */\n readonly groupId: string | undefined;\n}\n\n/**\n * The array {@link useLayersWhen} returns while inactive. Frozen, and shared so\n * the inactive selection never changes identity.\n */\nexport const NO_LAYERS: GeoLibreLayer[] = Object.freeze([]) as unknown as GeoLibreLayer[];\n\n/**\n * Select one layer record by id.\n *\n * @param state - The app state.\n * @param id - The layer id; `null`/`undefined` selects nothing.\n * @returns The layer record, or `undefined` when there is no such layer.\n */\nexport function selectLayerById(\n state: Pick<AppState, \"layers\">,\n id: string | null | undefined,\n): GeoLibreLayer | undefined {\n if (!id) return undefined;\n return state.layers.find((layer) => layer.id === id);\n}\n\n/**\n * Select the layer ids in store order (bottom to top).\n *\n * Returns a new array on every call; pair it with a shallow comparison (as\n * {@link useLayerIds} does) so an unchanged id list keeps its identity.\n *\n * @param state - The app state.\n * @returns The layer ids.\n */\nexport function selectLayerIds(state: Pick<AppState, \"layers\">): string[] {\n return state.layers.map((layer) => layer.id);\n}\n\n// Summaries from the previous call, by layer id. Reusing an entry whose fields\n// are unchanged keeps each summary's identity across unrelated edits (an\n// opacity change replaces the layer object but not its summary), which is what\n// lets a shallow comparison of the summary array succeed. Rebuilt on every call\n// so removed layers drop out. Summaries are immutable values, so sharing them\n// between subscribers is safe.\n//\n// The cache is module-level on purpose: every caller reads the one app store,\n// and an entry is reused only after `sameSummary` re-checks it against the\n// current layer. Another subscriber, or a React render that is discarded,\n// can therefore only cause a cache miss (a new summary object), never a stale\n// summary. Tests that care about identity should not rely on a clean cache.\nlet summaryCache = new Map<string, LayerSummary>();\n\nfunction sameSummary(summary: LayerSummary, layer: GeoLibreLayer): boolean {\n return (\n summary.name === layer.name &&\n summary.type === layer.type &&\n summary.visible === layer.visible &&\n summary.groupId === layer.groupId\n );\n}\n\n/**\n * Select a {@link LayerSummary} per layer, in store order (bottom to top).\n *\n * Each summary keeps its identity while its own fields are unchanged, so the\n * returned array is shallow-equal to the previous one after an edit that\n * touches none of those fields (opacity, style, data, metadata).\n *\n * @param state - The app state.\n * @returns One summary per layer.\n */\nexport function selectLayerSummaries(state: Pick<AppState, \"layers\">): LayerSummary[] {\n const next = new Map<string, LayerSummary>();\n const summaries = state.layers.map((layer) => {\n const previous = summaryCache.get(layer.id);\n const summary =\n previous && sameSummary(previous, layer)\n ? previous\n : Object.freeze({\n id: layer.id,\n name: layer.name,\n type: layer.type,\n visible: layer.visible,\n groupId: layer.groupId,\n });\n next.set(layer.id, summary);\n return summary;\n });\n summaryCache = next;\n return summaries;\n}\n\n/**\n * Build a selector for the full `layers` array that is live only while\n * `active`.\n *\n * @param active - Whether the caller currently needs the layers.\n * @returns A selector returning `state.layers` while active, and the constant\n * {@link NO_LAYERS} otherwise.\n */\nexport function selectLayersWhen(\n active: boolean,\n): (state: Pick<AppState, \"layers\">) => GeoLibreLayer[] {\n return active ? (state) => state.layers : () => NO_LAYERS;\n}\n\n/**\n * Subscribe to one layer record.\n *\n * Re-renders only when that layer changes (or appears/disappears), not when\n * another layer is edited.\n *\n * @param id - The layer id; `null`/`undefined` subscribes to nothing.\n * @returns The layer record, or `undefined` when there is no such layer.\n */\nexport function useLayer(id: string | null | undefined): GeoLibreLayer | undefined {\n return useAppStore((state) => selectLayerById(state, id));\n}\n\n/**\n * Subscribe to the ordered list of layer ids.\n *\n * Re-renders only when a layer is added, removed or reordered.\n *\n * @returns The layer ids in store order (bottom to top).\n */\nexport function useLayerIds(): string[] {\n return useAppStore(useShallow(selectLayerIds));\n}\n\n/**\n * Subscribe to a {@link LayerSummary} per layer.\n *\n * Re-renders only when a layer is added, removed, reordered, renamed, shown or\n * hidden, retyped or regrouped; opacity, style, data and metadata edits do not\n * re-render the caller.\n *\n * @returns One summary per layer, in store order (bottom to top).\n */\nexport function useLayerSummaries(): LayerSummary[] {\n return useAppStore(useShallow(selectLayerSummaries));\n}\n\n/**\n * Subscribe to the full `layers` array only while `active`.\n *\n * For always-mounted surfaces (dialogs, floating panels) that need every layer\n * record while open but keep local state across closes and so cannot simply be\n * unmounted. While inactive the caller gets the constant {@link NO_LAYERS} and\n * layer edits do not re-render it.\n *\n * @param active - Whether the caller currently needs the layers (for example,\n * whether its dialog is open).\n * @returns `layers` while active, otherwise an empty, frozen array.\n */\nexport function useLayersWhen(active: boolean): GeoLibreLayer[] {\n return useAppStore(selectLayersWhen(active));\n}\n","import type { Feature, Point } from \"geojson\";\nimport { getRuntimeEnvironment } from \"./runtime-env\";\n\n/**\n * Geocoding client and pure helpers shared by the batch-geocode dialog and the\n * reverse-geocode plugin.\n *\n * This module lives in `@geolibre/core` rather than `@geolibre/processing`\n * because `@geolibre/plugins` depends on core but not on processing, and the\n * reverse-geocode plugin needs the same client. The pure helpers (URL builders,\n * result mappers, pacing) carry no React or MapLibre dependency so they can be\n * unit-tested without a browser or network.\n *\n * Providers: geocoding is dispatched through a small {@link GeocodingProvider}\n * registry so alternatives to Nominatim (ArcGIS World Geocoder, CartoCiudad, Mapbox,\n * Pelias, Google) can be selected per project. Each provider builds its own request\n * URLs and normalizes its response into a {@link GeocodeMatch} (forward) or a\n * {@link ReverseGeocodeDisplay} (reverse), so the dialog and plugin stay\n * provider-agnostic. Nominatim is the default.\n *\n * The 1 request/second throttle and the row cap are part of Nominatim's public\n * usage policy and are therefore applied ONLY to the default public host; a\n * self-hosted endpoint relaxes both, and keyed providers (Mapbox/ArcGIS/Google)\n * are paced by their own quotas, not by us.\n *\n * Browser fetch cannot set `User-Agent`/`Referer`, so the app is identified to\n * Nominatim via the optional `email` query parameter plus the automatically\n * sent `Referer`. The desktop build overrides this with a native (Tauri) fetch\n * via {@link setGeocodingFetch} — see the note there. See\n * docs/user-guide/data-integrations.md#geocoding.\n */\n\nexport const DEFAULT_FORWARD_GEOCODE_ENDPOINT = \"https://nominatim.openstreetmap.org/search\";\nexport const DEFAULT_REVERSE_GEOCODE_ENDPOINT = \"https://nominatim.openstreetmap.org/reverse\";\n\n/** Host whose public usage policy (1 req/sec, bulk limits) we must respect. */\nexport const NOMINATIM_PUBLIC_HOST = \"nominatim.openstreetmap.org\";\n\n/** Minimum spacing between requests to the public Nominatim endpoint. */\nexport const NOMINATIM_MIN_INTERVAL_MS = 1100;\n\n/** Host of the free, unkeyed public CartoCiudad geocoder (IGN/CNIG). */\nexport const CARTOCIUDAD_PUBLIC_HOST = \"cartociudad.es\";\n\n/**\n * Courtesy spacing (~5 req/sec) between requests to the public CartoCiudad\n * endpoint. Unlike {@link NOMINATIM_MIN_INTERVAL_MS} this is not a published\n * policy — IGN documents no rate limit — it is our own conservative default so\n * a large batch does not burst at a free public government service. See\n * {@link geocoderMinIntervalMs}.\n */\nexport const CARTOCIUDAD_MIN_INTERVAL_MS = 200;\n\n/** Max rows a single batch run will geocode against the public endpoint. */\nexport const PUBLIC_GEOCODE_ROW_CAP = 1000;\n\n/** Property keys added to each geocoded feature. */\nexport const GEOCODE_LAT_KEY = \"geocode_lat\";\nexport const GEOCODE_LON_KEY = \"geocode_lon\";\nexport const GEOCODE_DISPLAY_NAME_KEY = \"geocode_display_name\";\nexport const GEOCODE_SCORE_KEY = \"geocode_importance\";\n/** Provider id that produced (or failed to produce) a match, when stamped. */\nexport const GEOCODE_PROVIDER_KEY = \"geocode_provider\";\n/** Whether a row matched, when stamped. See {@link GeocodeStatus}. */\nexport const GEOCODE_STATUS_KEY = \"geocode_status\";\n\n/** Identifier of a selectable geocoding backend. */\nexport type GeocodingProviderId =\n | \"nominatim\"\n | \"pelias\"\n | \"arcgis\"\n | \"mapbox\"\n | \"google\"\n | \"cartociudad\";\n\n/** The provider used when none is configured. */\nexport const DEFAULT_GEOCODING_PROVIDER_ID: GeocodingProviderId = \"nominatim\";\n\nexport interface GeocoderConfig {\n /** Which backend to dispatch through. */\n providerId: GeocodingProviderId;\n /** Forward (address -> point) endpoint. */\n forwardEndpoint: string;\n /** Reverse (point -> address) endpoint. */\n reverseEndpoint: string;\n /** Contact email sent as the `email` query param to identify the client. */\n email?: string;\n /** API key / access token for providers that require one. */\n apiKey?: string;\n}\n\n/** Per-request options passed to a provider's forward URL builder. */\nexport interface ForwardRequestOptions {\n email?: string;\n limit?: number;\n}\n\n/** Per-request options passed to a provider's reverse URL builder. */\nexport interface ReverseRequestOptions {\n email?: string;\n zoom?: number;\n}\n\n/**\n * A normalized forward-geocoding match. Each provider maps its own response\n * shape onto this so {@link geocodeMatchToFeature} can build a point feature\n * without knowing which backend produced it. `score` is the provider's match\n * confidence/importance on its own scale (Nominatim importance 0..1, ArcGIS\n * 0..100, ...), or null when the provider reports none.\n */\nexport interface GeocodeMatch {\n lat: number;\n lon: number;\n displayName: string;\n score: number | null;\n}\n\n/**\n * A geocoding backend. Builds provider-specific request URLs and normalizes the\n * raw response into the shared {@link GeocodeMatch} / {@link ReverseGeocodeDisplay}\n * shapes. Implementations are pure (no fetch) so they unit-test without a\n * network; {@link geocodeForward}/{@link geocodeReverse} own the actual fetch.\n */\nexport interface GeocodingProvider {\n id: GeocodingProviderId;\n /** Human-readable name shown in the provider picker. */\n label: string;\n /** Whether forward (address -> point) geocoding is supported. */\n forward: boolean;\n /** Whether reverse (point -> address) geocoding is supported. */\n reverse: boolean;\n /**\n * Whether the provider's default configuration cannot work without an API\n * key / access token. Drives the \"missing key\" warning and disables the run.\n */\n requiresApiKey: boolean;\n /**\n * Whether the provider can use an API key at all, so the Settings UI shows a\n * key field. True for every keyed provider plus Pelias (the hosted\n * geocode.earth endpoint needs a key, a self-hosted instance does not).\n */\n acceptsApiKey: boolean;\n /**\n * Whether the provider's API blocks browser cross-origin requests, so it\n * needs a same-origin proxy to work in the webview. The UI surfaces a hint\n * when true. Only set for Google's Geocoding API today.\n */\n browserCorsRestricted?: boolean;\n /** Default forward endpoint used when the project does not override it. */\n defaultForwardEndpoint: string;\n /** Default reverse endpoint used when the project does not override it. */\n defaultReverseEndpoint: string;\n buildForwardUrl(config: GeocoderConfig, query: string, options: ForwardRequestOptions): string;\n parseForward(data: unknown): GeocodeMatch[];\n buildReverseUrl(\n config: GeocoderConfig,\n lon: number,\n lat: number,\n options: ReverseRequestOptions,\n ): string;\n parseReverse(data: unknown): ReverseGeocodeDisplay | null;\n}\n\n/** A single Nominatim forward-geocoding result (jsonv2). */\nexport interface NominatimForwardResult {\n lat: string;\n lon: string;\n display_name?: string;\n importance?: number | string;\n [key: string]: unknown;\n}\n\n/** A Nominatim reverse-geocoding result (jsonv2). */\nexport interface NominatimReverseResult {\n lat?: string;\n lon?: string;\n display_name?: string;\n address?: Record<string, string>;\n /** Present (e.g. \"Unable to geocode\") when no match was found. */\n error?: string;\n [key: string]: unknown;\n}\n\n/** A row queued for geocoding, paired with its source CSV row. */\nexport interface GeocodeRequest {\n /** Zero-based index among the parsed data rows. */\n index: number;\n /** The composed address string sent to the geocoder. */\n address: string;\n /** The original CSV row, copied onto the output feature's properties. */\n row: Record<string, string>;\n}\n\n/** A reverse-geocode result resolved to a display string plus address parts. */\nexport interface ReverseGeocodeDisplay {\n displayName: string;\n parts: Record<string, string>;\n}\n\n/** Whether a row matched during batch geocoding, stamped as {@link GEOCODE_STATUS_KEY}. */\nexport type GeocodeStatus = \"matched\" | \"unmatched\";\n\nfunction coerceScore(value: number | string | undefined | null): number | null {\n if (value === undefined || value === null || value === \"\") return null;\n const num = Number(value);\n return Number.isFinite(num) ? num : null;\n}\n\n/** Append `base` to `existing`, suffixing `_2`, `_3`, ... if the key collides. */\nfunction uniqueKey(base: string, existing: Record<string, unknown>): string {\n if (!(base in existing)) return base;\n let suffix = 2;\n while (`${base}_${suffix}` in existing) suffix += 1;\n return `${base}_${suffix}`;\n}\n\n/**\n * Coerce an address-parts record to `Record<string, string>`. Provider address\n * objects can carry numbers (ArcGIS `Score`/`X`/`Y`, Pelias coordinates), so\n * values are stringified rather than unsafely cast; null/undefined are dropped.\n */\nfunction stringifyParts(obj: Record<string, unknown>): Record<string, string> {\n const parts: Record<string, string> = {};\n for (const [key, value] of Object.entries(obj)) {\n if (value === null || value === undefined) continue;\n parts[key] = typeof value === \"string\" ? value : String(value);\n }\n return parts;\n}\n\n/** Read a string property off an unknown object, or undefined. */\nfunction readString(obj: unknown, key: string): string | undefined {\n if (obj && typeof obj === \"object\" && key in obj) {\n const value = (obj as Record<string, unknown>)[key];\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\") return String(value);\n }\n return undefined;\n}\n\n/** Build a Nominatim forward-geocoding URL (jsonv2, address details on). */\nexport function buildForwardGeocodeUrl(\n endpoint: string,\n query: string,\n options: { email?: string; limit?: number } = {},\n): string {\n const url = new URL(endpoint);\n url.searchParams.set(\"q\", query);\n url.searchParams.set(\"format\", \"jsonv2\");\n url.searchParams.set(\"addressdetails\", \"1\");\n url.searchParams.set(\"limit\", String(options.limit ?? 1));\n if (options.email) url.searchParams.set(\"email\", options.email);\n return url.toString();\n}\n\n/** Build a Nominatim reverse-geocoding URL (jsonv2, address details on). */\nexport function buildReverseGeocodeUrl(\n endpoint: string,\n lon: number,\n lat: number,\n options: { email?: string; zoom?: number } = {},\n): string {\n const url = new URL(endpoint);\n url.searchParams.set(\"lat\", String(lat));\n url.searchParams.set(\"lon\", String(lon));\n url.searchParams.set(\"format\", \"jsonv2\");\n url.searchParams.set(\"addressdetails\", \"1\");\n if (options.zoom !== undefined) url.searchParams.set(\"zoom\", String(options.zoom));\n if (options.email) url.searchParams.set(\"email\", options.email);\n return url.toString();\n}\n\n/** Map one Nominatim forward result onto a normalized match, or null. */\nfunction nominatimForwardResultToMatch(result: NominatimForwardResult): GeocodeMatch | null {\n const lat = Number(result.lat);\n const lon = Number(result.lon);\n if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;\n return {\n lat,\n lon,\n displayName: result.display_name ?? \"\",\n score: coerceScore(result.importance),\n };\n}\n\n/**\n * Convert a normalized {@link GeocodeMatch} into a point Feature whose\n * properties carry the original CSV row plus `geocode_lat`/`geocode_lon`/\n * `geocode_display_name`/`geocode_importance`. The added keys are de-duplicated\n * against the original columns so an existing `geocode_lat` is not clobbered.\n * Geometry coordinates are `[lon, lat]`. Returns null when the match has no\n * finite coordinates.\n *\n * `extra.providerId`, when passed, also stamps `geocode_provider` and\n * `geocode_status: \"matched\"` (dropped when omitted, so the existing\n * `GeocodeDialog` call site keeps its current output unchanged).\n */\nexport function geocodeMatchToFeature(\n match: GeocodeMatch,\n originalRow: Record<string, string> = {},\n extra?: { providerId?: GeocodingProviderId },\n): Feature<Point> | null {\n if (!Number.isFinite(match.lat) || !Number.isFinite(match.lon)) return null;\n\n const properties: Record<string, unknown> = { ...originalRow };\n const added: Record<string, unknown> = {\n [GEOCODE_LAT_KEY]: match.lat,\n [GEOCODE_LON_KEY]: match.lon,\n [GEOCODE_DISPLAY_NAME_KEY]: match.displayName ?? \"\",\n [GEOCODE_SCORE_KEY]: match.score,\n };\n if (extra?.providerId) {\n added[GEOCODE_PROVIDER_KEY] = extra.providerId;\n added[GEOCODE_STATUS_KEY] = \"matched\" satisfies GeocodeStatus;\n }\n for (const [key, value] of Object.entries(added)) {\n properties[uniqueKey(key, properties)] = value;\n }\n\n return {\n type: \"Feature\",\n geometry: { type: \"Point\", coordinates: [match.lon, match.lat] },\n properties,\n };\n}\n\n/**\n * Build a null-geometry Feature for a CSV row that could not be geocoded, so\n * it is kept (not silently dropped) alongside the matched point features.\n * Mirrors the null-geometry shape {@link parseDelimitedTextLayer} already uses\n * for a non-spatial attribute table. Stamps `geocode_status: \"unmatched\"` and\n * `geocode_provider`, de-duplicated against the original columns the same way\n * {@link geocodeMatchToFeature} does.\n */\nexport function unmatchedGeocodeFeature(\n originalRow: Record<string, string>,\n providerId: GeocodingProviderId,\n): Feature<null> {\n const properties: Record<string, unknown> = { ...originalRow };\n const added: Record<string, unknown> = {\n [GEOCODE_STATUS_KEY]: \"unmatched\" satisfies GeocodeStatus,\n [GEOCODE_PROVIDER_KEY]: providerId,\n };\n for (const [key, value] of Object.entries(added)) {\n properties[uniqueKey(key, properties)] = value;\n }\n\n return { type: \"Feature\", geometry: null, properties };\n}\n\n/**\n * Convert a Nominatim forward result into a point Feature. Retained as a thin\n * wrapper over {@link geocodeMatchToFeature} for callers and tests that work\n * directly with Nominatim results. Returns null when the result has no finite\n * coordinates.\n */\nexport function nominatimResultToFeature(\n result: NominatimForwardResult,\n originalRow: Record<string, string> = {},\n): Feature<Point> | null {\n const match = nominatimForwardResultToMatch(result);\n return match ? geocodeMatchToFeature(match, originalRow) : null;\n}\n\n/**\n * Resolve a Nominatim reverse result to a display string and address parts, or\n * null when the point could not be reverse-geocoded.\n */\nexport function nominatimReverseResultToDisplay(\n result: NominatimReverseResult | null,\n): ReverseGeocodeDisplay | null {\n if (!result || result.error) return null;\n const displayName = result.display_name?.trim();\n if (!displayName) return null;\n return { displayName, parts: result.address ?? {} };\n}\n\n// --- Provider implementations ----------------------------------------------\n\nconst nominatimProvider: GeocodingProvider = {\n id: \"nominatim\",\n label: \"Nominatim (OpenStreetMap)\",\n forward: true,\n reverse: true,\n requiresApiKey: false,\n acceptsApiKey: false,\n defaultForwardEndpoint: DEFAULT_FORWARD_GEOCODE_ENDPOINT,\n defaultReverseEndpoint: DEFAULT_REVERSE_GEOCODE_ENDPOINT,\n buildForwardUrl: (config, query, options) =>\n buildForwardGeocodeUrl(config.forwardEndpoint, query, {\n email: options.email,\n limit: options.limit,\n }),\n parseForward: (data) =>\n Array.isArray(data)\n ? (data as NominatimForwardResult[])\n .map(nominatimForwardResultToMatch)\n .filter((m): m is GeocodeMatch => m !== null)\n : [],\n buildReverseUrl: (config, lon, lat, options) =>\n buildReverseGeocodeUrl(config.reverseEndpoint, lon, lat, {\n email: options.email,\n zoom: options.zoom,\n }),\n parseReverse: (data) =>\n nominatimReverseResultToDisplay(\n data && typeof data === \"object\" ? (data as NominatimReverseResult) : null,\n ),\n};\n\n/** Read a GeoJSON FeatureCollection's features array, or []. */\nfunction geojsonFeatures(data: unknown): Record<string, unknown>[] {\n if (\n data &&\n typeof data === \"object\" &&\n Array.isArray((data as { features?: unknown }).features)\n ) {\n return (data as { features: unknown[] }).features.filter(\n (f): f is Record<string, unknown> => !!f && typeof f === \"object\",\n );\n }\n return [];\n}\n\n/** Pull `[lon, lat]` from a GeoJSON Point geometry, or null. */\nfunction pointCoords(feature: Record<string, unknown>): [number, number] | null {\n const geometry = feature.geometry as { coordinates?: unknown } | undefined;\n const coords = geometry?.coordinates;\n if (Array.isArray(coords) && coords.length >= 2) {\n const lon = Number(coords[0]);\n const lat = Number(coords[1]);\n if (Number.isFinite(lon) && Number.isFinite(lat)) return [lon, lat];\n }\n return null;\n}\n\nconst peliasProvider: GeocodingProvider = {\n id: \"pelias\",\n label: \"Pelias\",\n forward: true,\n reverse: true,\n // The default endpoint is hosted geocode.earth (key required), but a\n // self-hosted Pelias needs none, so the key is accepted yet not mandatory.\n requiresApiKey: false,\n acceptsApiKey: true,\n defaultForwardEndpoint: \"https://api.geocode.earth/v1/search\",\n defaultReverseEndpoint: \"https://api.geocode.earth/v1/reverse\",\n buildForwardUrl: (config, query, options) => {\n const url = new URL(config.forwardEndpoint);\n url.searchParams.set(\"text\", query);\n if (options.limit) url.searchParams.set(\"size\", String(options.limit));\n if (config.apiKey) url.searchParams.set(\"api_key\", config.apiKey);\n return url.toString();\n },\n parseForward: (data) =>\n geojsonFeatures(data)\n .map((feature) => {\n const coords = pointCoords(feature);\n if (!coords) return null;\n const props = (feature.properties as Record<string, unknown>) ?? {};\n return {\n lon: coords[0],\n lat: coords[1],\n displayName: readString(props, \"label\") ?? \"\",\n score: coerceScore(props.confidence as number | undefined),\n } satisfies GeocodeMatch;\n })\n .filter((m): m is GeocodeMatch => m !== null),\n buildReverseUrl: (config, lon, lat) => {\n const url = new URL(config.reverseEndpoint);\n url.searchParams.set(\"point.lat\", String(lat));\n url.searchParams.set(\"point.lon\", String(lon));\n if (config.apiKey) url.searchParams.set(\"api_key\", config.apiKey);\n return url.toString();\n },\n parseReverse: (data) => {\n const feature = geojsonFeatures(data)[0];\n if (!feature) return null;\n const props = (feature.properties as Record<string, unknown>) ?? {};\n const displayName = readString(props, \"label\")?.trim();\n return displayName ? { displayName, parts: stringifyParts(props) } : null;\n },\n};\n\nconst arcgisProvider: GeocodingProvider = {\n id: \"arcgis\",\n label: \"ArcGIS World Geocoder\",\n forward: true,\n reverse: true,\n requiresApiKey: true,\n acceptsApiKey: true,\n defaultForwardEndpoint:\n \"https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/findAddressCandidates\",\n defaultReverseEndpoint:\n \"https://geocode.arcgis.com/arcgis/rest/services/World/GeocodeServer/reverseGeocode\",\n buildForwardUrl: (config, query, options) => {\n const url = new URL(config.forwardEndpoint);\n url.searchParams.set(\"SingleLine\", query);\n url.searchParams.set(\"f\", \"json\");\n url.searchParams.set(\"outFields\", \"Match_addr\");\n if (options.limit) url.searchParams.set(\"maxLocations\", String(options.limit));\n if (config.apiKey) url.searchParams.set(\"token\", config.apiKey);\n return url.toString();\n },\n parseForward: (data) => {\n const candidates =\n data &&\n typeof data === \"object\" &&\n Array.isArray((data as { candidates?: unknown }).candidates)\n ? ((data as { candidates: unknown[] }).candidates as Record<string, unknown>[])\n : [];\n return candidates\n .map((candidate) => {\n const location = candidate.location as { x?: unknown; y?: unknown } | undefined;\n const lon = Number(location?.x);\n const lat = Number(location?.y);\n if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;\n return {\n lon,\n lat,\n displayName: readString(candidate, \"address\") ?? \"\",\n score: coerceScore(candidate.score as number | undefined),\n } satisfies GeocodeMatch;\n })\n .filter((m): m is GeocodeMatch => m !== null);\n },\n buildReverseUrl: (config, lon, lat) => {\n const url = new URL(config.reverseEndpoint);\n url.searchParams.set(\"location\", `${lon},${lat}`);\n url.searchParams.set(\"f\", \"json\");\n if (config.apiKey) url.searchParams.set(\"token\", config.apiKey);\n return url.toString();\n },\n parseReverse: (data) => {\n const address =\n data && typeof data === \"object\" ? (data as { address?: unknown }).address : undefined;\n if (!address || typeof address !== \"object\") return null;\n const displayName = (\n readString(address, \"LongLabel\") ?? readString(address, \"Match_addr\")\n )?.trim();\n return displayName\n ? { displayName, parts: stringifyParts(address as Record<string, unknown>) }\n : null;\n },\n};\n\nconst mapboxProvider: GeocodingProvider = {\n id: \"mapbox\",\n label: \"Mapbox\",\n forward: true,\n reverse: true,\n requiresApiKey: true,\n acceptsApiKey: true,\n defaultForwardEndpoint: \"https://api.mapbox.com/geocoding/v5/mapbox.places\",\n defaultReverseEndpoint: \"https://api.mapbox.com/geocoding/v5/mapbox.places\",\n buildForwardUrl: (config, query, options) => {\n const base = config.forwardEndpoint.replace(/\\/+$/, \"\");\n const url = new URL(`${base}/${encodeURIComponent(query)}.json`);\n if (options.limit) url.searchParams.set(\"limit\", String(options.limit));\n if (config.apiKey) url.searchParams.set(\"access_token\", config.apiKey);\n return url.toString();\n },\n parseForward: (data) =>\n geojsonFeatures(data)\n .map((feature) => {\n const center = feature.center as unknown;\n let coords: [number, number] | null = null;\n if (Array.isArray(center) && center.length >= 2) {\n const lon = Number(center[0]);\n const lat = Number(center[1]);\n if (Number.isFinite(lon) && Number.isFinite(lat)) coords = [lon, lat];\n }\n coords ??= pointCoords(feature);\n if (!coords) return null;\n return {\n lon: coords[0],\n lat: coords[1],\n displayName: readString(feature, \"place_name\") ?? \"\",\n score: coerceScore(feature.relevance as number | undefined),\n } satisfies GeocodeMatch;\n })\n .filter((m): m is GeocodeMatch => m !== null),\n buildReverseUrl: (config, lon, lat) => {\n const base = config.reverseEndpoint.replace(/\\/+$/, \"\");\n const url = new URL(`${base}/${lon},${lat}.json`);\n if (config.apiKey) url.searchParams.set(\"access_token\", config.apiKey);\n return url.toString();\n },\n parseReverse: (data) => {\n const feature = geojsonFeatures(data)[0];\n if (!feature) return null;\n const displayName = readString(feature, \"place_name\")?.trim();\n return displayName\n ? {\n displayName,\n parts: stringifyParts((feature.properties as Record<string, unknown>) ?? {}),\n }\n : null;\n },\n};\n\n/**\n * Detect a Google Geocoding API error embedded in an HTTP 200 body. Google v3\n * returns REQUEST_DENIED / OVER_QUERY_LIMIT / INVALID_REQUEST and friends with\n * a 200 status and the failure only in the `status` field, so `response.ok`\n * alone would silently turn those into empty results. Returns null for OK and\n * ZERO_RESULTS (a successful empty match).\n */\nfunction googleErrorMessage(data: unknown): string | null {\n const status = readString(data, \"status\");\n if (!status || status === \"OK\" || status === \"ZERO_RESULTS\") return null;\n const detail = readString(data, \"error_message\");\n return detail\n ? `Google geocoder error: ${status} - ${detail}`\n : `Google geocoder error: ${status}`;\n}\n\nconst googleProvider: GeocodingProvider = {\n id: \"google\",\n label: \"Google\",\n forward: true,\n reverse: true,\n requiresApiKey: true,\n acceptsApiKey: true,\n browserCorsRestricted: true,\n defaultForwardEndpoint: \"https://maps.googleapis.com/maps/api/geocode/json\",\n defaultReverseEndpoint: \"https://maps.googleapis.com/maps/api/geocode/json\",\n // The Geocoding API has no result-count parameter, so the batch loop's\n // `limit` is intentionally not forwarded; only the top result is used.\n buildForwardUrl: (config, query) => {\n const url = new URL(config.forwardEndpoint);\n url.searchParams.set(\"address\", query);\n if (config.apiKey) url.searchParams.set(\"key\", config.apiKey);\n return url.toString();\n },\n parseForward: (data) => {\n const error = googleErrorMessage(data);\n if (error) throw new Error(error);\n return parseGoogleResults(data);\n },\n buildReverseUrl: (config, lon, lat) => {\n const url = new URL(config.reverseEndpoint);\n url.searchParams.set(\"latlng\", `${lat},${lon}`);\n if (config.apiKey) url.searchParams.set(\"key\", config.apiKey);\n return url.toString();\n },\n parseReverse: (data) => {\n const error = googleErrorMessage(data);\n if (error) throw new Error(error);\n const results =\n data && typeof data === \"object\" && Array.isArray((data as { results?: unknown }).results)\n ? ((data as { results: unknown[] }).results as Record<string, unknown>[])\n : [];\n const displayName = readString(results[0], \"formatted_address\")?.trim();\n return displayName ? { displayName, parts: {} } : null;\n },\n};\n\n/** Map a Google Geocoding API `results` array onto normalized matches. */\nfunction parseGoogleResults(data: unknown): GeocodeMatch[] {\n const results =\n data && typeof data === \"object\" && Array.isArray((data as { results?: unknown }).results)\n ? ((data as { results: unknown[] }).results as Record<string, unknown>[])\n : [];\n return results\n .map((result): GeocodeMatch | null => {\n const geometry = result.geometry as { location?: unknown } | undefined;\n const location = geometry?.location as { lat?: unknown; lng?: unknown } | undefined;\n const lat = Number(location?.lat);\n const lon = Number(location?.lng);\n if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;\n return {\n lat,\n lon,\n displayName: readString(result, \"formatted_address\") ?? \"\",\n score: null,\n };\n })\n .filter((m): m is GeocodeMatch => m !== null);\n}\n\n// --- CartoCiudad (IGN/CNIG — Spanish national geocoder) -------------------\n\n/**\n * A single CartoCiudad forward/reverse geocoding result. The API returns one\n * object (not an array) for both forward (`find`) and reverse (`reverseGeocode`)\n * lookups, even for an ambiguous query. `lat`/`lng` are top-level numbers.\n *\n * There is no in-body success flag or confidence signal to check. A query the\n * geocoder cannot match comes back as HTTP 204 with an empty body rather than\n * an object carrying a failure code, so {@link readGeocodeJson} turns it into\n * \"no results\" before a parser ever sees it. The `state`/`stateMsg` fields are\n * vestigial: IGN's own service docs record them as suppressed in the current\n * Elasticsearch-backed geocoder, which \"cannot configure candidate output by\n * degree of match\", so `state` is permanently 0 and `stateMsg` empty\n * (github.com/IDEESpain/Cartociudad). Neither is modeled here, and matches\n * carry `score: null` because the API exposes no ranking to map onto it.\n */\ninterface CartoCiudadResult {\n lat: number;\n lng: number;\n address?: string;\n type?: string;\n tip_via?: string;\n portalNumber?: number | string;\n postalCode?: string;\n poblacion?: string;\n muni?: string;\n province?: string;\n comunidadAutonoma?: string;\n refCatastral?: string;\n [key: string]: unknown;\n}\n\n/**\n * Read a CartoCiudad `lat`/`lng` field as a finite number, or null when the\n * field is absent, blank, or not numeric (`\"NaN\"` and `\"invalid\"` included).\n * Guarding the raw value matters because `Number(null)` is 0, which would turn\n * a coordinate-less response into a match in the Gulf of Guinea.\n */\nfunction cartociudadCoordinate(value: unknown): number | null {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n if (typeof value !== \"string\" || !value.trim()) return null;\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : null;\n}\n\n/** Validated WGS84 position for a result, or null when it carries none. */\nfunction cartociudadCoords(r: CartoCiudadResult): { lat: number; lon: number } | null {\n const lat = cartociudadCoordinate(r.lat);\n const lon = cartociudadCoordinate(r.lng);\n if (lat === null || lon === null) return null;\n if (lat < -90 || lat > 90 || lon < -180 || lon > 180) return null;\n return { lat, lon };\n}\n\n/** Default endpoints for the public CartoCiudad REST API (no auth required). */\nconst CARTOCIUDAD_FORWARD_ENDPOINT = \"https://www.cartociudad.es/geocoder/api/geocoder/find\";\nconst CARTOCIUDAD_REVERSE_ENDPOINT =\n \"https://www.cartociudad.es/geocoder/api/geocoder/reverseGeocode\";\n\n/**\n * Build a human-readable display name from CartoCiudad address fields.\n * Format: \"Tip Via Address Num, CP Poblacion (Province)\"\n */\nfunction cartociudadDisplayName(r: CartoCiudadResult): string {\n const parts: string[] = [];\n const streetParts = [r.tip_via, r.address]\n .filter((s) => s && String(s).trim())\n .map((s) => String(s).trim());\n if (streetParts.length) {\n let street = streetParts.join(\" \");\n const portalNumber =\n r.portalNumber === undefined || r.portalNumber === null ? \"\" : String(r.portalNumber).trim();\n if (portalNumber) street += ` ${portalNumber}`;\n parts.push(street);\n }\n // `poblacion` is null on some results that still carry `muni` — a road\n // kilometre point such as \"A-6 km 120\" answers with muni \"Espinosa de los\n // Caballeros\" and no poblacion — so fall back to the municipality rather than\n // drop it from the display name. When both are present, `poblacion` wins as\n // the more specific locality.\n const localityName = [r.poblacion, r.muni].find((s) => s && String(s).trim());\n const locality = [r.postalCode, localityName]\n .filter((s) => s && String(s).trim())\n .map((s) => String(s).trim())\n .join(\" \");\n if (locality) parts.push(locality);\n const province = r.province ? String(r.province).trim() : \"\";\n if (province) parts.push(`(${province})`);\n return parts.join(\", \") || r.address || r.poblacion || r.muni || \"\";\n}\n\nconst cartociudadProvider: GeocodingProvider = {\n id: \"cartociudad\",\n label: \"CartoCiudad (IGN España)\",\n forward: true,\n reverse: true,\n requiresApiKey: false,\n acceptsApiKey: false,\n defaultForwardEndpoint: CARTOCIUDAD_FORWARD_ENDPOINT,\n defaultReverseEndpoint: CARTOCIUDAD_REVERSE_ENDPOINT,\n buildForwardUrl: (config, query) => {\n const url = new URL(config.forwardEndpoint);\n url.searchParams.set(\"q\", query);\n // `find` answers with one best result and has no result-count parameter, so\n // the batch loop's `limit` is intentionally not forwarded.\n return url.toString();\n },\n parseForward: (data) => {\n // `find` returns a single object, never an array — verified against the live\n // endpoint with an ambiguous query (\"calle mayor\"), which still answers with\n // one object rather than a candidate list.\n if (!data || typeof data !== \"object\" || Array.isArray(data)) return [];\n const r = data as CartoCiudadResult;\n const coords = cartociudadCoords(r);\n if (!coords) return [];\n const displayName = cartociudadDisplayName(r).trim();\n if (!displayName) return [];\n return [\n {\n lat: coords.lat,\n lon: coords.lon,\n displayName,\n score: null,\n },\n ];\n },\n buildReverseUrl: (config, lon, lat) => {\n const url = new URL(config.reverseEndpoint);\n url.searchParams.set(\"lat\", String(lat));\n url.searchParams.set(\"lon\", String(lon));\n return url.toString();\n },\n parseReverse: (data) => {\n if (!data || typeof data !== \"object\" || Array.isArray(data)) return null;\n const r = data as CartoCiudadResult;\n if (!cartociudadCoords(r)) return null;\n const displayName = cartociudadDisplayName(r).trim();\n if (!displayName) return null;\n // Build a structured address-parts record from the rich CartoCiudad fields.\n const parts: Record<string, string> = {};\n for (const [key, value] of Object.entries({\n type: r.type,\n tip_via: r.tip_via,\n address: r.address,\n portalNumber: r.portalNumber,\n postalCode: r.postalCode,\n poblacion: r.poblacion,\n muni: r.muni,\n province: r.province,\n comunidadAutonoma: r.comunidadAutonoma,\n refCatastral: r.refCatastral,\n })) {\n if (value !== undefined && value !== null && value !== \"\") parts[key] = String(value);\n }\n return { displayName, parts };\n },\n};\n\n/** All selectable geocoding providers, Nominatim first (the default). */\nexport const GEOCODING_PROVIDERS: readonly GeocodingProvider[] = [\n nominatimProvider,\n peliasProvider,\n arcgisProvider,\n mapboxProvider,\n googleProvider,\n cartociudadProvider,\n];\n\nconst PROVIDERS_BY_ID = new Map<GeocodingProviderId, GeocodingProvider>(\n GEOCODING_PROVIDERS.map((provider) => [provider.id, provider]),\n);\n\n/** Coerce an arbitrary string to a known provider id, defaulting to Nominatim. */\nexport function normalizeGeocodingProviderId(\n value: string | undefined | null,\n): GeocodingProviderId {\n const id = value?.trim() as GeocodingProviderId | undefined;\n return id && PROVIDERS_BY_ID.has(id) ? id : DEFAULT_GEOCODING_PROVIDER_ID;\n}\n\n/** Look up a provider by id, falling back to Nominatim for unknown ids. */\nexport function getGeocodingProvider(id: string | undefined | null): GeocodingProvider {\n return PROVIDERS_BY_ID.get(normalizeGeocodingProviderId(id)) ?? nominatimProvider;\n}\n\n/**\n * Resolve the geocoder configuration from runtime env, falling back to the\n * selected provider's default endpoints. `VITE_GEOCODER_PROVIDER` selects the\n * backend; `VITE_GEOCODER_ENDPOINT`/`VITE_GEOCODER_REVERSE_ENDPOINT` override\n * the endpoints; `VITE_GEOCODER_EMAIL` supplies the contact email; and\n * `VITE_GEOCODER_API_KEY` supplies the API key for keyed providers.\n */\nexport function getGeocoderConfig(): GeocoderConfig {\n const env = getRuntimeEnvironment();\n const provider = getGeocodingProvider(env.VITE_GEOCODER_PROVIDER);\n return {\n providerId: provider.id,\n forwardEndpoint: env.VITE_GEOCODER_ENDPOINT?.trim() || provider.defaultForwardEndpoint,\n reverseEndpoint: env.VITE_GEOCODER_REVERSE_ENDPOINT?.trim() || provider.defaultReverseEndpoint,\n email: env.VITE_GEOCODER_EMAIL?.trim() || undefined,\n apiKey: env.VITE_GEOCODER_API_KEY?.trim() || undefined,\n };\n}\n\n/** Inputs to {@link resolveGeocoderConfig}, mirroring the project preference. */\nexport interface GeocodingPreferenceInput {\n providerId?: string;\n /** Per-provider API keys keyed by provider id. */\n apiKeys?: Record<string, string>;\n /** Optional custom forward endpoint (else the provider default). */\n forwardEndpoint?: string;\n /** Optional custom reverse endpoint (else the provider default). */\n reverseEndpoint?: string;\n email?: string;\n}\n\n/**\n * Build a {@link GeocoderConfig} from structured project preferences. Used by\n * the geocode dialog so a per-run provider choice resolves the right endpoints\n * and API key without round-tripping through runtime env.\n */\nexport function resolveGeocoderConfig(input: GeocodingPreferenceInput): GeocoderConfig {\n const provider = getGeocodingProvider(input.providerId);\n return {\n providerId: provider.id,\n forwardEndpoint: input.forwardEndpoint?.trim() || provider.defaultForwardEndpoint,\n reverseEndpoint: input.reverseEndpoint?.trim() || provider.defaultReverseEndpoint,\n email: input.email?.trim() || undefined,\n apiKey: input.apiKeys?.[provider.id]?.trim() || undefined,\n };\n}\n\n/** Hostname of `endpoint`, or null when it does not parse as a URL. */\nfunction geocoderHostname(endpoint: string): string | null {\n try {\n return new URL(endpoint).hostname;\n } catch {\n return null;\n }\n}\n\n/** Whether `hostname` is the public CartoCiudad service or a subdomain of it. */\nfunction isCartociudadHost(hostname: string | null): boolean {\n if (hostname === null) return false;\n return hostname === CARTOCIUDAD_PUBLIC_HOST || hostname.endsWith(`.${CARTOCIUDAD_PUBLIC_HOST}`);\n}\n\n/**\n * Whether requests to `endpoint` must be throttled AND row-capped. True for the\n * public Nominatim host (its usage policy applies) and, defensively, for any\n * endpoint that does not parse as a URL. Every other host (a self-hosted\n * Nominatim or a keyed provider) returns false.\n *\n * Note this is the *Nominatim policy* gate, not \"is this host paced at all\" —\n * see {@link geocoderMinIntervalMs}, which paces CartoCiudad more gently\n * without capping its row count.\n */\nexport function shouldThrottle(endpoint: string): boolean {\n const hostname = geocoderHostname(endpoint);\n return hostname === null || hostname === NOMINATIM_PUBLIC_HOST;\n}\n\n/**\n * The row cap to apply for `endpoint`: a finite cap for the public Nominatim\n * host, else Infinity. CartoCiudad is uncapped — IGN publishes no row limit and\n * its own bulk address geocoder processes up to 60,000 records per run, so a\n * cap here would be an invented constraint. It is still paced; see\n * {@link geocoderMinIntervalMs}.\n */\nexport function rowCap(endpoint: string): number {\n return shouldThrottle(endpoint) ? PUBLIC_GEOCODE_ROW_CAP : Number.POSITIVE_INFINITY;\n}\n\n/**\n * Minimum spacing (ms) between requests for `endpoint`. Gated on the hostname,\n * not the selected provider, so pointing any provider at the public Nominatim\n * host still honors its 1 req/sec policy.\n *\n * CartoCiudad gets a gentler pace of its own. IGN documents no rate limit for\n * the single-lookup `find`/`reverseGeocode` endpoints either way, so the honest\n * position is that unbounded bursting is unverified rather than sanctioned:\n * a large CSV run against a free public government service with zero spacing\n * risks getting the caller's IP throttled and degrading the service for\n * everyone. Nominatim's 1 req/sec is that provider's published policy and is\n * not borrowed here. Keyed providers and self-hosted endpoints are paced by\n * their own quotas, not by us.\n */\nexport function geocoderMinIntervalMs(endpoint: string): number {\n if (shouldThrottle(endpoint)) return NOMINATIM_MIN_INTERVAL_MS;\n return isCartociudadHost(geocoderHostname(endpoint)) ? CARTOCIUDAD_MIN_INTERVAL_MS : 0;\n}\n\n/** Hosted Pelias service that rejects keyless requests. */\nconst GEOCODE_EARTH_HOST_SUFFIX = \"geocode.earth\";\n\n/**\n * Whether `config` cannot geocode without an API key. True for the keyed\n * providers (ArcGIS, Mapbox, Google) and for Pelias only when it targets the\n * hosted geocode.earth endpoint (a self-hosted Pelias needs no key). Drives the\n * batch dialog's \"missing key\" warning and disabled run.\n */\nexport function geocoderNeedsApiKey(config: GeocoderConfig): boolean {\n const provider = getGeocodingProvider(config.providerId);\n if (provider.requiresApiKey) return true;\n if (provider.id === \"pelias\") {\n try {\n const host = new URL(config.forwardEndpoint).hostname;\n return host === GEOCODE_EARTH_HOST_SUFFIX || host.endsWith(`.${GEOCODE_EARTH_HOST_SUFFIX}`);\n } catch {\n return false;\n }\n }\n return false;\n}\n\n/**\n * Milliseconds to wait before starting the next request so consecutive\n * requests are spaced at least `intervalMs` apart, measured from the previous\n * request's start time (not its completion) so a slow network does not double\n * the wait. Returns 0 for the first request or when enough time has elapsed.\n */\nexport function nextDelayMs(lastStartedAt: number | null, now: number, intervalMs: number): number {\n if (lastStartedAt === null) return 0;\n return Math.max(0, intervalMs - (now - lastStartedAt));\n}\n\n/**\n * Build geocoding requests from parsed CSV rows. The address for each row is\n * the selected columns trimmed and joined with \", \" (so multi-part addresses\n * like street/city/country can be combined). Rows whose composed address is\n * empty are skipped.\n */\nexport function csvRowsToGeocodeRequests(\n rows: Record<string, string>[],\n addressColumns: string[],\n): GeocodeRequest[] {\n const requests: GeocodeRequest[] = [];\n rows.forEach((row, index) => {\n const address = addressColumns\n .map((column) => (row[column] ?? \"\").trim())\n .filter(Boolean)\n .join(\", \")\n .trim();\n if (!address) return;\n requests.push({ index, address, row });\n });\n return requests;\n}\n\n/**\n * The fetch implementation used by {@link geocodeForward}/{@link geocodeReverse}.\n * Null means \"use the global browser `fetch`\" (the default for the web and\n * embedded builds).\n */\nlet geocodingFetch: typeof globalThis.fetch | null = null;\n\n/**\n * Override the fetch used for geocoding requests, or pass null to restore the\n * global browser `fetch`.\n *\n * The desktop shell sets this to Tauri's native HTTP fetch so geocoding requests\n * are made from the Rust side, which:\n * - bypasses the WebView's CORS enforcement. Public Nominatim's CDN\n * intermittently omits the `Access-Control-Allow-Origin` header on cached\n * responses, which the browser then rejects — surfacing to the user as\n * \"Search failed. Try again.\" (the symptom that failed Microsoft Store\n * certification); and\n * - can send a `User-Agent` identifying the app, as Nominatim's usage policy\n * requires (browser fetch cannot set that header).\n *\n * Safe to call multiple times; only the most recent implementation is used.\n */\nexport function setGeocodingFetch(fetchImpl: typeof globalThis.fetch | null): void {\n geocodingFetch = fetchImpl;\n}\n\n/** The active geocoding fetch: the injected override, else the global fetch. */\nfunction geocodeFetch(): typeof globalThis.fetch {\n return geocodingFetch ?? fetch;\n}\n\n/**\n * Read a geocoder response body as JSON, treating an empty one as \"no results\".\n *\n * CartoCiudad answers an unmatched query with HTTP 204 and an empty body rather\n * than a JSON null (verified against the live `find` and `reverseGeocode`\n * endpoints). `response.ok` is true for 204, so parsing unconditionally would\n * throw a JSON syntax error and surface a nonsense address as \"Search failed\"\n * instead of \"no match\". Returning undefined lets each provider's parser report\n * no results the way it already does for a null body.\n */\nasync function readGeocodeJson(response: Response): Promise<unknown> {\n if (response.status === 204) return undefined;\n const text = await response.text();\n if (!text.trim()) return undefined;\n return JSON.parse(text) as unknown;\n}\n\n/**\n * Normalize a longitude into [-180, 180], or null when it is not finite.\n *\n * MapLibre reports `lngLat.lng` unwrapped once the map is zoomed out far enough\n * to show repeated world copies, so a click on a non-primary copy arrives as\n * e.g. 210 rather than -150. Rejecting those outright would silently drop a\n * legitimate reverse-geocode click for every provider, so wrap instead.\n *\n * An in-range longitude returns untouched: the modulo round-trip is not exact\n * in floating point (-3.7 comes back as -3.7000000000000455), and the common\n * case should not be perturbed to handle the rare one.\n */\nfunction wrapLongitude(lon: number): number | null {\n if (!Number.isFinite(lon)) return null;\n if (lon >= -180 && lon <= 180) return lon;\n return ((((lon + 180) % 360) + 360) % 360) - 180;\n}\n\n/**\n * Forward-geocode a single query through the configured provider, returning\n * normalized matches.\n */\nexport async function geocodeForward(\n query: string,\n options: { signal?: AbortSignal; config?: GeocoderConfig; limit?: number } = {},\n): Promise<GeocodeMatch[]> {\n const config = options.config ?? getGeocoderConfig();\n const provider = getGeocodingProvider(config.providerId);\n const url = provider.buildForwardUrl(config, query, {\n email: config.email,\n limit: options.limit,\n });\n const response = await geocodeFetch()(url, {\n signal: options.signal,\n headers: { Accept: \"application/json\" },\n });\n if (!response.ok) {\n throw new Error(`Geocoder returned HTTP ${response.status}`);\n }\n const data: unknown = await readGeocodeJson(response);\n return provider.parseForward(data);\n}\n\n/**\n * Reverse-geocode a single point ([lon, lat]) through the configured provider,\n * returning a normalized display result or null when no address was found.\n */\nexport async function geocodeReverse(\n lon: number,\n lat: number,\n options: { signal?: AbortSignal; config?: GeocoderConfig; zoom?: number } = {},\n): Promise<ReverseGeocodeDisplay | null> {\n const wrappedLon = wrapLongitude(lon);\n if (wrappedLon === null || !Number.isFinite(lat) || lat < -90 || lat > 90) {\n return null;\n }\n const config = options.config ?? getGeocoderConfig();\n const provider = getGeocodingProvider(config.providerId);\n const url = provider.buildReverseUrl(config, wrappedLon, lat, {\n email: config.email,\n zoom: options.zoom,\n });\n const response = await geocodeFetch()(url, {\n signal: options.signal,\n headers: { Accept: \"application/json\" },\n });\n if (!response.ok) {\n throw new Error(`Geocoder returned HTTP ${response.status}`);\n }\n const data: unknown = await readGeocodeJson(response);\n return provider.parseReverse(data);\n}\n","/**\n * Platform detection shared across the workspace. Carries no React/MapLibre\n * dependency so callers stay unit-testable.\n */\n\n/**\n * Whether a \"Macintosh\" user agent is really an iPad.\n *\n * iPadOS 13+ requests desktop sites by default and reports a macOS user agent,\n * so the UA string alone cannot tell an iPad from a Mac. Multi-touch does: a\n * real Mac reports `maxTouchPoints` 0 or 1, an iPad reports more.\n *\n * This lives in `@geolibre/core` because two packages need it and the rule is\n * an Apple behaviour that could change: the desktop app's `isMobile` (which\n * hides sidecar-backed tools on mobile) and `@geolibre/plugins`'\n * `isEarthEngineAvailable` (which hides Earth Engine sign-in on the Apple App\n * Store builds). Keeping one copy means a future correction lands in both.\n *\n * @param userAgent - The user agent string to test.\n * @param maxTouchPoints - `navigator.maxTouchPoints`, or undefined where the\n * runtime does not provide it (Node exposes a `navigator` without it).\n * @returns True when the UA claims macOS but the device reports multi-touch.\n */\nexport function isIpadDesktopUserAgent(\n userAgent: string,\n maxTouchPoints: number | undefined,\n): boolean {\n return /Macintosh/.test(userAgent) && (maxTouchPoints ?? 0) > 1;\n}\n","import { getGoogleMapsApiKey } from \"./runtime-env\";\n\n// Shared request-header resolution for 3D Tiles layers. Google Photorealistic\n// 3D Tiles authenticate with a Maps API key sent in the `X-GOOG-API-KEY` header,\n// but the key is deliberately stripped from the store/project record (so shared\n// projects never carry it in plain text) and re-injected at render time from\n// runtime env. Both the MapLibre/deck.gl render path (packages/plugins) and the\n// Cesium globe path (packages/map) must resolve headers the same way, so the\n// logic lives here in core rather than being duplicated per renderer.\n\n/** Google Photorealistic 3D Tiles root host + path. */\nconst GOOGLE_PHOTOREALISTIC_TILES_HOST = \"tile.googleapis.com\";\nconst GOOGLE_PHOTOREALISTIC_TILES_PATH = \"/v1/3dtiles/root.json\";\n/** Header Google's tile server expects the Maps API key in. */\nexport const GOOGLE_MAPS_API_KEY_HEADER = \"X-GOOG-API-KEY\";\n\n/** Whether `url` points at Google's Photorealistic 3D Tiles root tileset. */\nexport function isGooglePhotorealisticTilesetUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return (\n parsed.hostname === GOOGLE_PHOTOREALISTIC_TILES_HOST &&\n parsed.pathname === GOOGLE_PHOTOREALISTIC_TILES_PATH\n );\n } catch {\n return false;\n }\n}\n\n/** A masked placeholder (all asterisks) is a UI stand-in, not a real key. */\nfunction isMaskedKey(value: string): boolean {\n return /^\\*+$/.test(value.trim());\n}\n\n/** Drop the `X-GOOG-API-KEY` header (case-insensitive), keeping the rest. */\nexport function stripGoogleMapsApiKeyHeader(\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const entries = Object.entries(headers).filter(\n ([name]) => name.toLowerCase() !== GOOGLE_MAPS_API_KEY_HEADER.toLowerCase(),\n );\n return entries.length > 0 ? Object.fromEntries(entries) : undefined;\n}\n\n/**\n * The real `X-GOOG-API-KEY` value from `headers`, or `undefined` when absent,\n * blank, or a masked placeholder.\n */\nexport function googleMapsApiKeyHeaderValue(\n headers: Record<string, string> | undefined,\n): string | undefined {\n if (!headers) return undefined;\n const entry = Object.entries(headers).find(\n ([name]) => name.toLowerCase() === GOOGLE_MAPS_API_KEY_HEADER.toLowerCase(),\n );\n const value = entry?.[1].trim();\n if (!value || isMaskedKey(value)) return undefined;\n return value;\n}\n\n/** Collapse an empty record to `undefined`, otherwise pass it through. */\nexport function nonEmptyRecord(\n value: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n return value && Object.keys(value).length > 0 ? value : undefined;\n}\n\n/**\n * Resolve the request headers a 3D Tiles layer should load with. For non-Google\n * tilesets the headers pass through unchanged. For Google Photorealistic tiles\n * the `X-GOOG-API-KEY` header is (re)built from, in order: an explicit key in\n * the given headers, the `googleMapsApiKey` argument, then `getGoogleMapsApiKey()`\n * (runtime env). Returns `undefined` when there is nothing to send.\n */\nexport function resolveThreeDTilesRequestHeaders(\n url: string,\n headers: Record<string, string> | undefined,\n googleMapsApiKey?: string,\n): Record<string, string> | undefined {\n if (!isGooglePhotorealisticTilesetUrl(url)) return headers;\n const nonGoogleHeaders = stripGoogleMapsApiKeyHeader(headers);\n const apiKey = googleMapsApiKeyHeaderValue(headers) ?? googleMapsApiKey ?? getGoogleMapsApiKey();\n if (!apiKey) return nonEmptyRecord(nonGoogleHeaders);\n return {\n ...(nonGoogleHeaders ?? {}),\n [GOOGLE_MAPS_API_KEY_HEADER]: apiKey,\n };\n}\n\n/**\n * The request headers to persist on a 3D Tiles layer record. Non-Google\n * tilesets keep their headers; Google Photorealistic tiles keep any non-key\n * custom headers but never persist the `X-GOOG-API-KEY` value, so shared\n * projects do not carry the key in plain text (it is re-injected at render\n * time by `resolveThreeDTilesRequestHeaders`).\n */\nexport function persistedThreeDTilesRequestHeaders(\n url: string,\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!isGooglePhotorealisticTilesetUrl(url)) return headers;\n return nonEmptyRecord(stripGoogleMapsApiKeyHeader(headers));\n}\n","/**\n * Credentials a project refers to by name rather than carrying inline.\n *\n * On the desktop app the geocoding API keys and secret environment variables\n * of a project live in the OS keychain, keyed device-wide by provider id /\n * variable name. The project file keeps the names with empty values. Request\n * headers never go there: they reference variables as `${NAME}` instead (see\n * `resolveProjectHeaderReferences`). This module is the pure half of that\n * scheme: the account names, the overlay that fills empty values from the\n * stored ones, and the split that moves values out of a project before it is\n * written. The app installs the lookup; without one (web, Jupyter) every\n * function here is the identity, so values stay in the project file.\n *\n * A non-empty value in the project always wins over the stored one: a file\n * that still carries plaintext (older files, the web build) is used as-is for\n * the session.\n */\n\nimport { GEOCODING_PROVIDERS } from \"./geocoding\";\nimport { resolveHeaderReferences } from \"./header-references\";\nimport { useAppStore } from \"./store\";\nimport type {\n GeocodingPreferences,\n GeoLibreProject,\n ProjectPreferences,\n RuntimeEnvironmentVariable,\n} from \"./types\";\n\n// Account names are stable keychain data: renaming one orphans every value a\n// user already stored under it.\nexport function geocodingApiKeyAccount(providerId: string): string {\n return `project.geocoding.apiKey.${providerId}`;\n}\n\nexport function environmentVariableAccount(key: string): string {\n return `project.env.${key}`;\n}\n\n/** Rows are secret unless explicitly marked `secret: false`. */\nexport function isSecretEnvironmentVariable(variable: RuntimeEnvironmentVariable): boolean {\n return variable.secret !== false;\n}\n\nexport type ProjectCredentialLookup = (account: string) => string | undefined;\n\nlet projectCredentialLookup: ProjectCredentialLookup | null = null;\n\n/** Install (or clear, with `null`) the source of stored project credentials. */\nexport function setProjectCredentialLookup(lookup: ProjectCredentialLookup | null): void {\n projectCredentialLookup = lookup;\n}\n\n/** The stored value for `account`, or `undefined` when none (or no lookup). */\nexport function lookupProjectCredential(account: string): string | undefined {\n const value = projectCredentialLookup?.(account);\n return value ? value : undefined;\n}\n\n/** Fill empty geocoding API keys from the stored ones. */\nexport function overlayStoredGeocodingApiKeys(\n geocoding: GeocodingPreferences,\n): GeocodingPreferences {\n let apiKeys: Record<string, string> | null = null;\n for (const { id } of GEOCODING_PROVIDERS) {\n if (geocoding.apiKeys[id]?.trim()) continue;\n const stored = lookupProjectCredential(geocodingApiKeyAccount(id));\n if (!stored) continue;\n apiKeys ??= { ...geocoding.apiKeys };\n apiKeys[id] = stored;\n }\n return apiKeys ? { ...geocoding, apiKeys } : geocoding;\n}\n\n/** Fill empty secret environment variable values from the stored ones. */\nexport function overlayStoredEnvironmentVariables(\n variables: RuntimeEnvironmentVariable[],\n): RuntimeEnvironmentVariable[] {\n let changed = false;\n const overlaid = variables.map((variable) => {\n if (!isSecretEnvironmentVariable(variable) || variable.value !== \"\") return variable;\n const key = variable.key.trim();\n const stored = key ? lookupProjectCredential(environmentVariableAccount(key)) : undefined;\n if (!stored) return variable;\n changed = true;\n return { ...variable, value: stored };\n });\n return changed ? overlaid : variables;\n}\n\n/** Preferences with every empty stored credential filled in. */\nexport function overlayStoredPreferenceCredentials(\n preferences: ProjectPreferences,\n): ProjectPreferences {\n const geocoding = overlayStoredGeocodingApiKeys(preferences.geocoding);\n const environmentVariables = overlayStoredEnvironmentVariables(preferences.environmentVariables);\n if (\n geocoding === preferences.geocoding &&\n environmentVariables === preferences.environmentVariables\n ) {\n return preferences;\n }\n return { ...preferences, geocoding, environmentVariables };\n}\n\n/**\n * The credential changes an edited Settings draft makes, keyed by account\n * (`\"\"` deletes).\n *\n * - `seeded`: the overlaid preferences the dialog opened with.\n * - `next`: the normalized draft being saved.\n *\n * Only values the user changed are returned, so an untouched session override\n * (plaintext from the opened file) stays out of the keychain and opening a\n * file never writes it. Removing, renaming, or un-secreting a variable never\n * deletes its stored value: names are shared by every project on the device.\n * Clearing a field deletes the stored value only when the field showed that\n * stored value; clearing a file override that differed from it just drops the\n * override, so the shared value applies again and other projects keep it.\n */\nexport function changedPreferenceCredentials(\n seeded: ProjectPreferences,\n next: ProjectPreferences,\n): Record<string, string> {\n const changes: Record<string, string> = {};\n const record = (account: string, seededValue: string, nextValue: string) => {\n if (nextValue === seededValue) return;\n if (nextValue === \"\" && seededValue !== lookupProjectCredential(account)) return;\n changes[account] = nextValue;\n };\n\n const providerIds = new Set([\n ...Object.keys(seeded.geocoding.apiKeys),\n ...Object.keys(next.geocoding.apiKeys),\n ]);\n for (const providerId of providerIds) {\n record(\n geocodingApiKeyAccount(providerId),\n seeded.geocoding.apiKeys[providerId]?.trim() ?? \"\",\n next.geocoding.apiKeys[providerId]?.trim() ?? \"\",\n );\n }\n\n const seededSecrets = new Map<string, string>();\n for (const variable of seeded.environmentVariables) {\n if (!isSecretEnvironmentVariable(variable) || seededSecrets.has(variable.key)) continue;\n seededSecrets.set(variable.key, variable.value);\n }\n for (const variable of next.environmentVariables) {\n if (!isSecretEnvironmentVariable(variable)) continue;\n const seededValue = seededSecrets.get(variable.key);\n // A new blank row falls back to any value stored under the name; it\n // never deletes it.\n if (seededValue === undefined && variable.value === \"\") continue;\n record(environmentVariableAccount(variable.key), seededValue ?? \"\", variable.value);\n }\n\n return changes;\n}\n\n/**\n * Move unambiguous geocoding API keys and secret environment variable values\n * out of `project` (a copy; the input is untouched) into `secrets`, keyed by\n * account. Secret environment rows that have no unique name remain in the\n * project for an explicit keep/strip choice. Layers pass through: their\n * request headers reference variables as `${NAME}` rather than holding\n * secrets that need moving.\n */\nexport function splitProjectCredentials(project: GeoLibreProject): {\n project: GeoLibreProject;\n secrets: Record<string, string>;\n} {\n const secrets: Record<string, string> = {};\n\n for (const [providerId, apiKey] of Object.entries(project.preferences.geocoding.apiKeys)) {\n const trimmed = apiKey.trim();\n if (trimmed) secrets[geocodingApiKeyAccount(providerId)] = trimmed;\n }\n // A hand-edited file can contain duplicate names even though Settings\n // rejects them. One keychain account cannot preserve two different values.\n const nameCounts = new Map<string, number>();\n for (const variable of project.preferences.environmentVariables) {\n const key = variable.key.trim();\n if (key) nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);\n }\n const environmentVariables = project.preferences.environmentVariables.map((variable) => {\n if (!isSecretEnvironmentVariable(variable) || variable.value === \"\") return variable;\n const key = variable.key.trim();\n // Keep ambiguous or nameless rows intact for the local keep/strip prompt.\n if (!key || (nameCounts.get(key) ?? 0) > 1) return variable;\n secrets[environmentVariableAccount(key)] = variable.value;\n return { ...variable, value: \"\" };\n });\n\n return {\n project: {\n ...project,\n preferences: {\n ...project.preferences,\n geocoding: { ...project.preferences.geocoding, apiKeys: {} },\n environmentVariables,\n },\n },\n secrets,\n };\n}\n\n/**\n * `headers` with every `${NAME}` resolved from the project's enabled\n * Environment Variables (stored secret values filled in). Only those rows\n * count, not the OS environment or build-time values that share the runtime\n * env. A header referencing an unset variable is dropped.\n */\nexport function resolveProjectHeaderReferences(\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!headers) return headers;\n const values: Record<string, string> = {};\n const variables = overlayStoredEnvironmentVariables(\n useAppStore.getState().preferences.environmentVariables,\n );\n for (const variable of variables) {\n const key = variable.key.trim();\n if (variable.enabled && key) values[key] = variable.value;\n }\n return resolveHeaderReferences(headers, values);\n}\n","import type { FeatureCollection } from \"geojson\";\nimport type { GeoLibreProject, FieldVisibility } from \"./types\";\n\n/**\n * Returns a new FeatureCollection with properties marked as \"excluded\" removed.\n */\nexport function excludeHiddenFieldsFromGeojson(\n geojson: FeatureCollection,\n fieldVisibility?: Record<string, FieldVisibility>,\n): FeatureCollection {\n const excludedKeys = new Set(\n Object.entries(fieldVisibility || {})\n .filter(([_, visibility]) => visibility === \"excluded\")\n .map(([key]) => key),\n );\n\n if (excludedKeys.size === 0) {\n return geojson;\n }\n\n // Deep clone to avoid mutating the live store state\n const stripped: FeatureCollection = {\n ...geojson,\n features: geojson.features.map((feature) => {\n const properties = { ...feature.properties };\n for (const key of excludedKeys) {\n delete properties[key];\n }\n return { ...feature, properties };\n }),\n };\n\n return stripped;\n}\n\n/**\n * Returns a new GeoLibreProject where all layers have their excluded fields\n * physically removed from their inline GeoJSON.\n */\nexport function excludeHiddenFieldsFromProject(project: GeoLibreProject): GeoLibreProject {\n let changed = false;\n const layers = project.layers.map((layer) => {\n if (!layer.fieldVisibility) return layer;\n\n let updatedLayer = layer;\n\n if (layer.geojson) {\n const strippedGeojson = excludeHiddenFieldsFromGeojson(layer.geojson, layer.fieldVisibility);\n if (strippedGeojson !== layer.geojson) {\n changed = true;\n updatedLayer = { ...updatedLayer, geojson: strippedGeojson };\n }\n }\n\n if (layer.metadata?.embeddedGeoJSON) {\n const strippedEmbedded = excludeHiddenFieldsFromGeojson(\n layer.metadata.embeddedGeoJSON as FeatureCollection,\n layer.fieldVisibility,\n );\n if (strippedEmbedded !== layer.metadata.embeddedGeoJSON) {\n changed = true;\n updatedLayer = {\n ...updatedLayer,\n metadata: {\n ...updatedLayer.metadata,\n embeddedGeoJSON: strippedEmbedded,\n },\n };\n }\n }\n\n return updatedLayer;\n });\n\n return changed ? { ...project, layers } : project;\n}\n","import type { Feature, FeatureCollection } from \"geojson\";\nimport type { EditorTrackingConfig } from \"./types\";\n\n/** Default field names for maintained editor tracking columns. */\nexport const DEFAULT_EDITOR_TRACKING_CONFIG: Required<EditorTrackingConfig> = {\n enabled: false,\n createdByField: \"created_by\",\n createdAtField: \"created_at\",\n editedByField: \"edited_by\",\n editedAtField: \"edited_at\",\n};\n\n/**\n * Identity written to the author fields when nothing better is known.\n *\n * Deliberately NOT translated: it is written into the user's data, where a\n * value that changes with the UI language would make the same editor appear as\n * several different people across a project's history.\n */\nexport const DEFAULT_EDITOR_IDENTITY = \"local-user\";\n\n/**\n * `localStorage` key holding the display name this browser edits under.\n *\n * Shared with the Comments panel, which asks for the same name and stores it\n * here, so a user who has introduced themselves once is recognized by both\n * features instead of being prompted twice.\n */\nexport const EDITOR_IDENTITY_STORAGE_KEY = \"geolibre_author_name\";\n\n/**\n * Choose the identity to stamp, most authoritative source first.\n *\n * A live collaboration session names its participants, so that name wins: it is\n * the one other people in the session see attached to the same edits. Otherwise\n * fall back to the locally configured display name, then to\n * {@link DEFAULT_EDITOR_IDENTITY}.\n *\n * @param collabName Display name from an active collaboration session.\n * @param storedName Locally configured display name.\n * @returns A non-empty identity string.\n */\nexport function pickEditorIdentity(collabName?: string | null, storedName?: string | null): string {\n return collabName?.trim() || storedName?.trim() || DEFAULT_EDITOR_IDENTITY;\n}\n\n/**\n * The four maintained column names, or `null` when tracking is off or the\n * configuration is unusable. Ordered created-then-edited so callers that render\n * the columns get a stable, meaningful order.\n */\nexport function editorTrackingFieldNames(config?: EditorTrackingConfig): string[] | null {\n const resolved = resolveEditorTrackingConfigForQuery(config);\n if (!resolved?.enabled) return null;\n return [\n resolved.createdByField,\n resolved.createdAtField,\n resolved.editedByField,\n resolved.editedAtField,\n ];\n}\n\n/**\n * Options for editor tracking stamping functions.\n */\nexport interface EditorTrackingStampOptions {\n /** Optional custom editor tracking field configuration. */\n config?: EditorTrackingConfig;\n /**\n * Identity string of the author/editor (e.g. username, email, or client ID).\n * Defaults to {@link DEFAULT_EDITOR_IDENTITY}.\n */\n userIdentity?: string;\n /** ISO timestamp override for deterministic testing or batch operations. Defaults to `new Date().toISOString()`. */\n timestamp?: string;\n}\n\n/**\n * Fully resolve an {@link EditorTrackingConfig} with fallback default field names.\n */\nexport function resolveEditorTrackingConfig(\n config?: EditorTrackingConfig,\n): Required<EditorTrackingConfig> {\n // Trimmed on the way in, because these become literal GeoJSON property keys:\n // a name typed as `\" created_by \"` in the settings form would otherwise pass\n // validation and then write a column whose name has invisible padding,\n // matching neither what the user typed nor the default it looks like.\n const resolved = {\n enabled: config?.enabled ?? DEFAULT_EDITOR_TRACKING_CONFIG.enabled,\n createdByField: (\n config?.createdByField ?? DEFAULT_EDITOR_TRACKING_CONFIG.createdByField\n ).trim(),\n createdAtField: (\n config?.createdAtField ?? DEFAULT_EDITOR_TRACKING_CONFIG.createdAtField\n ).trim(),\n editedByField: (config?.editedByField ?? DEFAULT_EDITOR_TRACKING_CONFIG.editedByField).trim(),\n editedAtField: (config?.editedAtField ?? DEFAULT_EDITOR_TRACKING_CONFIG.editedAtField).trim(),\n };\n\n // Tracking that is off writes nothing, so the names cannot matter — and a\n // half-filled or hand-edited config left behind on a disabled layer must not\n // make every save on that layer throw. Validate only what will be used.\n if (!resolved.enabled) return resolved;\n\n const fields = [\n resolved.createdByField,\n resolved.createdAtField,\n resolved.editedByField,\n resolved.editedAtField,\n ];\n if (fields.some((field) => field === \"\") || new Set(fields).size !== fields.length) {\n throw new Error(\"Editor tracking field names must be non-empty and unique\");\n }\n\n return resolved;\n}\n\n/**\n * Resolve a config for the query helpers, yielding `null` rather than throwing when\n * the configured field names are blank or collide.\n *\n * The query helpers run once per field on every Attribute Table / Field Calculator\n * render, and the field names are user-editable strings, so a half-filled settings\n * form or a hand-edited `.geolibre.json` must degrade to \"tracking off\" instead of\n * crashing the panel. The stamping helpers keep the throw: a write that silently\n * skipped tracking would leave a layer looking tracked while recording nothing.\n */\nfunction resolveEditorTrackingConfigForQuery(\n config?: EditorTrackingConfig,\n): Required<EditorTrackingConfig> | null {\n try {\n return resolveEditorTrackingConfig(config);\n } catch {\n return null;\n }\n}\n\n/**\n * Check whether a field name corresponds to one of the maintained editor tracking columns.\n *\n * Returns `false` for an invalid config, so callers can use this as a plain predicate.\n */\nexport function isMaintainedEditorTrackingField(\n fieldName: string,\n config?: EditorTrackingConfig,\n): boolean {\n const resolved = resolveEditorTrackingConfigForQuery(config);\n if (!resolved?.enabled) {\n return false;\n }\n return (\n fieldName === resolved.createdByField ||\n fieldName === resolved.createdAtField ||\n fieldName === resolved.editedByField ||\n fieldName === resolved.editedAtField\n );\n}\n\n/**\n * Ensure all configured editor tracking field names are included in a field list.\n *\n * Returns the list unchanged for an invalid config, matching\n * {@link isMaintainedEditorTrackingField}.\n */\nexport function ensureEditorTrackingFields(\n fields: string[],\n config?: EditorTrackingConfig,\n): string[] {\n const trackingFields = editorTrackingFieldNames(config);\n if (!trackingFields) {\n return fields;\n }\n const result = [...fields];\n for (const tf of trackingFields) {\n if (!result.includes(tf)) {\n result.push(tf);\n }\n }\n return result;\n}\n\n/**\n * Stamp editor tracking metadata onto a feature's properties object.\n *\n * On `\"create\"`: sets all four columns to this editor and this timestamp.\n * `\"create\"` states that the feature is being created here and now, so it is\n * authoritative — a value already sitting in one of these columns describes\n * some other feature, not this one. That case is reachable: the geometry\n * editor's copy action clones a tracked feature's properties, and a Field\n * Collection form can define a capture field whose key happens to be a tracking\n * column. Both would otherwise credit a brand-new feature to whoever created\n * the thing it was copied from.\n *\n * On `\"update\"`: sets `edited_by`/`edited_at` and preserves `created_by` /\n * `created_at`, which record something this edit did not change.\n */\nexport function stampFeaturePropertiesEditorTracking(\n properties: Record<string, unknown> | null | undefined,\n action: \"create\" | \"update\",\n options?: EditorTrackingStampOptions,\n): Record<string, unknown> {\n const resolved = resolveEditorTrackingConfig(options?.config);\n if (!resolved.enabled) {\n return properties ? { ...properties } : {};\n }\n\n const result: Record<string, unknown> = properties ? { ...properties } : {};\n const now = options?.timestamp ?? new Date().toISOString();\n const actor = options?.userIdentity || DEFAULT_EDITOR_IDENTITY;\n\n if (action === \"create\") {\n result[resolved.createdByField] = actor;\n result[resolved.createdAtField] = now;\n }\n\n result[resolved.editedByField] = actor;\n result[resolved.editedAtField] = now;\n\n return result;\n}\n\n/**\n * Stamp editor tracking metadata onto a GeoJSON {@link Feature}.\n */\nexport function stampFeatureEditorTracking<T extends Feature>(\n feature: T,\n action: \"create\" | \"update\",\n options?: EditorTrackingStampOptions,\n): T {\n const resolved = resolveEditorTrackingConfig(options?.config);\n if (!resolved.enabled) {\n return feature;\n }\n\n return {\n ...feature,\n properties: stampFeaturePropertiesEditorTracking(feature.properties, action, options),\n };\n}\n\n/**\n * Stamp editor tracking metadata onto all features in a {@link FeatureCollection}.\n */\nexport function stampFeatureCollectionEditorTracking(\n collection: FeatureCollection,\n action: \"create\" | \"update\",\n options?: EditorTrackingStampOptions,\n): FeatureCollection {\n const resolved = resolveEditorTrackingConfig(options?.config);\n if (!resolved.enabled) {\n return collection;\n }\n\n const timestamp = options?.timestamp ?? new Date().toISOString();\n const opts = { ...options, timestamp };\n\n return {\n ...collection,\n features: collection.features.map((feat) => stampFeatureEditorTracking(feat, action, opts)),\n };\n}\n","import { EDITOR_IDENTITY_STORAGE_KEY, pickEditorIdentity } from \"./editor-tracking\";\nimport { useAppStore } from \"./store\";\n\n/**\n * Runtime resolution of \"who is editing\", for the editor tracking stamps.\n *\n * Split from the pure `editor-tracking.ts` helpers because it reads two ambient\n * sources — the collaboration session in the store and `localStorage` — which\n * the stamping functions must stay free of so they remain unit-testable and\n * usable from Node (the MCP server authors projects without either).\n */\n\n/**\n * The display name this browser edits under, or `\"\"` when none is set.\n *\n * `localStorage` throws rather than returning null in a partitioned or\n * storage-blocked context (Safari private browsing, an embed in a third-party\n * iframe), so a failure here has to read as \"no name set\" — an editor whose\n * browser refuses storage must still be able to edit.\n */\nexport function readStoredAuthorName(): string {\n try {\n if (typeof localStorage === \"undefined\") return \"\";\n return localStorage.getItem(EDITOR_IDENTITY_STORAGE_KEY)?.trim() ?? \"\";\n } catch {\n return \"\";\n }\n}\n\n/**\n * Persist the display name to edit under. A blank name clears it, so the user\n * can go back to the anonymous default rather than being stuck with a typo.\n */\nexport function setStoredAuthorName(name: string): void {\n try {\n if (typeof localStorage === \"undefined\") return;\n const trimmed = name.trim();\n if (trimmed) {\n localStorage.setItem(EDITOR_IDENTITY_STORAGE_KEY, trimmed);\n } else {\n localStorage.removeItem(EDITOR_IDENTITY_STORAGE_KEY);\n }\n } catch {\n // Storage unavailable: the name applies to this session only.\n }\n}\n\n/**\n * The identity to stamp on edits made right now: the collaboration session's\n * name when one is active, else the locally configured name, else the anonymous\n * default.\n *\n * @returns A non-empty identity string.\n */\nexport function currentEditorIdentity(): string {\n const { isActive, selfName } = useAppStore.getState().collaboration;\n return pickEditorIdentity(isActive ? selfName : undefined, readStoredAuthorName());\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["uuidv4","isPlainObject","isPlainObject","uuidv4","MAX_REDACT_DEPTH","uuidv4"],"sources":["../src/diagram.ts","../src/marker-shape.ts","../src/hyperlink.ts","../src/photo.ts","../src/color-ramp.ts","../src/paths.ts","../src/routing.ts","../src/vector-color.ts","../src/style-layer-evaluator.ts","../src/document-locale.ts","../src/label-number-format.ts","../src/external-native-paint.ts","../src/attribute-form.ts","../src/popup.ts","../src/quick-filters.ts","../src/layer-filters.ts","../src/deployment-capabilities.ts","../src/store/capabilities-slice.ts","../src/store/collaboration-slice.ts","../src/store/session-slice.ts","../src/store/layer-removal.ts","../src/store/layer-groups-slice.ts","../src/file-name.ts","../src/layer-defaults.ts","../src/layer-style-clipboard.ts","../src/layer-style-file.ts","../src/store/layers-slice.ts","../src/layer-library.ts","../src/store/libraries-slice.ts","../src/store/map-view-slice.ts","../src/store/processing-slice.ts","../src/store/project-content-slice.ts","../src/history.ts","../src/store/undo-history.ts","../src/store/project-slice.ts","../src/store/ui-slice.ts","../src/store.ts","../src/selection-actions.ts","../src/scale-units.ts","../src/elevation.ts","../src/camera-altitude.ts","../src/layers-file.ts","../src/pixel-format.ts","../src/s3.ts","../src/storymap-sample.ts","../src/storymap-playback.ts","../src/storymap-io.ts","../src/layer-selectors.ts","../src/platform.ts","../src/three-d-tiles.ts","../src/project-credential-refs.ts","../src/visibility.ts","../src/editor-tracking.ts","../src/editor-identity.ts","../src/datum-shift.ts","../src/cog-jpeg-tables.ts"],"sourcesContent":["/**\n * Pure helpers for the diagram renderer (per-feature pie/donut/bar charts,\n * QGIS-style diagram symbology). Everything here is DOM-free so the desktop\n * app, the deck.gl overlay, and the node test suite share one implementation:\n * anchor placement, value extraction, and size scaling. The canvas drawing and\n * deck.gl layer construction live in `@geolibre/plugins`.\n */\nimport type { Feature, FeatureCollection, Geometry, Position } from \"geojson\";\nimport { styleValue, type LayerStyle } from \"./types\";\n\n/**\n * Hard cap on how many features get a diagram. Each diagram is rasterized into\n * an icon atlas, so an unbounded layer (e.g. a million-point parquet) would\n * hang the UI; typical thematic layers (counties, districts) sit well below\n * this.\n */\nexport const MAX_DIAGRAM_FEATURES = 2000;\n\n/**\n * Hard cap on how many raw features {@link collectDiagramData} visits while\n * looking for drawable diagrams. Without it, a layer whose drawable ratio is\n * low (sparse attributes, degenerate geometry) degenerates to a full scan of\n * an arbitrarily large collection on every rebuild. Features past this bound\n * are treated as truncation, which the Style Panel surfaces.\n */\nexport const MAX_DIAGRAM_SCAN_FEATURES = 100_000;\n\n/** Smallest rendered diagram size in pixels under scaled sizing. */\nexport const MIN_DIAGRAM_SIZE = 8;\n\n/** One feature's chart, ready for drawing. */\nexport interface DiagramDatum {\n /** Anchor position: the point location or polygon/line midpoint centroid. */\n position: [number, number];\n /** One value per configured diagram field (negatives clamped to 0). */\n values: number[];\n /** Sum of {@link values}. */\n total: number;\n /** The value driving scaled sizing (total, attribute value, or 1 for fixed). */\n sizeValue: number;\n}\n\n/** The per-layer diagram dataset plus the maxima needed for scaling. */\nexport interface DiagramData {\n data: DiagramDatum[];\n /** Largest {@link DiagramDatum.sizeValue} (0 when there is no data). */\n maxSizeValue: number;\n /** Largest single field value across all features (for bar axis scaling). */\n maxFieldValue: number;\n /** Largest {@link DiagramDatum.total} across all features. */\n maxTotal: number;\n /** True when the feature cap truncated the dataset. */\n truncated: boolean;\n}\n\n/**\n * Whether a layer style has a drawable diagram configuration: a chart type and\n * at least one mapped attribute.\n *\n * @param style - The layer style to test.\n */\nexport function isDiagramStyleEnabled(style: LayerStyle): boolean {\n return (\n styleValue(style, \"diagramType\") !== \"none\" &&\n styleValue(style, \"diagramFields\").some((field) => field.property !== \"\")\n );\n}\n\n/**\n * Whether a heatmap/cluster point renderer suppresses the layer's diagrams.\n * The point renderer only applies (and is only user-editable) on point-only\n * layers, so a stale `pointRenderer` value on a layer that has since gained\n * non-point geometry must not block diagrams — this mirrors the Style Panel's\n * own `!supportsPointRenderer || pointRenderer === \"single\"` visibility gate,\n * keeping the render and legend gates consistent with what the panel lets the\n * user configure. The full feature list is scanned (matching the panel's\n * isPointOnlyGeoJsonLayer, which uses an uncapped every()) so the two gates\n * can never disagree on layers whose non-point features sit past a sampling\n * window; the scan only runs for the uncommon non-\"single\" renderer values.\n *\n * @param geojson - The layer's feature collection (undefined suppresses nothing).\n * @param style - The layer style carrying `pointRenderer`.\n */\nexport function diagramsSuppressedByPointRenderer(\n geojson: FeatureCollection | undefined,\n style: LayerStyle,\n): boolean {\n if (styleValue(style, \"pointRenderer\") === \"single\") return false;\n const features = geojson?.features ?? [];\n if (features.length === 0) return false;\n return features.every((feature) => {\n const type = feature?.geometry?.type;\n return type === \"Point\" || type === \"MultiPoint\";\n });\n}\n\n/** Shoelace area of a linear ring (planar, in degree space; sign = winding). */\nfunction ringArea(ring: Position[]): number {\n let area = 0;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n // Same sign convention as ringCentroid's cross terms, so centroid / area\n // divisions stay consistent for either winding.\n area += ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];\n }\n return area / 2;\n}\n\n/**\n * Area-weighted centroid of a polygon's outer ring, falling back to the ring's\n * vertex average when the area is degenerate. Planar math in degree space is\n * fine at diagram-placement accuracy.\n */\nfunction ringCentroid(ring: Position[]): [number, number] | null {\n if (ring.length === 0) return null;\n const area = ringArea(ring);\n if (Math.abs(area) < 1e-12) {\n let x = 0;\n let y = 0;\n for (const [lng, lat] of ring) {\n x += lng;\n y += lat;\n }\n return [x / ring.length, y / ring.length];\n }\n let cx = 0;\n let cy = 0;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {\n const cross = ring[j][0] * ring[i][1] - ring[i][0] * ring[j][1];\n cx += (ring[j][0] + ring[i][0]) * cross;\n cy += (ring[j][1] + ring[i][1]) * cross;\n }\n return [cx / (6 * area), cy / (6 * area)];\n}\n\nfunction finitePosition(position: Position | undefined): [number, number] | null {\n if (!position) return null;\n const [lng, lat] = position;\n return Number.isFinite(lng) && Number.isFinite(lat) ? [lng as number, lat as number] : null;\n}\n\n/**\n * Where a feature's diagram is anchored: the point itself, a line's middle\n * vertex, or the polygon centroid (largest part of a multi-polygon). Returns\n * null for empty/degenerate geometry so the feature is skipped.\n *\n * @param geometry - The feature geometry (may be null for null-geometry rows).\n */\nexport function diagramAnchor(geometry: Geometry | null | undefined): [number, number] | null {\n if (!geometry) return null;\n switch (geometry.type) {\n case \"Point\":\n return finitePosition(geometry.coordinates);\n case \"MultiPoint\":\n return finitePosition(geometry.coordinates[0]);\n case \"LineString\": {\n const coords = geometry.coordinates;\n return finitePosition(coords[Math.floor(coords.length / 2)]);\n }\n case \"MultiLineString\": {\n // Anchor on the longest part (by vertex count) so the diagram sits on\n // the feature's dominant line.\n let longest: Position[] = [];\n for (const part of geometry.coordinates) {\n if (part.length > longest.length) longest = part;\n }\n return finitePosition(longest[Math.floor(longest.length / 2)]);\n }\n case \"Polygon\":\n return ringCentroid(geometry.coordinates[0] ?? []);\n case \"MultiPolygon\": {\n let best: Position[] = [];\n let bestArea = -1;\n for (const polygon of geometry.coordinates) {\n const ring = polygon[0] ?? [];\n const area = Math.abs(ringArea(ring));\n if (area > bestArea) {\n bestArea = area;\n best = ring;\n }\n }\n return ringCentroid(best);\n }\n case \"GeometryCollection\": {\n for (const child of geometry.geometries) {\n const anchor = diagramAnchor(child);\n if (anchor) return anchor;\n }\n return null;\n }\n default:\n return null;\n }\n}\n\n/** Reads a feature property as a non-negative finite number (else 0). */\nfunction readValue(feature: Feature, property: string): number {\n const raw = feature.properties?.[property];\n const num = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(num) && num > 0 ? num : 0;\n}\n\n/**\n * Extract the per-feature diagram dataset from a layer's GeoJSON and style:\n * one {@link DiagramDatum} per feature that has an anchor and at least one\n * positive value, capped at {@link MAX_DIAGRAM_FEATURES}, plus the global\n * maxima scaled sizing and bar axes need.\n *\n * @param geojson - The layer's feature collection.\n * @param style - The layer style carrying the diagram configuration.\n */\nexport function collectDiagramData(geojson: FeatureCollection, style: LayerStyle): DiagramData {\n const properties = styleValue(style, \"diagramFields\")\n .map((field) => field.property)\n .filter((property) => property !== \"\");\n const sizeMode = styleValue(style, \"diagramSizeMode\");\n const sizeProperty = styleValue(style, \"diagramSizeProperty\");\n\n const data: DiagramDatum[] = [];\n let maxSizeValue = 0;\n let maxFieldValue = 0;\n let maxTotal = 0;\n let truncated = false;\n let visited = 0;\n\n for (const feature of geojson.features) {\n if (data.length >= MAX_DIAGRAM_FEATURES || visited >= MAX_DIAGRAM_SCAN_FEATURES) {\n truncated = true;\n break;\n }\n visited += 1;\n const position = diagramAnchor(feature.geometry);\n if (!position) continue;\n const values = properties.map((property) => readValue(feature, property));\n const total = values.reduce((sum, value) => sum + value, 0);\n if (total <= 0) continue;\n // Attribute sizing with no attribute chosen yet falls back to a constant\n // (full-size diagrams) instead of collapsing everything to the floor.\n const sizeValue =\n sizeMode === \"sum\"\n ? total\n : sizeMode === \"attribute\" && sizeProperty !== \"\"\n ? readValue(feature, sizeProperty)\n : 1;\n if (sizeValue > maxSizeValue) maxSizeValue = sizeValue;\n if (total > maxTotal) maxTotal = total;\n for (const value of values) {\n if (value > maxFieldValue) maxFieldValue = value;\n }\n data.push({ position, values, total, sizeValue });\n }\n\n return { data, maxSizeValue, maxFieldValue, maxTotal, truncated };\n}\n\n/**\n * The rendered size (diameter for pie/donut, box height/width for bars) of one\n * diagram in pixels. Fixed sizing returns the configured size; scaled sizing\n * maps the datum's size value onto the configured size by square root, so\n * diagram *area* tracks the value, with a small floor to keep every drawn\n * diagram legible.\n *\n * @param datum - The feature's diagram datum.\n * @param style - The layer style carrying the diagram configuration.\n * @param maxSizeValue - The dataset's largest size value (from\n * {@link collectDiagramData}).\n */\nexport function diagramPixelSize(\n datum: DiagramDatum,\n style: LayerStyle,\n maxSizeValue: number,\n): number {\n const size = Math.max(4, styleValue(style, \"diagramSize\"));\n if (styleValue(style, \"diagramSizeMode\") === \"fixed\") return size;\n if (maxSizeValue <= 0 || datum.sizeValue <= 0) return MIN_DIAGRAM_SIZE;\n return Math.max(MIN_DIAGRAM_SIZE, size * Math.sqrt(datum.sizeValue / maxSizeValue));\n}\n","import type { MarkerShape } from \"./types\";\n\n/**\n * Trace the outline of a built-in marker {@link MarkerShape} onto a 2D canvas\n * path, centered in a `size`×`size` box. The caller supplies the fill/stroke;\n * this only builds the path (via `beginPath`/`closePath`), so it stays free of\n * any DOM other than the passed context and can be shared by the map's sprite\n * baker (`@geolibre/map`) and the Print Layout legend renderer.\n *\n * `\"custom\"` (SVG) markers have no built-in path and are handled by the caller\n * (rasterized separately); passing one draws the default circle so the marker\n * never vanishes silently.\n *\n * @param ctx - Any 2D path-drawing context (a real `CanvasRenderingContext2D`\n * satisfies this structurally).\n * @param shape - The built-in marker shape to trace.\n * @param size - The box edge length in pixels; the shape is inset slightly so a\n * stroke is not clipped at the edge.\n */\nexport function drawMarkerPath(\n ctx: CanvasRenderingContext2D,\n shape: MarkerShape,\n size: number,\n): void {\n const c = size / 2;\n // Leave a small inset so the stroke is not clipped at the tile edge.\n const r = c * 0.82;\n ctx.beginPath();\n switch (shape) {\n case \"square\":\n ctx.rect(c - r, c - r, r * 2, r * 2);\n break;\n case \"triangle\":\n ctx.moveTo(c, c - r);\n ctx.lineTo(c + r, c + r);\n ctx.lineTo(c - r, c + r);\n ctx.closePath();\n break;\n case \"diamond\":\n ctx.moveTo(c, c - r);\n ctx.lineTo(c + r, c);\n ctx.lineTo(c, c + r);\n ctx.lineTo(c - r, c);\n ctx.closePath();\n break;\n case \"star\": {\n const outer = r;\n const inner = r * 0.42;\n for (let point = 0; point < 10; point += 1) {\n const radius = point % 2 === 0 ? outer : inner;\n const angle = (Math.PI / 5) * point - Math.PI / 2;\n const x = c + radius * Math.cos(angle);\n const y = c + radius * Math.sin(angle);\n if (point === 0) ctx.moveTo(x, y);\n else ctx.lineTo(x, y);\n }\n ctx.closePath();\n break;\n }\n case \"cross\": {\n const arm = r * 0.42;\n ctx.moveTo(c - arm, c - r);\n ctx.lineTo(c + arm, c - r);\n ctx.lineTo(c + arm, c - arm);\n ctx.lineTo(c + r, c - arm);\n ctx.lineTo(c + r, c + arm);\n ctx.lineTo(c + arm, c + arm);\n ctx.lineTo(c + arm, c + r);\n ctx.lineTo(c - arm, c + r);\n ctx.lineTo(c - arm, c + arm);\n ctx.lineTo(c - r, c + arm);\n ctx.lineTo(c - r, c - arm);\n ctx.lineTo(c - arm, c - arm);\n ctx.closePath();\n break;\n }\n case \"pin\": {\n // A teardrop: a circle bowl with a point at the bottom.\n const bowlR = r * 0.7;\n const bowlY = c - r * 0.2;\n ctx.moveTo(c, c + r);\n ctx.quadraticCurveTo(c - bowlR, bowlY + bowlR * 0.4, c - bowlR, bowlY);\n ctx.arc(c, bowlY, bowlR, Math.PI, Math.PI * 2);\n ctx.quadraticCurveTo(c + bowlR, bowlY + bowlR * 0.4, c, c + r);\n ctx.closePath();\n break;\n }\n case \"circle\":\n default:\n ctx.arc(c, c, r, 0, Math.PI * 2);\n }\n}\n\nconst RASTER_IMAGE_EXTENSION = /\\.(?:png|jpe?g|gif|webp|bmp|avif)$/i;\n\n/**\n * Whether a custom marker source is a raster image (PNG, JPEG, GIF, ...) rather\n * than SVG: a non-SVG `data:image/...` URL, or an `http(s)` URL whose path ends\n * in a raster extension. Raster markers cannot be recolored, so the marker\n * color and per-class colors only apply to SVG sources. A remote URL without a\n * telling extension is not classified here; the map sniffs its content type.\n *\n * @param markup - The `markerSvg` value.\n * @returns `true` for a recognizably raster source.\n */\nexport function isRasterMarkerSource(markup: string): boolean {\n const trimmed = markup.trim();\n if (/^data:image\\//i.test(trimmed)) return !/^data:image\\/svg\\+xml/i.test(trimmed);\n if (!/^https?:\\/\\//i.test(trimmed)) return false;\n return RASTER_IMAGE_EXTENSION.test(urlPath(trimmed));\n}\n\n/**\n * Whether a remote marker response holds SVG text, judged by its\n * `content-type` header: `image/svg+xml`, or a text/XML type (a raw GitHub\n * file is served as `text/plain`), reads as SVG and any other `image/*` does\n * not. A missing or generic header falls back to a `.svg` path.\n *\n * @param url - The requested URL.\n * @param contentType - The response's `content-type` header, if any.\n * @returns `true` when the body should be read as SVG markup.\n */\nexport function isSvgMarkerResponse(url: string, contentType: string | null): boolean {\n const type = (contentType ?? \"\").split(\";\")[0].trim().toLowerCase();\n if (type === \"image/svg+xml\") return true;\n if (type.startsWith(\"image/\")) return false;\n if (type.startsWith(\"text/\") || type.endsWith(\"/xml\") || type.endsWith(\"+xml\")) return true;\n return /\\.svg$/i.test(urlPath(url));\n}\n\nfunction urlPath(url: string): string {\n try {\n return new URL(url).pathname;\n } catch {\n return url.split(/[?#]/)[0];\n }\n}\n\n// Remote SVG sources we have already warned about, so the console message below\n// fires once per distinct URL instead of on every image regeneration.\nconst warnedRemoteSvgSources = new Set<string>();\n\n/**\n * Resolve user-supplied SVG input to an `Image.src`: inline markup (starting\n * with `<`) is encoded as a data URL; otherwise only `data:` and `http(s):`\n * URLs are accepted. Returns null for empty input or an unsupported scheme\n * (e.g. `file:`), which the caller treats as \"no image\" rather than letting an\n * arbitrary URL be loaded.\n *\n * Remote `http(s):` URLs are supported intentionally (custom marker/pattern\n * SVGs) but trigger a cross-origin request when rendered. Because a shared\n * `.geolibre.json` can carry such a URL, we log a one-time warning so the\n * outbound request is visible; prefer inline `<svg>` or `data:` in shared\n * projects.\n *\n * Lives here (rather than beside the map's sprite baker) because every surface\n * that previews a custom marker — the map, the Print Layout legend, the on-map\n * Legend panel — must resolve the same input the same way. A second, local\n * `data:image/svg+xml,…` wrapper is exactly what left URL and `data:` markers\n * blank in the on-map legend while the map drew them fine.\n *\n * @param markup - Raw SVG markup, a `data:` URL, or an `http(s)` URL.\n * @returns An `Image.src` value, or `null` when nothing loadable was given.\n */\nexport function resolveSvgSource(markup: string): string | null {\n const trimmed = markup.trim();\n if (!trimmed) return null;\n if (trimmed.startsWith(\"<\")) {\n return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(trimmed)}`;\n }\n if (/^https?:\\/\\//i.test(trimmed)) {\n if (!warnedRemoteSvgSources.has(trimmed)) {\n warnedRemoteSvgSources.add(trimmed);\n console.warn(\n `[geolibre] Loading a custom image from a remote URL triggers a ` +\n `cross-origin request: ${trimmed}. Prefer inline <svg> markup or a ` +\n `data: URL in shared projects.`,\n );\n }\n return trimmed;\n }\n if (trimmed.startsWith(\"data:\")) {\n return trimmed;\n }\n return null;\n}\n","/**\n * Return a whole-value HTTP(S) URL that is safe to expose as an attribute link.\n *\n * Attribute values may contain arbitrary user data, so only explicit web URLs\n * are accepted. Substrings in prose and schemes such as `javascript:` and\n * `file:` remain plain text.\n */\nexport function attributeLinkUrl(value: unknown): string | null {\n if (typeof value !== \"string\") return null;\n const trimmed = value.trim();\n if (!trimmed || /\\s/.test(trimmed)) return null;\n // Bidirectional controls can make an untrusted hostname appear to read as a\n // different domain even though the browser opens the original URL.\n if (/[\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069]/u.test(trimmed)) return null;\n if (!/^https?:\\/\\/[^/?#]/i.test(trimmed)) return null;\n\n try {\n const { protocol } = new URL(trimmed);\n return protocol === \"http:\" || protocol === \"https:\" ? trimmed : null;\n } catch {\n return null;\n }\n}\n","/**\n * Feature-property keys for inline photo images, shared by the app and the map\n * package so neither hardcodes a second copy that could drift. These keys are\n * part of the project schema: they are serialized into `.geolibre.json` on a\n * geotagged-photo or field-collection layer.\n */\n\n/** Property key under which a photo's downscaled thumbnail (data URL) is stored. */\nexport const PHOTO_PROPERTY = \"photo\";\n\n/**\n * Property key under which a geotagged photo's full-resolution image (a data URL\n * of the original, un-re-encoded bytes) is stored. {@link PHOTO_PROPERTY} holds\n * the small thumbnail shown on the map marker/popup; this holds the\n * native-resolution original used by the enlarged/fullscreen viewer and a \"Save\n * image\". Absent when the source is already at or below the thumbnail cap, or a\n * format a browser cannot display at native size (TIFF/HEIC).\n */\nexport const PHOTO_FULL_PROPERTY = \"photo_full\";\n","/**\n * Color-ramp definitions and classification-break helpers shared by the\n * vector graduated-styling panel and the raster symbology panel. Kept in\n * `@geolibre/core` (dependency-light, map-agnostic) so both UIs derive class\n * colors and breaks from the same source. The ramp `value`s intentionally\n * match the named colormaps shipped by `@developmentseed/deck.gl-raster`\n * (`COLORMAP_INDEX` keys) so a ramp choice maps 1:1 onto the raster render\n * path.\n */\n\nexport type ColorRamp = {\n value: string;\n label: string;\n colors: readonly string[];\n};\n\n/** The built-in color ramps offered by the style panels. */\nexport const VECTOR_COLOR_RAMPS: readonly ColorRamp[] = [\n {\n value: \"viridis\",\n label: \"Viridis\",\n colors: [\"#440154\", \"#31688e\", \"#35b779\", \"#fde725\"],\n },\n {\n value: \"plasma\",\n label: \"Plasma\",\n colors: [\"#0d0887\", \"#9c179e\", \"#ed7953\", \"#f0f921\"],\n },\n {\n value: \"inferno\",\n label: \"Inferno\",\n colors: [\"#000004\", \"#781c6d\", \"#ed6925\", \"#fcffa4\"],\n },\n {\n value: \"magma\",\n label: \"Magma\",\n colors: [\"#000004\", \"#721f81\", \"#f1605d\", \"#fcfdbf\"],\n },\n {\n value: \"cividis\",\n label: \"Cividis\",\n colors: [\"#00204d\", \"#575d6d\", \"#a59c74\", \"#ffea46\"],\n },\n {\n value: \"turbo\",\n label: \"Turbo\",\n colors: [\"#30123b\", \"#4777ef\", \"#1ccfd0\", \"#b9e642\", \"#fb8022\", \"#7a0403\"],\n },\n {\n value: \"spectral\",\n label: \"Spectral\",\n colors: [\"#9e0142\", \"#f46d43\", \"#ffffbf\", \"#66c2a5\", \"#5e4fa2\"],\n },\n {\n value: \"blues\",\n label: \"Blues\",\n colors: [\"#eff6ff\", \"#93c5fd\", \"#2563eb\", \"#1e3a8a\"],\n },\n {\n value: \"greens\",\n label: \"Greens\",\n colors: [\"#f0fdf4\", \"#86efac\", \"#16a34a\", \"#14532d\"],\n },\n {\n value: \"oranges\",\n label: \"Oranges\",\n colors: [\"#fff7ed\", \"#fdba74\", \"#f97316\", \"#7c2d12\"],\n },\n {\n value: \"reds\",\n label: \"Reds\",\n colors: [\"#fff5f0\", \"#fcae91\", \"#fb6a4a\", \"#cb181d\", \"#67000d\"],\n },\n {\n value: \"purples\",\n label: \"Purples\",\n colors: [\"#fcfbfd\", \"#bcbddc\", \"#807dba\", \"#54278f\", \"#3f007d\"],\n },\n {\n value: \"terrain\",\n label: \"Terrain\",\n colors: [\"#333399\", \"#21bcb3\", \"#79d05a\", \"#e8e85a\", \"#a87b54\", \"#ffffff\"],\n },\n {\n value: \"rdylgn\",\n label: \"Red-Yellow-Green\",\n colors: [\"#a50026\", \"#f46d43\", \"#ffffbf\", \"#66bd63\", \"#006837\"],\n },\n {\n value: \"rdylbu\",\n label: \"Red-Yellow-Blue\",\n colors: [\"#a50026\", \"#f46d43\", \"#ffffbf\", \"#74add1\", \"#313695\"],\n },\n {\n value: \"rdbu\",\n label: \"Red-Blue\",\n colors: [\"#b2182b\", \"#ef8a62\", \"#f7f7f7\", \"#67a9cf\", \"#2166ac\"],\n },\n {\n value: \"coolwarm\",\n label: \"Cool-Warm\",\n colors: [\"#3b4cc0\", \"#7b9ff9\", \"#dddcdc\", \"#f49a7b\", \"#b40426\"],\n },\n {\n value: \"jet\",\n label: \"Jet\",\n colors: [\"#000080\", \"#0000ff\", \"#00ffff\", \"#ffff00\", \"#ff0000\", \"#800000\"],\n },\n {\n value: \"greys\",\n label: \"Greys\",\n colors: [\"#ffffff\", \"#bdbdbd\", \"#636363\", \"#000000\"],\n },\n {\n value: \"gray\",\n label: \"Grayscale\",\n colors: [\"#000000\", \"#ffffff\"],\n },\n] as const;\n\n/**\n * Resolves a ramp definition by name, falling back to the first ramp when the\n * name is unknown.\n *\n * @param value - The ramp `value` (e.g. \"viridis\").\n * @returns The matching ramp, or the first ramp.\n */\nexport function getVectorColorRamp(value: string): ColorRamp {\n return VECTOR_COLOR_RAMPS.find((colorRamp) => colorRamp.value === value) ?? VECTOR_COLOR_RAMPS[0];\n}\n\n/**\n * Samples a ramp into `count` evenly spaced hex colors by linearly\n * interpolating between the ramp's anchor colors.\n *\n * @param colorRamp - The ramp `value`.\n * @param count - Number of colors to produce.\n * @returns An array of `count` hex colors (a single end color when count <= 1).\n */\nexport function interpolateRampColors(colorRamp: string, count: number): string[] {\n return interpolateColors(getVectorColorRamp(colorRamp).colors, count);\n}\n\n/**\n * Samples an explicit list of anchor colors into `count` evenly spaced hex\n * colors by linear interpolation. Backs both the named ramps and user-defined\n * custom ramps, so a list of hex codes is sampled identically to a built-in.\n *\n * @param colors - The anchor colors (at least one).\n * @param count - Number of colors to produce.\n * @returns An array of `count` hex colors (a single end color when count <= 1).\n */\nexport function interpolateColors(colors: readonly string[], count: number): string[] {\n const anchors = colors.length > 0 ? colors : [\"#000000\"];\n if (count <= 0) return [];\n if (count === 1) return [anchors[anchors.length - 1]];\n if (anchors.length === 1) {\n return Array.from({ length: count }, () => anchors[0]);\n }\n return Array.from({ length: count }, (_, index) => {\n const scaled = (index / (count - 1)) * (anchors.length - 1);\n const lowerIndex = Math.floor(scaled);\n const upperIndex = Math.min(anchors.length - 1, Math.ceil(scaled));\n const ratio = scaled - lowerIndex;\n return interpolateHexColor(anchors[lowerIndex], anchors[upperIndex], ratio);\n });\n}\n\n/**\n * Normalizes a single user-entered color token into a canonical `#rrggbb`\n * lowercase hex string, or null when it is not a valid 3- or 6-digit hex\n * color. Accepts values with or without a leading `#` and expands shorthand\n * (`#abc` → `#aabbcc`).\n *\n * @param token - A raw color token (e.g. \"FF0000\", \"#f00\", \" #AaBbCc \").\n * @returns The canonical `#rrggbb` color, or null when invalid.\n */\nexport function normalizeHexColor(token: string): string | null {\n let value = token.trim().toLowerCase();\n if (value === \"\") return null;\n if (!value.startsWith(\"#\")) value = `#${value}`;\n if (/^#[0-9a-f]{3}$/.test(value)) {\n value = `#${value\n .slice(1)\n .split(\"\")\n .map((channel) => channel + channel)\n .join(\"\")}`;\n }\n return /^#[0-9a-f]{6}$/.test(value) ? value : null;\n}\n\n/**\n * Parses a free-text list of hex color codes into canonical `#rrggbb` colors,\n * for the custom color-ramp inputs. Tokens may be separated by commas,\n * semicolons, or any whitespace (including newlines); invalid tokens are\n * dropped so a partial paste still yields the colors it can.\n *\n * @param input - The raw text the user entered.\n * @returns The valid, normalized colors in input order.\n */\nexport function parseHexColorList(input: string): string[] {\n return input\n .split(/[\\s,;]+/)\n .map(normalizeHexColor)\n .filter((color): color is string => color !== null);\n}\n\n/**\n * Linearly interpolates between two hex colors.\n *\n * @param from - Start hex color (e.g. \"#440154\").\n * @param to - End hex color.\n * @param ratio - Blend factor in [0, 1].\n * @returns The interpolated hex color.\n */\nexport function interpolateHexColor(from: string, to: string, ratio: number): string {\n const start = parseHexColor(from);\n const end = parseHexColor(to);\n return rgbToHex({\n r: Math.round(start.r + (end.r - start.r) * ratio),\n g: Math.round(start.g + (end.g - start.g) * ratio),\n b: Math.round(start.b + (end.b - start.b) * ratio),\n });\n}\n\n/**\n * Parses a `#rrggbb` hex color into its RGB channels.\n *\n * @param value - A `#rrggbb` hex color.\n * @returns The red, green, and blue channels (0-255).\n */\nexport function parseHexColor(value: string): { b: number; g: number; r: number } {\n // Caller must pass a well-formed \"#rrggbb\" string; malformed input parses to\n // NaN and the bitwise ops below coerce it to black ({ r: 0, g: 0, b: 0 }).\n const numeric = Number.parseInt(value.slice(1), 16);\n return {\n r: (numeric >> 16) & 255,\n g: (numeric >> 8) & 255,\n b: numeric & 255,\n };\n}\n\n/**\n * Formats RGB channels as a `#rrggbb` hex color.\n *\n * @param color - The red, green, and blue channels (0-255).\n * @returns A `#rrggbb` hex color.\n */\nexport function rgbToHex(color: { b: number; g: number; r: number }): string {\n return `#${[color.r, color.g, color.b]\n .map((channel) => channel.toString(16).padStart(2, \"0\"))\n .join(\"\")}`;\n}\n\n/**\n * Builds `count` evenly spaced break values across [min, max].\n *\n * @param min - The minimum value.\n * @param max - The maximum value.\n * @param count - Number of breaks to produce.\n * @returns The break values (min..max inclusive when count > 1).\n */\nexport function createEqualIntervalBreaks(min: number, max: number, count: number): number[] {\n return Array.from({ length: count }, (_, index) => {\n const ratio = count === 1 ? 0 : index / (count - 1);\n return min + (max - min) * ratio;\n });\n}\n\n/**\n * Builds `count` quantile break values from a sample of numeric values.\n *\n * These are ramp *edges* spanning the whole sample (the first break is the\n * minimum, the last the maximum), matching {@link createEqualIntervalBreaks}.\n * Graduated vector classes want class *lower bounds* instead; use\n * {@link createGraduatedClassBreaks}.\n *\n * @param values - The sample values.\n * @param count - Number of breaks to produce.\n * @returns The quantile break values.\n */\nexport function createQuantileBreaks(values: number[], count: number): number[] {\n if (count <= 0) return [];\n const sorted = [...values].sort((a, b) => a - b);\n // An empty sample would read `undefined` through the index math below and\n // yield NaN breaks; callers that have no values get an empty result instead.\n if (sorted.length === 0) return [];\n return Array.from({ length: count }, (_, index) => {\n const position = count === 1 ? 0 : (index / (count - 1)) * (sorted.length - 1);\n return quantileAtPosition(sorted, position);\n });\n}\n\n/** Linearly interpolated order statistic at a fractional index into `sorted`. */\nfunction quantileAtPosition(sorted: number[], position: number): number {\n const lowerIndex = Math.floor(position);\n const upperIndex = Math.min(sorted.length - 1, Math.ceil(position));\n const ratio = position - lowerIndex;\n return sorted[lowerIndex] + (sorted[upperIndex] - sorted[lowerIndex]) * ratio;\n}\n\n/** The classification schemes the graduated vector renderer offers. */\nexport type GraduatedClassificationScheme =\n | \"equal-interval\"\n | \"quantile\"\n | \"natural-breaks\"\n | \"standard-deviation\"\n | \"geometric-interval\";\n\n/**\n * Builds the class *lower bounds* for a graduated vector renderer.\n *\n * A graduated renderer's stops are lower bounds, not ramp edges: with N stops\n * sorted ascending, class `i` covers `[stops[i], stops[i + 1])` and the last\n * class is open-ended above. The legend (`≥ value`) and the QML/SLD exporters\n * already read them that way, so the breaks must follow the same convention:\n * `count` classes yield `count` breaks whose first entry is the sample minimum\n * and whose last entry opens the top class. Producing edges instead (ending at\n * the maximum) leaves the top class holding only the single largest feature and\n * puts the interior breaks at the wrong percentiles.\n *\n * The result is strictly ascending: duplicate breaks (a skewed sample can push\n * several quantiles onto the same value) are collapsed, so the caller may get\n * fewer than `count` breaks and must size its colors off `breaks.length`.\n * MapLibre rejects a `step` expression whose inputs are not strictly ascending,\n * so this de-duplication is load-bearing, not cosmetic.\n *\n * Standard deviation and geometric interval derive their breaks from the data,\n * so `count` is a ceiling there: a standard-deviation break that falls outside\n * the sample range is dropped rather than producing an empty class. See\n * {@link standardDeviationClassBreaks} and {@link geometricIntervalClassBreaks}\n * for how each treats its edge cases.\n *\n * @param values - The finite numeric values of the classified property.\n * @param count - Number of classes requested.\n * @param scheme - The classification scheme.\n * @returns Up to `count` strictly ascending class lower bounds.\n */\nexport function createGraduatedClassBreaks(\n values: number[],\n count: number,\n scheme: GraduatedClassificationScheme,\n): number[] {\n if (count <= 0 || values.length === 0) return [];\n if (scheme === \"natural-breaks\") return ascendingUnique(naturalClassBreaks(values, count));\n const sorted = [...values].sort((a, b) => a - b);\n if (scheme === \"quantile\") {\n return ascendingUnique(\n Array.from({ length: count }, (_, index) =>\n quantileAtPosition(sorted, (index / count) * (sorted.length - 1)),\n ),\n );\n }\n if (scheme === \"standard-deviation\") {\n return ascendingUnique(standardDeviationClassBreaks(sorted, count));\n }\n if (scheme === \"geometric-interval\") {\n const breaks = geometricIntervalClassBreaks(sorted, count);\n if (breaks) return ascendingUnique(breaks);\n }\n return ascendingUnique(equalIntervalClassBreaks(sorted, count));\n}\n\n/** Equal-interval lower bounds over the range of an ascending sample. */\nfunction equalIntervalClassBreaks(sorted: number[], count: number): number[] {\n const min = sorted[0];\n const max = sorted[sorted.length - 1];\n return Array.from({ length: count }, (_, index) => min + ((max - min) * index) / count);\n}\n\n/**\n * Standard-deviation lower bounds: classes one standard deviation wide,\n * centered on the mean. An even `count` puts a break on the mean (classes\n * `[mean - σ, mean)` and `[mean, mean + σ)` meet there); an odd `count` makes\n * the middle class straddle it (`[mean - σ/2, mean + σ/2)`). The first break is\n * the sample minimum, and interior breaks outside `(min, max]` are dropped, so\n * the open-ended bottom and top classes absorb the tails and no class is empty.\n * A constant sample (σ = 0) yields the minimum alone.\n *\n * Uses the population standard deviation, as QGIS and ArcGIS do.\n */\nfunction standardDeviationClassBreaks(sorted: number[], count: number): number[] {\n const min = sorted[0];\n const max = sorted[sorted.length - 1];\n const mean = sorted.reduce((sum, value) => sum + value, 0) / sorted.length;\n const variance =\n sorted.reduce((sum, value) => sum + (value - mean) * (value - mean), 0) / sorted.length;\n const deviation = Math.sqrt(variance);\n if (!(deviation > 0)) return [min];\n const interior: number[] = [];\n for (let index = 1; index < count; index += 1) {\n const value = mean + (index - count / 2) * deviation;\n if (value > min && value <= max) interior.push(value);\n }\n return [min, ...interior];\n}\n\n/**\n * Geometric-interval lower bounds: class edges growing by a constant ratio\n * from the smallest positive value to the maximum, so each class spans the\n * same multiple of its lower bound. Suits skewed data spanning orders of\n * magnitude (population, income, concentrations).\n *\n * A geometric series cannot start at or below zero, so it starts at the\n * smallest positive value and the first break stays the sample minimum: zero\n * and negative values all fall into the first class. When no value is\n * positive, the result is `null` and the caller falls back to equal interval.\n */\nfunction geometricIntervalClassBreaks(sorted: number[], count: number): number[] | null {\n const max = sorted[sorted.length - 1];\n const lowest = sorted.find((value) => value > 0);\n if (lowest === undefined) return null;\n // Step in log space: `max / lowest` overflows to Infinity for samples\n // spanning more than ~308 orders of magnitude, while the logs stay finite.\n const logLowest = Math.log(lowest);\n const logStep = (Math.log(max) - logLowest) / count;\n return [\n sorted[0],\n ...Array.from({ length: count - 1 }, (_, index) => Math.exp(logLowest + logStep * (index + 1))),\n ];\n}\n\n/** Drops non-finite breaks and collapses repeats, keeping ascending order. */\nfunction ascendingUnique(breaks: number[]): number[] {\n const out: number[] = [];\n for (const value of breaks) {\n if (!Number.isFinite(value)) continue;\n if (out.length > 0 && value <= out[out.length - 1]) continue;\n out.push(value);\n }\n return out;\n}\n\n/** Cap on the Jenks input size; the DP below is roughly O(n^2 * k). */\nconst MAX_NATURAL_BREAK_SAMPLES = 1000;\n\n/** Evenly thins a sorted array down to at most `maxSamples` entries. */\nfunction downsampleSortedValues(values: number[], maxSamples: number): number[] {\n if (values.length <= maxSamples) return values;\n const result: number[] = [];\n const step = (values.length - 1) / (maxSamples - 1);\n for (let index = 0; index < maxSamples; index += 1) {\n result.push(values[Math.round(index * step)]);\n }\n return result;\n}\n\n/**\n * Jenks natural breaks: the class lower bounds that minimize the summed\n * within-class variance. Returns the sample minimum first, then the first value\n * of each subsequent class.\n */\nfunction naturalClassBreaks(values: number[], count: number): number[] {\n const unique = Array.from(new Set(values)).sort((a, b) => a - b);\n // Cap the input so a large layer does not freeze the Style panel's UI thread.\n const sorted = downsampleSortedValues(unique, MAX_NATURAL_BREAK_SAMPLES);\n if (sorted.length <= count) return sorted;\n\n const lowerClassLimits = Array.from({ length: sorted.length + 1 }, () =>\n Array(count + 1).fill(0),\n );\n const varianceCombinations = Array.from({ length: sorted.length + 1 }, () =>\n Array(count + 1).fill(Number.POSITIVE_INFINITY),\n );\n\n for (let classIndex = 1; classIndex <= count; classIndex += 1) {\n lowerClassLimits[1][classIndex] = 1;\n varianceCombinations[1][classIndex] = 0;\n }\n\n for (let valueIndex = 2; valueIndex <= sorted.length; valueIndex += 1) {\n let sum = 0;\n let sumSquares = 0;\n let weight = 0;\n\n for (let lowerIndex = 1; lowerIndex <= valueIndex; lowerIndex += 1) {\n const currentIndex = valueIndex - lowerIndex + 1;\n const value = sorted[currentIndex - 1];\n weight += 1;\n sum += value;\n sumSquares += value * value;\n const variance = sumSquares - (sum * sum) / weight;\n const previousIndex = currentIndex - 1;\n if (previousIndex === 0) continue;\n\n for (let classIndex = 2; classIndex <= count; classIndex += 1) {\n const candidate = variance + varianceCombinations[previousIndex][classIndex - 1];\n if (varianceCombinations[valueIndex][classIndex] >= candidate) {\n lowerClassLimits[valueIndex][classIndex] = currentIndex;\n varianceCombinations[valueIndex][classIndex] = candidate;\n }\n }\n }\n\n lowerClassLimits[valueIndex][1] = 1;\n varianceCombinations[valueIndex][1] = sumSquares - (sum * sum) / Math.max(1, weight);\n }\n\n // Walk the DP table back from the top class: `lowerClassLimits[i][k]` is the\n // 1-based index in `sorted` where class `k` starts, which is exactly that\n // class's lower bound. Class 1 always starts at the sample minimum.\n const breaks = Array(count).fill(sorted[0]) as number[];\n let valueIndex = sorted.length;\n for (let classIndex = count; classIndex >= 2; classIndex -= 1) {\n const lowerClassLimit = Math.max(1, lowerClassLimits[valueIndex][classIndex]);\n breaks[classIndex - 1] = sorted[lowerClassLimit - 1];\n valueIndex = lowerClassLimit - 1;\n }\n breaks[0] = sorted[0];\n return breaks;\n}\n","// Matches a `..` directory-traversal segment (bounded by separators or the\n// string ends), without rejecting a `..` inside a filename like `v1..2.gpkg`.\nconst PATH_TRAVERSAL = /(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/;\n\n/**\n * Whether a path contains a `..` directory-traversal segment. Used to reject a\n * (possibly hand-edited) project's `sourcePath` before re-reading it off disk.\n *\n * @param path - The path to check.\n * @returns True when the path contains a traversal segment.\n */\nexport function hasPathTraversal(path: string): boolean {\n return PATH_TRAVERSAL.test(path);\n}\n\n/**\n * Whether a path is an absolute local filesystem path a host could read.\n *\n * Matches the raw path (not a trimmed copy): a whitespace-padded value would\n * pass a trimmed check but reach the file read unchanged and fail there, so it\n * is rejected up front. Accepts POSIX paths and Windows drive-letter paths only.\n * UNC paths (`\\\\server\\share`) are deliberately rejected: reading one can make\n * Windows auto-authenticate against a remote host (NTLM hash capture), and a\n * remote share is not a supported local data source.\n *\n * Lives in core so validation of a persisted path is available wherever a\n * record is normalized — the desktop app's `isAbsoluteLocalPath` re-exports the\n * same rule for its Tauri call sites.\n *\n * @param path - The path to check.\n * @returns True when the path is an absolute POSIX or Windows path.\n */\nexport function isAbsoluteFilesystemPath(path: string): boolean {\n return path.startsWith(\"/\") || /^[a-z]:[\\\\/]/i.test(path);\n}\n","import type { Feature, FeatureCollection, LineString, MultiPolygon, Polygon } from \"geojson\";\nimport { decodePolyline } from \"./polyline\";\nimport { getRuntimeEnvironment } from \"./runtime-env\";\n\n/**\n * Routing / network-analysis client and pure helpers backing the Processing →\n * Network tools (isochrones / service areas, OD cost matrices).\n *\n * Provider: Valhalla. OSRM (the Directions backend) has no isochrone endpoint,\n * so isochrones require Valhalla, which also serves the OD matrix\n * (`/sources_to_targets`) — one consistent API. The default is the shared\n * public FOSSGIS server, overridable via runtime env (`VITE_ROUTING_ENDPOINT`)\n * so a self-hosted Valhalla can be used instead. The pure request builders and\n * response parsers carry no React/MapLibre dependency so they can be\n * unit-tested without a browser or network.\n */\n\n/** Default public Valhalla server (FOSSGIS). CORS-enabled; usage limits apply. */\nexport const DEFAULT_ROUTING_ENDPOINT = \"https://valhalla1.openstreetmap.de\";\n\n/** Valhalla costing models exposed in the UI. */\nexport type RoutingMode = \"auto\" | \"pedestrian\" | \"bicycle\";\n\n/** Isochrone contour metric: travel time (minutes) or distance (km). */\nexport type RoutingMetric = \"time\" | \"distance\";\n\nexport interface RoutingConfig {\n /** Base URL of the Valhalla server (no trailing slash). */\n endpoint: string;\n}\n\n/** A point with a stable id, used as an isochrone origin or matrix source/target. */\nexport interface RoutingPoint {\n id: string | number;\n lon: number;\n lat: number;\n}\n\n/**\n * Resolves the routing configuration from runtime env, defaulting to the public\n * Valhalla server. `VITE_ROUTING_ENDPOINT` overrides the endpoint.\n *\n * @returns The resolved routing configuration.\n */\nexport function getRoutingConfig(): RoutingConfig {\n const env = getRuntimeEnvironment();\n return {\n endpoint: stripTrailingSlash(env.VITE_ROUTING_ENDPOINT?.trim()) || DEFAULT_ROUTING_ENDPOINT,\n };\n}\n\nfunction stripTrailingSlash(value: string | undefined): string | undefined {\n return value ? value.replace(/\\/+$/, \"\") : value;\n}\n\n/**\n * Parses a comma/space-separated contour string (\"5, 10, 15\") into a sorted,\n * de-duplicated list of positive numbers.\n *\n * @param value - The raw contour string.\n * @returns Ascending unique positive contour values.\n */\nexport function parseContours(value: string): number[] {\n const seen = new Set<number>();\n for (const token of value.split(/[\\s,]+/)) {\n if (!token) continue;\n const n = Number(token);\n if (Number.isFinite(n) && n > 0) seen.add(n);\n }\n return [...seen].sort((a, b) => a - b);\n}\n\nexport interface IsochroneRequestBody {\n locations: { lon: number; lat: number }[];\n costing: RoutingMode;\n contours: ({ time: number } | { distance: number })[];\n polygons: true;\n}\n\n/**\n * Builds a Valhalla `/isochrone` request body for a single origin.\n *\n * @param point - The origin `[lon, lat]`.\n * @param opts - Travel mode, metric, and contour values (minutes for time, km for distance).\n * @returns The request body.\n */\nexport function buildIsochroneRequest(\n point: [number, number],\n opts: { mode: RoutingMode; metric: RoutingMetric; contours: number[] },\n): IsochroneRequestBody {\n const [lon, lat] = point;\n return {\n locations: [{ lon, lat }],\n costing: opts.mode,\n contours: opts.contours.map((value) =>\n opts.metric === \"time\" ? { time: value } : { distance: value },\n ),\n polygons: true,\n };\n}\n\ntype IsochroneFeatureProps = {\n source_id: string | number;\n mode: RoutingMode;\n metric: RoutingMetric;\n contour: number | null;\n};\n\n/**\n * Converts a Valhalla isochrone GeoJSON response into polygon features tagged\n * with their origin, mode, metric, and contour value. Non-polygon features\n * (Valhalla can also emit contour LineStrings) are dropped.\n *\n * @param response - The Valhalla `/isochrone` GeoJSON FeatureCollection.\n * @param ctx - Origin id, travel mode, and metric to tag onto each polygon.\n * @returns The tagged polygon features.\n */\nexport function isochroneResponseToFeatures(\n response: unknown,\n ctx: { sourceId: string | number; mode: RoutingMode; metric: RoutingMetric },\n): Feature<Polygon | MultiPolygon, IsochroneFeatureProps>[] {\n const features = (response as FeatureCollection | null)?.features;\n if (!Array.isArray(features)) return [];\n const out: Feature<Polygon | MultiPolygon, IsochroneFeatureProps>[] = [];\n for (const feature of features) {\n const type = feature?.geometry?.type;\n if (type !== \"Polygon\" && type !== \"MultiPolygon\") continue;\n const contour = (feature.properties as { contour?: unknown } | null)?.contour;\n out.push({\n type: \"Feature\",\n geometry: feature.geometry as Polygon | MultiPolygon,\n properties: {\n source_id: ctx.sourceId,\n mode: ctx.mode,\n metric: ctx.metric,\n contour: typeof contour === \"number\" ? contour : null,\n },\n });\n }\n return out;\n}\n\nexport interface MatrixRequestBody {\n sources: { lon: number; lat: number }[];\n targets: { lon: number; lat: number }[];\n costing: RoutingMode;\n}\n\n/**\n * Builds a Valhalla `/sources_to_targets` request body.\n *\n * @param origins - The source points.\n * @param targets - The target points.\n * @param mode - The travel mode.\n * @returns The request body.\n */\nexport function buildMatrixRequest(\n origins: RoutingPoint[],\n targets: RoutingPoint[],\n mode: RoutingMode,\n): MatrixRequestBody {\n return {\n sources: origins.map((p) => ({ lon: p.lon, lat: p.lat })),\n targets: targets.map((p) => ({ lon: p.lon, lat: p.lat })),\n costing: mode,\n };\n}\n\ntype MatrixCell = {\n from_index: number;\n to_index: number;\n /** Travel time in seconds, or null when unreachable. */\n time: number | null;\n /** Travel distance in km, or null when unreachable. */\n distance: number | null;\n};\n\ntype MatrixFeatureProps = {\n origin_id: string | number;\n dest_id: string | number;\n time_s: number;\n distance_km: number;\n mode: RoutingMode;\n};\n\n/**\n * Converts a Valhalla `/sources_to_targets` response into one LineString per\n * reachable origin→destination pair, carrying the travel time and distance.\n * Unreachable pairs (null time) are dropped.\n *\n * @param response - The Valhalla matrix response.\n * @param origins - The origin points (indexed by `from_index`).\n * @param targets - The target points (indexed by `to_index`).\n * @param ctx - The travel mode to tag onto each pair.\n * @returns The OD-pair LineString features.\n */\nexport function matrixResponseToFeatures(\n response: unknown,\n origins: RoutingPoint[],\n targets: RoutingPoint[],\n ctx: { mode: RoutingMode },\n): Feature<LineString, MatrixFeatureProps>[] {\n const rows = (response as { sources_to_targets?: MatrixCell[][] } | null)?.sources_to_targets;\n if (!Array.isArray(rows)) return [];\n const out: Feature<LineString, MatrixFeatureProps>[] = [];\n for (const row of rows) {\n if (!Array.isArray(row)) continue;\n for (const cell of row) {\n if (cell?.time == null || cell.distance == null) continue;\n const origin = origins[cell.from_index];\n const target = targets[cell.to_index];\n if (!origin || !target) continue;\n out.push({\n type: \"Feature\",\n geometry: {\n type: \"LineString\",\n coordinates: [\n [origin.lon, origin.lat],\n [target.lon, target.lat],\n ],\n },\n properties: {\n origin_id: origin.id,\n dest_id: target.id,\n time_s: cell.time,\n distance_km: cell.distance,\n mode: ctx.mode,\n },\n });\n }\n }\n return out;\n}\n\nexport interface RouteRequestBody {\n locations: { lon: number; lat: number }[];\n costing: RoutingMode;\n directions_options: { units: \"kilometers\" };\n}\n\n/**\n * Builds a Valhalla `/route` request body that visits the points in the given\n * order. Valhalla snaps each location to the nearest routable edge, so points\n * that are off the road network (cell sites, sensors) still route cleanly.\n *\n * @param points - The waypoints in visiting order (at least two).\n * @param mode - The travel mode (costing model).\n * @returns The request body.\n */\nexport function buildRouteRequest(points: RoutingPoint[], mode: RoutingMode): RouteRequestBody {\n return {\n locations: points.map((p) => ({ lon: p.lon, lat: p.lat })),\n costing: mode,\n directions_options: { units: \"kilometers\" },\n };\n}\n\ntype RouteLeg = {\n shape?: string;\n summary?: { time?: number; length?: number };\n};\n\ntype RouteFeatureProps = {\n leg_index: number;\n from_id: string | number;\n to_id: string | number;\n time_s: number;\n distance_km: number;\n mode: RoutingMode;\n};\n\n/**\n * Converts a Valhalla `/route` response into one LineString per leg (the road\n * path between two consecutive waypoints), carrying the leg's travel time and\n * distance. Legs without a decodable shape are dropped.\n *\n * @param response - The Valhalla `/route` response.\n * @param points - The waypoints passed to the request, indexing the legs.\n * @param ctx - The travel mode to tag onto each leg.\n * @returns One LineString feature per routed leg.\n */\nexport function routeResponseToFeatures(\n response: unknown,\n points: RoutingPoint[],\n ctx: { mode: RoutingMode },\n): Feature<LineString, RouteFeatureProps>[] {\n const legs = (response as { trip?: { legs?: RouteLeg[] } } | null)?.trip?.legs;\n if (!Array.isArray(legs)) return [];\n const out: Feature<LineString, RouteFeatureProps>[] = [];\n legs.forEach((leg, index) => {\n if (typeof leg?.shape !== \"string\") return;\n const coordinates = decodePolyline(leg.shape, 6);\n if (coordinates.length < 2) return;\n const from = points[index];\n const to = points[index + 1];\n out.push({\n type: \"Feature\",\n geometry: { type: \"LineString\", coordinates },\n properties: {\n leg_index: index,\n from_id: from?.id ?? index,\n to_id: to?.id ?? index + 1,\n time_s: typeof leg.summary?.time === \"number\" ? leg.summary.time : 0,\n distance_km: typeof leg.summary?.length === \"number\" ? leg.summary.length : 0,\n mode: ctx.mode,\n },\n });\n });\n return out;\n}\n\n/**\n * Comparator for ordering route waypoints by a chosen attribute value. Numeric\n * strings and timestamps (anything `Date.parse` understands, e.g. ISO 8601)\n * sort chronologically/numerically; values that parse this way sort before\n * free-form text, which falls back to a locale string comparison. Empty/missing\n * values sort last so unlabeled points trail the ordered ones.\n *\n * Numeric parsing is tried before date parsing, so a single column is expected\n * to use one convention: a column mixing plain integers with ISO timestamps\n * would compare them on the same numeric axis (epoch millis dwarf small\n * integers), which is not a meaningful ordering. Real sequence/time columns use\n * one convention, so this is acceptable in practice.\n *\n * @param a - The first value.\n * @param b - The second value.\n * @returns A negative, zero, or positive number for ascending order.\n */\nexport function compareSequenceValues(a: unknown, b: unknown): number {\n const aMissing = isMissingValue(a);\n const bMissing = isMissingValue(b);\n if (aMissing && bMissing) return 0;\n if (aMissing) return 1;\n if (bMissing) return -1;\n const an = toComparableNumber(a);\n const bn = toComparableNumber(b);\n if (an !== null && bn !== null) return an - bn;\n if (an !== null) return -1;\n if (bn !== null) return 1;\n return String(a).localeCompare(String(b));\n}\n\nfunction isMissingValue(value: unknown): boolean {\n return value == null || (typeof value === \"string\" && value.trim() === \"\");\n}\n\nfunction toComparableNumber(value: unknown): number | null {\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (trimmed === \"\") return null;\n // Only treat plain decimal/scientific notation as numeric, so hex (\"0x1A\")\n // and other Number()-coercible forms sort as text rather than by their\n // surprising numeric value.\n if (/^[+-]?\\d+(\\.\\d+)?([eE][+-]?\\d+)?$/.test(trimmed)) {\n return Number(trimmed);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isFinite(parsed)) return parsed;\n }\n return null;\n}\n\n/**\n * Error thrown when a Valhalla request returns a non-2xx response. Carries the\n * HTTP status so callers can branch on it (e.g. a 4xx rejection) without\n * pattern-matching the message string.\n */\nexport class RoutingRequestError extends Error {\n readonly status: number;\n\n constructor(status: number, statusText: string) {\n super(`Routing request failed (${status} ${statusText})`);\n this.name = \"RoutingRequestError\";\n this.status = status;\n }\n}\n\nasync function postJson(url: string, body: unknown, signal?: AbortSignal): Promise<unknown> {\n const response = await fetch(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n signal,\n });\n if (!response.ok) {\n throw new RoutingRequestError(response.status, response.statusText);\n }\n return response.json();\n}\n\n/**\n * Requests an isochrone from the Valhalla server.\n *\n * @param endpoint - The Valhalla base URL.\n * @param body - The request body from {@link buildIsochroneRequest}.\n * @param signal - Optional abort signal.\n * @returns The Valhalla GeoJSON response.\n */\nexport function requestIsochrone(\n endpoint: string,\n body: IsochroneRequestBody,\n signal?: AbortSignal,\n): Promise<unknown> {\n return postJson(`${stripTrailingSlash(endpoint)}/isochrone`, body, signal);\n}\n\n/**\n * Requests an OD cost matrix from the Valhalla server.\n *\n * @param endpoint - The Valhalla base URL.\n * @param body - The request body from {@link buildMatrixRequest}.\n * @param signal - Optional abort signal.\n * @returns The Valhalla matrix response.\n */\nexport function requestMatrix(\n endpoint: string,\n body: MatrixRequestBody,\n signal?: AbortSignal,\n): Promise<unknown> {\n return postJson(`${stripTrailingSlash(endpoint)}/sources_to_targets`, body, signal);\n}\n\n/**\n * Requests a route through ordered waypoints from the Valhalla server.\n *\n * @param endpoint - The Valhalla base URL.\n * @param body - The request body from {@link buildRouteRequest}.\n * @param signal - Optional abort signal.\n * @returns The Valhalla `/route` response.\n */\nexport function requestRoute(\n endpoint: string,\n body: RouteRequestBody,\n signal?: AbortSignal,\n): Promise<unknown> {\n return postJson(`${stripTrailingSlash(endpoint)}/route`, body, signal);\n}\n\n/**\n * Requests a turn-by-turn route from the Valhalla server. Unlike\n * {@link requestRoute}, the body is passed through as is, so the caller can ask\n * for Valhalla's OSRM-compatible output with voice and banner instructions,\n * alternates, a start heading, and costing options (the navigation tool).\n *\n * @param endpoint - The Valhalla base URL.\n * @param body - The `/route` request body.\n * @param signal - Optional abort signal.\n * @returns The Valhalla `/route` response.\n */\nexport function requestNavigationRoute(\n endpoint: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n): Promise<unknown> {\n return postJson(`${stripTrailingSlash(endpoint)}/route`, body, signal);\n}\n","import { DEFAULT_LAYER_STYLE, styleValue, type LayerStyle, type VectorRule } from \"./types\";\nimport { getVectorColorRamp } from \"./color-ramp\";\nimport { getActiveSemiMajorAxisMeters } from \"./ellipsoids\";\nimport { removeTrailingJsonCommas } from \"./expressions\";\n\n/**\n * A data-driven color value for a vector paint property: either a plain CSS\n * color string, or a MapLibre expression array (e.g. a categorized `match` or\n * graduated `interpolate`). Typed maplibre-agnostically so `@geolibre/core`\n * stays free of a maplibre-gl dependency; consumers cast to the concrete\n * `PropertyValueSpecification<string>` where the MapLibre types are in scope.\n */\nexport type VectorColorValue = string | unknown[];\n\n/** Whether a color value is a data-driven expression rather than a flat color. */\nexport function isVectorColorExpression(value: VectorColorValue): value is unknown[] {\n return Array.isArray(value);\n}\n\n/**\n * Whether a string is a 6-digit hex color (`#rrggbb`), the form the vector\n * renderer treats as valid. Exported so the symbology exporters can apply the\n * same validity check the live `match`/`case` expressions use rather than\n * re-deriving the regex.\n */\nexport function isHexColor(value: unknown): boolean {\n // Guard the type: `stop.color`/`rule.color` are typed `string`, but the data\n // can come from a hand-edited or imported .geolibre.json, so a missing/null\n // value must return false rather than throw on `.trim()`.\n return typeof value === \"string\" && /^#[0-9a-f]{6}$/i.test(value.trim());\n}\n\n/** A 3- or 6-digit hex color, as emitted by the simplestyle spec. */\nfunction isSimpleStyleColor(value: string): boolean {\n return /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(value.trim());\n}\n\n/**\n * simplestyle-spec color and numeric property names. Color keys carry CSS hex\n * colors; numeric keys carry plain numbers. See\n * https://github.com/mapbox/simplestyle-spec.\n */\nconst SIMPLE_STYLE_COLOR_KEYS = [\"fill\", \"stroke\", \"marker-color\"] as const;\nconst SIMPLE_STYLE_NUMBER_KEYS = [\n \"fill-opacity\",\n \"stroke-width\",\n \"stroke-opacity\",\n // Non-standard: alpha from a KML IconStyle color, wired into circle-opacity.\n \"marker-opacity\",\n] as const;\n\nfunction isSimpleStyleEnabled(style: LayerStyle): boolean {\n return styleValue(style, \"simpleStyleEnabled\") === true;\n}\n\n/**\n * Wrap a resolved color value so a per-feature simplestyle property takes\n * precedence when {@link LayerStyle.simpleStyleEnabled} is set. Returns the base\n * value unchanged when the feature lacks the property or the mode is off.\n *\n * @param style - The layer style.\n * @param property - The simplestyle property name (e.g. `fill`, `stroke`).\n * @param base - The flat color or expression to fall back to.\n * @returns A `coalesce` expression, or the base value when disabled.\n */\nfunction withSimpleStyleColor(\n style: LayerStyle,\n property: (typeof SIMPLE_STYLE_COLOR_KEYS)[number],\n base: VectorColorValue,\n): VectorColorValue {\n if (!isSimpleStyleEnabled(style)) return base;\n // A zoom-stepped base (per-rule scale ranges) must keep its step outermost:\n // MapLibre only allows [\"zoom\"] as the input of a top-level step/interpolate,\n // so the per-feature coalesce is applied inside each step output instead of\n // around the whole expression.\n return mapZoomStepOutputs(base, (value) => [\n \"coalesce\",\n [\"get\", property],\n value,\n ]) as VectorColorValue;\n}\n\n/**\n * Apply a per-feature transform to a value, descending into the outputs of a\n * top-level `[\"step\", [\"zoom\"], …]` expression (as produced by the rule-based\n * per-rule zoom compilation) so the zoom step stays outermost. MapLibre\n * rejects any expression where `[\"zoom\"]` is not the input of a top-level\n * `step`/`interpolate`, so wrappers (simplestyle coalesce, opacity folds)\n * must be pushed inside the step's outputs rather than wrapped around it.\n * Non-step values are transformed directly.\n */\nexport function mapZoomStepOutputs(\n value: unknown,\n transform: (output: unknown) => unknown,\n): unknown {\n if (\n Array.isArray(value) &&\n value[0] === \"step\" &&\n Array.isArray(value[1]) &&\n value[1][0] === \"zoom\"\n ) {\n const out: unknown[] = [\"step\", value[1], transform(value[2])];\n for (let index = 3; index < value.length; index += 2) {\n out.push(value[index], transform(value[index + 1]));\n }\n return out;\n }\n return transform(value);\n}\n\n/**\n * Resolve a numeric paint value, letting a per-feature simplestyle property\n * override the layer value when {@link LayerStyle.simpleStyleEnabled} is set.\n *\n * The lookup is wrapped in a `coalesce` rather than leaning on `to-number`'s\n * own fallback argument, because that fallback only fires when a value fails\n * to convert — and a *missing* property does not fail, it converts to 0. Since\n * simplestyle is enabled for the whole layer as soon as any feature carries any\n * one of these keys, features that carry only some of them (an ArcGIS KML\n * export writes `fill`/`fill-opacity` but no `marker-opacity`, for instance)\n * would otherwise be painted at opacity 0 or width 0 — present in the layer,\n * invisible on the map (#1552).\n *\n * @param style - The layer style.\n * @param property - The simplestyle property name (e.g. `stroke-width`).\n * @param base - The layer-level fallback value.\n * @returns A `to-number` expression, or `base` when disabled.\n */\nexport function simpleStyleNumberValue(\n style: LayerStyle,\n property: (typeof SIMPLE_STYLE_NUMBER_KEYS)[number],\n base: number,\n): number | unknown[] {\n if (!isSimpleStyleEnabled(style)) return base;\n return [\"to-number\", [\"coalesce\", [\"get\", property], base], base];\n}\n\n/**\n * Ground resolution (meters per pixel) at MapLibre zoom 0 on the equator, for\n * the Web Mercator projection: earth circumference (2*pi*6378137) over the\n * 512px world at zoom 0. Resolution halves with every zoom level. Exported so\n * the Mapbox-style importer can reverse {@link metersWidthExpression} without a\n * second copy of the magic number.\n */\nexport const MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0 = (2 * Math.PI * 6378137) / 512;\n\n/**\n * Same ground resolution as {@link MERCATOR_METERS_PER_PIXEL_AT_ZOOM_0} but for\n * the project's active body: its circumference over the 512px zoom-0 world. On\n * Earth this equals the constant; on the Moon/Mars it uses that body's radius so\n * a stroke width given in ground meters renders at the correct on-screen size.\n * Exported so the Mapbox-style importer can reverse {@link metersWidthExpression}\n * with the same ellipsoid-aware factor the exporter used (keeping the round trip\n * correct on non-Earth projects), rather than the Earth-only constant.\n */\nexport function mercatorMetersPerPixelAtZoom0(): number {\n return (2 * Math.PI * getActiveSemiMajorAxisMeters()) / 512;\n}\n\n// Largest zoom MapLibre renders; used as the upper interpolation stop.\nconst MAX_MERCATOR_ZOOM = 24;\n\n/**\n * Build a zoom-driven width expression that keeps a stroke proportional to the\n * map scale, so a width given in ground meters renders thicker when zoomed in\n * and thinner when zoomed out (QGIS \"map units\" behavior).\n *\n * In Web Mercator the pixels-per-meter ratio doubles with each zoom level, so\n * an `[\"exponential\", 2]` interpolation between two stops one zoom apart is\n * exact across the whole range. The conversion is referenced to the equator;\n * because Mercator stretches distances toward the poles, the on-screen width at\n * higher latitudes is correspondingly larger, matching how the underlying map\n * is itself stretched.\n *\n * Typed maplibre-agnostically (`unknown[]`); consumers cast to the concrete\n * `PropertyValueSpecification<number>` where the MapLibre types are in scope.\n *\n * @param meters - The stroke width in ground meters.\n * @returns A MapLibre `interpolate` expression array.\n */\nexport function metersWidthExpression(meters: number): unknown[] {\n const widthAtZoom0 = meters / mercatorMetersPerPixelAtZoom0();\n return [\n \"interpolate\",\n [\"exponential\", 2],\n [\"zoom\"],\n 0,\n widthAtZoom0,\n MAX_MERCATOR_ZOOM,\n widthAtZoom0 * 2 ** MAX_MERCATOR_ZOOM,\n ];\n}\n\n/**\n * Resolve the `line-width` paint value for a layer style, honoring the\n * {@link LayerStyle.strokeWidthUnit}:\n *\n * - `\"meters\"`: a zoom-driven {@link metersWidthExpression} from the flat\n * `strokeWidth`, so the stroke scales with the map. A per-feature pixel\n * `stroke-width` override no longer applies in this mode.\n * - `\"pixels\"` (default): the constant pixel width, still honoring any\n * per-feature simplestyle `stroke-width`.\n *\n * Shared by the map style-mapper and the geo-editor plugin so the Sketches\n * store layer and Geoman's interaction display layers render an identical\n * width.\n *\n * @param style - The layer style.\n * @returns A number (constant pixels) or a MapLibre expression array.\n */\nexport function lineWidthValue(style: LayerStyle): number | unknown[] {\n // Proportional (graduated) sizing takes precedence: width is driven by a\n // numeric field, reusing the circle-radius output range as the width range.\n const proportionalWidth = proportionalRadiusExpression(style);\n if (proportionalWidth) {\n // Per-rule widths still override the proportional base for matched\n // features (the property-driven interpolate nests legally inside a\n // case), mirroring circleRadiusValue.\n return vectorStrokeWidthValue(style, proportionalWidth);\n }\n if (styleValue(style, \"strokeWidthUnit\") === \"meters\") {\n // The meters width is itself a zoom-driven interpolation; embedding it\n // inside a per-rule case would nest [\"zoom\"] below the top level, which\n // MapLibre rejects. Like the per-feature simplestyle width, per-rule pixel\n // widths do not apply in meters mode.\n return metersWidthExpression(styleValue(style, \"strokeWidth\"));\n }\n // Per-rule symbol widths (rule-based mode) override the layer width for\n // features a rule matches; unmatched features keep the base value.\n return vectorStrokeWidthValue(\n style,\n simpleStyleNumberValue(style, \"stroke-width\", styleValue(style, \"strokeWidth\")),\n );\n}\n\n/**\n * Whether a FeatureCollection carries per-feature simplestyle-spec properties\n * worth honoring: at least one feature with a valid hex color in a color key\n * (`fill`/`stroke`/`marker-color`) or a finite number in a numeric key\n * (`fill-opacity`/`stroke-width`/`stroke-opacity`). The scan is capped so very\n * large collections do not pay a full pass.\n *\n * @param geojson - The collection to inspect (may be undefined).\n * @returns `true` when simplestyle rendering should be enabled for the layer.\n */\nexport function hasSimpleStyleProperties(\n geojson: { features?: { properties?: Record<string, unknown> | null }[] } | undefined,\n): boolean {\n const features = geojson?.features;\n if (!features?.length) return false;\n const limit = Math.min(features.length, 1000);\n for (let index = 0; index < limit; index += 1) {\n const properties = features[index]?.properties;\n if (!properties) continue;\n for (const key of SIMPLE_STYLE_COLOR_KEYS) {\n const value = properties[key];\n if (typeof value === \"string\" && isSimpleStyleColor(value)) return true;\n }\n for (const key of SIMPLE_STYLE_NUMBER_KEYS) {\n const value = properties[key];\n if (typeof value === \"number\" && Number.isFinite(value)) return true;\n }\n }\n return false;\n}\n\n/**\n * Parses a user-entered MapLibre expression string into an expression array,\n * tolerating trailing commas. Returns null when the text is empty or not a\n * JSON array.\n */\nexport function parseJsonExpression(expression: string): unknown[] | null {\n const trimmed = expression.trim();\n if (!trimmed) return null;\n\n try {\n const parsed = JSON.parse(removeTrailingJsonCommas(trimmed));\n if (!Array.isArray(parsed)) return null;\n return parsed;\n } catch {\n return null;\n }\n}\n\n/**\n * Builds the data-driven color value for a vector layer's current style mode.\n * `single` (or any mode that cannot produce a valid expression) returns the\n * flat fallback color; `categorized` returns a `match` expression, `graduated`\n * a `step` expression, and `expression` the parsed user expression.\n *\n * Graduated stops are class lower bounds (see\n * {@link createGraduatedClassBreaks}), so the classes are discrete: the map\n * paints exactly one color per class, which is what the legend (`≥ value`) and\n * the QML/SLD exporters already describe. Rendering them as a continuous\n * `interpolate` instead blends a shade per feature, so a 4-class layer draws in\n * many more than 4 colors and stops matching its own legend (#1384).\n *\n * @param style - The layer style.\n * @param fallbackColor - The flat color used for `single` mode and as the\n * expression fallback.\n * @returns A flat color string or a MapLibre color expression.\n */\nexport function vectorColorExpression(style: LayerStyle, fallbackColor: string): VectorColorValue {\n const mode = styleValue(style, \"vectorStyleMode\");\n if (mode === \"single\") return fallbackColor;\n\n if (mode === \"expression\") {\n return parseJsonExpression(styleValue(style, \"vectorStyleExpression\")) ?? fallbackColor;\n }\n\n if (mode === \"rule-based\") {\n return ruleBasedColorExpression(style, fallbackColor);\n }\n\n const property = styleValue(style, \"vectorStyleProperty\").trim();\n if (!property) return fallbackColor;\n\n if (mode === \"categorized\") {\n const stops = styleValue(style, \"vectorStyleStops\").filter(\n (stop) => String(stop.value).trim().length > 0 && isHexColor(stop.color),\n );\n if (stops.length === 0) return fallbackColor;\n\n return [\n \"match\",\n [\"to-string\", [\"get\", property]],\n ...stops.flatMap((stop) => [String(stop.value).trim(), stop.color]),\n fallbackColor,\n ];\n }\n\n const stops = graduatedStops(style);\n if (stops.length < 2) return fallbackColor;\n\n // `step` outputs the base color below the first break, then switches at each\n // subsequent break, so every feature lands in exactly one class and features\n // under the lowest break keep the first class's color (matching how the SLD\n // exporter writes its leading `< first` rule).\n return [\n \"step\",\n [\"to-number\", [\"get\", property], stops[0].value],\n stops[0].color,\n ...stops.slice(1).flatMap((stop) => [stop.value, stop.color]),\n ];\n}\n\n/**\n * The graduated stops of a style, cleaned for rendering: non-numeric values and\n * invalid colors dropped, sorted ascending, and de-duplicated. MapLibre rejects\n * a `step` expression whose inputs are not strictly ascending, and stops reach\n * here from hand-edited projects and style imports as well as the Style panel,\n * so repeats are dropped rather than trusted away.\n */\nfunction graduatedStops(style: LayerStyle): { color: string; value: number }[] {\n const sorted = styleValue(style, \"vectorStyleStops\")\n .map((stop) => ({\n color: stop.color,\n value: typeof stop.value === \"number\" ? stop.value : Number.parseFloat(stop.value),\n }))\n .filter((stop) => Number.isFinite(stop.value) && isHexColor(stop.color))\n .sort((a, b) => a.value - b.value);\n return sorted.filter((stop, index) => index === 0 || stop.value > sorted[index - 1].value);\n}\n\n/**\n * A rule-based renderer rule resolved for rendering: the rule tree walked down\n * to a drawable leaf, with the ancestors' filters ANDed in and zoom ranges\n * intersected. Only enabled leaves with a valid\n * color and filter appear; group rules (rules other rules name as\n * {@link VectorRule.parentId}) contribute constraints but are not themselves\n * emitted. Shared by the paint compilers, the legend, and the symbology\n * exporters so all of them agree on which rules actually draw.\n */\nexport interface EffectiveVectorRule {\n id: string;\n /** The leaf rule's label (for the legend / exported rule titles). */\n label: string;\n /** The symbol fill/circle color (valid 6-digit hex). */\n color: string;\n /** The parsed MapLibre filter, ANDed with every ancestor's filter. */\n filter: unknown[];\n /** Intersected lower zoom bound (inclusive), when any rule in the chain has one. */\n minZoom?: number;\n /** Intersected upper zoom bound (exclusive), when any rule in the chain has one. */\n maxZoom?: number;\n strokeColor?: string;\n strokeWidth?: number;\n fillOpacity?: number;\n circleRadius?: number;\n}\n\n/** A finite per-rule zoom bound, or undefined when unset/invalid. */\nfunction ruleZoom(value: number | undefined): number | undefined {\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Resolve the layer's rule tree into the drawable leaf rules (see\n * {@link EffectiveVectorRule}) plus the catch-all else rule. Disabled rules\n * (and every descendant of a disabled group), rules with an invalid color or\n * filter, subtrees under a group whose non-blank filter does not parse, and\n * rules whose intersected zoom range is empty are all dropped. Cycles in\n * `parentId` are treated as broken chains and dropped rather than looping.\n *\n * @param style - The layer style (reads {@link LayerStyle.vectorRules}).\n * @returns The ordered drawable rules and the enabled else rule, if any.\n */\nexport function effectiveVectorRules(style: LayerStyle): {\n rules: EffectiveVectorRule[];\n elseRule: VectorRule | null;\n} {\n const all = styleValue(style, \"vectorRules\");\n const elseRule = all.find((rule) => rule.isElse && rule.enabled !== false) ?? null;\n const byId = new Map<string, VectorRule>();\n for (const rule of all) {\n if (!rule.isElse) byId.set(rule.id, rule);\n }\n // A rule other rules point at is a group; only leaves draw. A rule naming\n // itself as its parent is treated as top-level (mirroring the editor and\n // the QML exporter), not as its own group.\n const groupIds = new Set<string>();\n for (const rule of byId.values()) {\n if (rule.parentId && rule.parentId !== rule.id && byId.has(rule.parentId)) {\n groupIds.add(rule.parentId);\n }\n }\n\n const rules: EffectiveVectorRule[] = [];\n for (const rule of all) {\n if (rule.isElse || groupIds.has(rule.id)) continue;\n if (rule.enabled === false || !isHexColor(rule.color)) continue;\n const own = parseJsonExpression(rule.filter);\n // A MapLibre filter is an expression that must start with a string operator;\n // skip non-operator arrays (e.g. a bare value) so the compiled `case` never\n // carries a non-boolean condition that MapLibre would reject at runtime.\n if (!own || typeof own[0] !== \"string\") continue;\n\n const filters: unknown[][] = [own];\n let minZoom = ruleZoom(rule.minZoom);\n let maxZoom = ruleZoom(rule.maxZoom);\n let dropped = false;\n const seen = new Set([rule.id]);\n // A self-referencing parentId means \"no parent\" (a normal top-level\n // rule), matching the editor's tree walk and the QML exporter.\n let parent = rule.parentId && rule.parentId !== rule.id ? byId.get(rule.parentId) : undefined;\n while (parent) {\n if (seen.has(parent.id) || parent.enabled === false) {\n dropped = true;\n break;\n }\n seen.add(parent.id);\n if (parent.filter.trim()) {\n const parentFilter = parseJsonExpression(parent.filter);\n // A group whose filter is written but unreadable can match nothing;\n // dropping the subtree mirrors how an invalid leaf filter is skipped.\n if (!parentFilter || typeof parentFilter[0] !== \"string\") {\n dropped = true;\n break;\n }\n filters.unshift(parentFilter);\n }\n const parentMin = ruleZoom(parent.minZoom);\n const parentMax = ruleZoom(parent.maxZoom);\n if (parentMin !== undefined) {\n minZoom = minZoom === undefined ? parentMin : Math.max(minZoom, parentMin);\n }\n if (parentMax !== undefined) {\n maxZoom = maxZoom === undefined ? parentMax : Math.min(maxZoom, parentMax);\n }\n // An ancestor with a self-referencing parentId is a top-level rule (the\n // same convention as the initial parent resolution above); advancing to\n // itself would trip the cycle guard and wrongly drop the whole subtree.\n parent =\n parent.parentId && parent.parentId !== parent.id ? byId.get(parent.parentId) : undefined;\n }\n if (dropped) continue;\n if (minZoom !== undefined && maxZoom !== undefined && minZoom >= maxZoom) {\n continue;\n }\n\n rules.push({\n id: rule.id,\n label: rule.label,\n color: rule.color,\n filter: filters.length === 1 ? filters[0] : [\"all\", ...filters],\n ...(minZoom !== undefined ? { minZoom } : {}),\n ...(maxZoom !== undefined ? { maxZoom } : {}),\n ...(isHexColor(rule.strokeColor) ? { strokeColor: rule.strokeColor } : {}),\n ...(typeof rule.strokeWidth === \"number\" && Number.isFinite(rule.strokeWidth)\n ? { strokeWidth: rule.strokeWidth }\n : {}),\n ...(typeof rule.fillOpacity === \"number\" && Number.isFinite(rule.fillOpacity)\n ? { fillOpacity: rule.fillOpacity }\n : {}),\n ...(typeof rule.circleRadius === \"number\" && Number.isFinite(rule.circleRadius)\n ? { circleRadius: rule.circleRadius }\n : {}),\n });\n }\n return { rules, elseRule };\n}\n\n/**\n * A rule's match condition as a MapLibre filter: its (ancestor-combined)\n * attribute filter, with any scale range folded in as `[\"zoom\"]` comparisons.\n * The zoom window is half-open (`min <= zoom < max`), matching how\n * {@link zoomWrappedRuleValue} activates rules per zoom segment. MapLibre only\n * re-evaluates `[\"zoom\"]` in filters at integer zoom levels, so a scale-ranged\n * rule's features appear/disappear on whole zooms — the paint expressions keep\n * the fractional-zoom precision.\n */\nfunction ruleMatchFilter(rule: EffectiveVectorRule): unknown[] {\n const conditions: unknown[] = [rule.filter];\n if (rule.minZoom !== undefined) {\n conditions.push([\">=\", [\"zoom\"], rule.minZoom]);\n }\n if (rule.maxZoom !== undefined) {\n conditions.push([\"<\", [\"zoom\"], rule.maxZoom]);\n }\n return conditions.length === 1 ? rule.filter : [\"all\", ...conditions];\n}\n\n// The layer sync recomputes the visibility filter for every render sub-layer\n// (fill, line, point, labels, …) on every sync tick, and compiling it re-walks\n// and re-parses the whole rule tree. The store replaces the vectorRules array\n// on every edit, so the array's identity is a correct cache key; memoizing on\n// it makes the repeated per-sub-layer calls O(1), mirroring the WeakMap caches\n// in the map package (e.g. the text-marker scan).\nconst visibilityFilterCache = new WeakMap<VectorRule[], unknown[] | null>();\n\n/**\n * The MapLibre filter that hides features matching no rule, for a rule-based\n * layer whose catch-all else rule has been switched off. Returns `null` — and\n * callers then leave their geometry filters untouched — unless the layer is in\n * `\"rule-based\"` mode AND carries an else record explicitly disabled\n * (`enabled: false`). An absent else record keeps the historical fallback\n * rendering, so projects saved before the toggle existed are unaffected.\n *\n * With the else rule off, the returned `[\"any\", filter1, filter2, …]` keeps\n * only features matched by a drawable rule (see {@link effectiveVectorRules});\n * with no drawable rules it returns the vacuous `[\"any\"]`, which matches\n * nothing, so every feature hides. Sub-layers combine it with their geometry\n * filters, which also drops unmatched features from labels and hit-testing —\n * mirroring QGIS, where features matching no rule are simply not drawn.\n *\n * @param style - The layer style (reads {@link LayerStyle.vectorRules}).\n * @returns The filter to `[\"all\", …]`-combine into each sub-layer, or `null`.\n */\nexport function ruleBasedVisibilityFilter(style: LayerStyle): unknown[] | null {\n if (styleValue(style, \"vectorStyleMode\") !== \"rule-based\") return null;\n const all = styleValue(style, \"vectorRules\");\n const cached = visibilityFilterCache.get(all);\n if (cached !== undefined) return cached;\n const elseRecord = all.find((rule) => rule.isElse);\n const filter =\n !elseRecord || elseRecord.enabled !== false\n ? null\n : [\"any\", ...effectiveVectorRules(style).rules.map(ruleMatchFilter)];\n visibilityFilterCache.set(all, filter);\n return filter;\n}\n\n/**\n * Wrap a per-zoom-segment value builder in a `[\"step\", [\"zoom\"], …]` expression\n * when any rule carries a zoom bound. MapLibre only allows `[\"zoom\"]` as the\n * top-level input of a `step`/`interpolate` in paint properties (not inside a\n * `case` condition), so per-rule scale ranges compile by splitting the zoom\n * axis at every rule bound and rebuilding the `case` expression per segment\n * with only the rules active in that segment. With no zoom bounds the builder\n * runs once and its plain value is returned unchanged, keeping the historical\n * `case` shape (which the Mapbox-style importer recognizes).\n */\nfunction zoomWrappedRuleValue(\n rules: EffectiveVectorRule[],\n build: (active: EffectiveVectorRule[]) => unknown,\n): unknown {\n const bounds = new Set<number>();\n for (const rule of rules) {\n if (rule.minZoom !== undefined) bounds.add(rule.minZoom);\n if (rule.maxZoom !== undefined) bounds.add(rule.maxZoom);\n }\n if (bounds.size === 0) return build(rules);\n const breaks = [...bounds].sort((a, b) => a - b);\n const activeAt = (segmentStart: number) =>\n rules.filter(\n (rule) =>\n (rule.minZoom ?? -Infinity) <= segmentStart && segmentStart < (rule.maxZoom ?? Infinity),\n );\n const expression: unknown[] = [\"step\", [\"zoom\"], build(activeAt(-Infinity))];\n for (const zoom of breaks) {\n expression.push(zoom, build(activeAt(zoom)));\n }\n return expression;\n}\n\n/**\n * Compiles the `\"rule-based\"` renderer's ordered rules into a MapLibre `case`\n * color expression: `[\"case\", filter1, color1, filter2, color2, …, elseColor]`.\n * Rules are evaluated top to bottom; the first matching filter wins. Disabled\n * rules and rules with an invalid filter JSON or a non-hex color are skipped;\n * nested rules AND their ancestors' filters (see {@link effectiveVectorRules}).\n * When any rule carries a zoom range the `case` is rebuilt per zoom segment\n * inside a `[\"step\", [\"zoom\"], …]` wrapper. The catch-all (`isElse`) rule\n * supplies the trailing fallback; when absent or invalid the layer\n * `fallbackColor` is used. With no usable rules the flat fallback color is\n * returned.\n *\n * @param style - The layer style (reads {@link LayerStyle.vectorRules}).\n * @param fallbackColor - The color used when no else rule defines one.\n * @returns A MapLibre expression, or a flat color when no rule applies.\n */\nexport function ruleBasedColorExpression(\n style: LayerStyle,\n fallbackColor: string,\n): VectorColorValue {\n const { rules, elseRule } = effectiveVectorRules(style);\n const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : fallbackColor;\n if (rules.length === 0) return elseColor;\n return zoomWrappedRuleValue(rules, (active) => {\n if (active.length === 0) return elseColor;\n return [\"case\", ...active.flatMap((rule) => [rule.filter, rule.color]), elseColor];\n }) as VectorColorValue;\n}\n\n/** The per-rule override fields a paint channel can read. */\ntype VectorRuleOverrideKey = \"strokeColor\" | \"strokeWidth\" | \"fillOpacity\" | \"circleRadius\";\n\n/**\n * Build a paint value honoring per-rule symbol overrides for one field: a\n * `case` expression (zoom-wrapped when rules carry scale ranges) where each\n * drawable rule's branch yields its override, or the layer fallback when that\n * rule does not override the field. Returns the fallback unchanged when the\n * layer is not in rule-based mode or no rule (including the else rule)\n * overrides the field, so simple styles keep their flat paint values.\n *\n * First-match semantics are preserved: every drawable rule contributes a\n * branch (even without an override) so a feature matched by an earlier rule\n * never picks up a later rule's override.\n */\nfunction ruleOverrideValue(\n style: LayerStyle,\n key: VectorRuleOverrideKey,\n fallback: unknown,\n): unknown {\n if (styleValue(style, \"vectorStyleMode\") !== \"rule-based\") return fallback;\n const { rules, elseRule } = effectiveVectorRules(style);\n const elseOverride = readRuleOverride(elseRule ?? undefined, key);\n if (elseOverride === undefined && !rules.some((rule) => rule[key] !== undefined)) {\n return fallback;\n }\n const elseValue = elseOverride ?? fallback;\n return zoomWrappedRuleValue(rules, (active) => {\n if (active.length === 0) return elseValue;\n return [\"case\", ...active.flatMap((rule) => [rule.filter, rule[key] ?? fallback]), elseValue];\n });\n}\n\n/** A validated override read straight off a (possibly else) {@link VectorRule}. */\nfunction readRuleOverride(\n rule: VectorRule | undefined,\n key: VectorRuleOverrideKey,\n): string | number | undefined {\n const value = rule?.[key];\n if (key === \"strokeColor\") {\n return isHexColor(value) ? (value as string) : undefined;\n }\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * The stroke/outline color for polygon outlines and circle strokes, honoring\n * per-rule {@link VectorRule.strokeColor} overrides in rule-based mode.\n * Falls back to the flat layer stroke color.\n */\nexport function vectorOutlineColorValue(style: LayerStyle): VectorColorValue {\n return ruleOverrideValue(\n style,\n \"strokeColor\",\n styleValue(style, \"strokeColor\"),\n ) as VectorColorValue;\n}\n\n/**\n * A stroke-width paint value honoring per-rule {@link VectorRule.strokeWidth}\n * overrides in rule-based mode; `fallback` is the layer-level width (a number\n * or an already-built expression such as the meters-based interpolation).\n */\nexport function vectorStrokeWidthValue(\n style: LayerStyle,\n fallback: number | unknown[],\n): number | unknown[] {\n return ruleOverrideValue(style, \"strokeWidth\", fallback) as number | unknown[];\n}\n\n/**\n * A fill/circle opacity paint value honoring per-rule\n * {@link VectorRule.fillOpacity} overrides in rule-based mode; `fallback` is\n * the layer-level opacity value (number or expression).\n */\nexport function vectorFillOpacityValue(\n style: LayerStyle,\n fallback: number | unknown[],\n): number | unknown[] {\n return ruleOverrideValue(style, \"fillOpacity\", fallback) as number | unknown[];\n}\n\n/**\n * The color-ramp colors a density heatmap paints with.\n *\n * {@link getVectorColorRamp} falls back to the *first* ramp (\"viridis\") for an\n * unknown name, which is not the heatmap default (\"turbo\"). Both the map paint\n * (`heatmapPaint` in `@geolibre/map`) and the auto-legend gradient resolve the\n * ramp here so a stale or hand-edited `heatmapColorRamp` cannot make the legend\n * and the rendered heatmap disagree about which ramp is in use.\n *\n * @param style - The layer style.\n * @returns The ramp's colors, or the default ramp's colors for an unknown name.\n */\nexport function heatmapRampColors(style: LayerStyle): readonly string[] {\n const value = styleValue(style, \"heatmapColorRamp\");\n const ramp = getVectorColorRamp(value);\n return (ramp.value === value ? ramp : getVectorColorRamp(DEFAULT_LAYER_STYLE.heatmapColorRamp))\n .colors;\n}\n\n/** The validated proportional (graduated) size configuration of a layer. */\nexport interface ProportionalSizeRange {\n property: string;\n minValue: number;\n maxValue: number;\n minRadius: number;\n maxRadius: number;\n}\n\n/**\n * The proportional-size configuration, or `null` when proportional sizing\n * does not apply: disabled, no field chosen, non-finite or degenerate\n * (`maxValue <= minValue`) value range, or non-finite radii. This is the\n * single validation chain for every proportional consumer — circle radius,\n * line width, and marker icon-size (`@geolibre/map`) — so their notion of\n * \"active\" can never drift apart.\n *\n * @param style - The layer style.\n * @returns The validated range, or `null` when proportional sizing is off.\n */\nexport function proportionalSizeRange(style: LayerStyle): ProportionalSizeRange | null {\n if (!styleValue(style, \"proportionalSizeEnabled\")) return null;\n return validatedSizeRange(\n styleValue(style, \"proportionalSizeProperty\"),\n styleValue(style, \"proportionalSizeMinValue\"),\n styleValue(style, \"proportionalSizeMaxValue\"),\n styleValue(style, \"proportionalSizeMinRadius\"),\n styleValue(style, \"proportionalSizeMaxRadius\"),\n );\n}\n\n/**\n * The validation every field-driven size range shares: a chosen field, a\n * finite non-degenerate value span, and finite radii. Kept in one place so a\n * future tightening (rejecting negative radii, say) cannot land on the\n * layer-wide proportional renderer without also landing on the geometry\n * generator's centroids.\n */\nfunction validatedSizeRange(\n rawProperty: string,\n minValue: number,\n maxValue: number,\n minRadius: number,\n maxRadius: number,\n): ProportionalSizeRange | null {\n const property = rawProperty.trim();\n if (!property) return null;\n if (!(Number.isFinite(minValue) && Number.isFinite(maxValue))) return null;\n if (maxValue <= minValue) return null;\n if (!(Number.isFinite(minRadius) && Number.isFinite(maxRadius))) return null;\n return { property, minValue, maxValue, minRadius, maxRadius };\n}\n\n/**\n * The proportional `interpolate` expression mapping\n * `proportionalSizeMinValue..proportionalSizeMaxValue` onto\n * `proportionalSizeMinRadius..proportionalSizeMaxRadius`, or `null` when\n * proportional sizing does not apply (see {@link proportionalSizeRange}).\n *\n * @param style - The layer style.\n * @returns The `interpolate` expression, or `null`.\n */\nexport function proportionalRadiusExpression(style: LayerStyle): unknown[] | null {\n const range = proportionalSizeRange(style);\n return range ? radiusInterpolateExpression(range) : null;\n}\n\n/**\n * The `interpolate` that maps a validated range's value span onto its radius\n * span. Shared by the layer-wide proportional renderer and the geometry\n * generator's centroid sizing so both scale identically (linear, with the\n * value clamped to the range's ends by `interpolate`'s own stop behavior).\n */\nfunction radiusInterpolateExpression(range: ProportionalSizeRange): unknown[] {\n return [\n \"interpolate\",\n [\"linear\"],\n [\"to-number\", [\"get\", range.property], range.minValue],\n range.minValue,\n Math.max(0, range.minRadius),\n range.maxValue,\n Math.max(0, range.maxRadius),\n ];\n}\n\n/**\n * The geometry generator's centroid-size configuration, or `null` when the\n * generated points should all take the flat\n * {@link LayerStyle.geometryGeneratorCircleRadius}: no field chosen, non-finite\n * or degenerate (`maxValue <= minValue`) value range, or non-finite radii.\n * Mirrors {@link proportionalSizeRange} against the generator's own keys.\n *\n * @param style - The layer style.\n * @returns The validated range, or `null` when field-driven sizing is off.\n */\nexport function generatorSizeRange(style: LayerStyle): ProportionalSizeRange | null {\n return validatedSizeRange(\n styleValue(style, \"geometryGeneratorSizeProperty\"),\n styleValue(style, \"geometryGeneratorSizeMinValue\"),\n styleValue(style, \"geometryGeneratorSizeMaxValue\"),\n styleValue(style, \"geometryGeneratorSizeMinRadius\"),\n styleValue(style, \"geometryGeneratorSizeMaxRadius\"),\n );\n}\n\n/**\n * Builds the `circle-radius` paint value for the geometry generator's derived\n * centroid points: an `interpolate` over\n * {@link LayerStyle.geometryGeneratorSizeProperty} when field-driven sizing\n * applies (see {@link generatorSizeRange}), otherwise the flat\n * {@link LayerStyle.geometryGeneratorCircleRadius}.\n *\n * The flat fallback keeps the 1px floor the generator has always applied; the\n * interpolate is left as authored, so a 0px min radius still hides the\n * smallest symbols on purpose.\n *\n * @param style - The layer style.\n * @returns A constant radius (pixels) or a MapLibre `interpolate` expression.\n */\nexport function generatorCircleRadiusValue(style: LayerStyle): number | unknown[] {\n const range = generatorSizeRange(style);\n if (range) return radiusInterpolateExpression(range);\n return Math.max(1, styleValue(style, \"geometryGeneratorCircleRadius\"));\n}\n\n/**\n * Builds the `circle-radius` paint value, honoring proportional (graduated)\n * symbol sizing. When {@link LayerStyle.proportionalSizeEnabled} is set with a\n * chosen numeric field and a valid value range, returns an `interpolate` that\n * maps `proportionalSizeMinValue..proportionalSizeMaxValue` onto\n * `proportionalSizeMinRadius..proportionalSizeMaxRadius`; otherwise the constant\n * {@link LayerStyle.circleRadius}.\n *\n * @param style - The layer style.\n * @returns A constant radius (pixels) or a MapLibre `interpolate` expression.\n */\nexport function circleRadiusValue(style: LayerStyle): number | unknown[] {\n // Per-rule symbol sizes (rule-based mode) override the base radius for\n // features a rule matches; unmatched features keep the base value.\n return ruleOverrideValue(style, \"circleRadius\", baseCircleRadiusValue(style)) as\n | number\n | unknown[];\n}\n\n/** The layer-level circle radius before per-rule overrides. */\nfunction baseCircleRadiusValue(style: LayerStyle): number | unknown[] {\n return proportionalRadiusExpression(style) ?? styleValue(style, \"circleRadius\");\n}\n\n/** Fill color value for a polygon layer (fallback: the layer fill color). */\nexport function vectorFillColorValue(style: LayerStyle): VectorColorValue {\n return withSimpleStyleColor(\n style,\n \"fill\",\n vectorColorExpression(style, styleValue(style, \"fillColor\")),\n );\n}\n\n/**\n * Circle color value for a point layer. Intentionally identical to\n * `vectorFillColorValue`: GeoLibre has no separate point-fill color, so point\n * circles share the polygon fill color (matching `circlePaint` in the map\n * package). Kept as its own function so the per-geometry callers read in\n * parallel and a future dedicated circle color stays a one-line change here.\n */\nexport function vectorCircleColorValue(style: LayerStyle): VectorColorValue {\n return withSimpleStyleColor(\n style,\n \"marker-color\",\n vectorColorExpression(style, styleValue(style, \"fillColor\")),\n );\n}\n\n/**\n * Line color value for line geometry and polygon outlines (fallback: the\n * layer stroke color). For non-`expression` modes the data-driven color is\n * applied to line geometry only, while polygon outlines keep the flat stroke\n * color, matching the polygon-fill-only behavior of categorized/graduated\n * styling.\n */\nexport function vectorLineColorValue(style: LayerStyle): VectorColorValue {\n const strokeColor = styleValue(style, \"strokeColor\");\n if (styleValue(style, \"vectorStyleMode\") === \"rule-based\") {\n return withSimpleStyleColor(style, \"stroke\", ruleBasedLineColorExpression(style));\n }\n const vectorColor = vectorColorExpression(style, strokeColor);\n const resolved =\n vectorColor === strokeColor\n ? strokeColor\n : styleValue(style, \"vectorStyleMode\") === \"expression\"\n ? vectorColor\n : [\"case\", [\"==\", [\"geometry-type\"], \"Polygon\"], strokeColor, vectorColor];\n return withSimpleStyleColor(style, \"stroke\", resolved);\n}\n\n/**\n * The line color for the rule-based renderer: line geometry takes the rule\n * color, polygon outlines take the per-rule stroke color override (falling\n * back to the flat layer stroke). Both channels are built inside one shared\n * zoom wrapper, since composing two independently zoom-stepped expressions in\n * a single `case` would nest `[\"zoom\"]` below the top level, which MapLibre\n * rejects.\n */\nfunction ruleBasedLineColorExpression(style: LayerStyle): VectorColorValue {\n const strokeColor = styleValue(style, \"strokeColor\");\n const { rules, elseRule } = effectiveVectorRules(style);\n const elseColor = elseRule && isHexColor(elseRule.color) ? elseRule.color : strokeColor;\n const elseOutline =\n (readRuleOverride(elseRule ?? undefined, \"strokeColor\") as string | undefined) ?? strokeColor;\n const hasOutlineOverride =\n rules.some((rule) => rule.strokeColor !== undefined) || elseOutline !== strokeColor;\n if (rules.length === 0 && elseColor === strokeColor && !hasOutlineOverride) {\n return strokeColor;\n }\n return zoomWrappedRuleValue(rules, (active) => {\n const colorValue: unknown =\n active.length === 0\n ? elseColor\n : [\"case\", ...active.flatMap((rule) => [rule.filter, rule.color]), elseColor];\n const outlineValue: unknown = !hasOutlineOverride\n ? strokeColor\n : active.length === 0\n ? elseOutline\n : [\n \"case\",\n ...active.flatMap((rule) => [rule.filter, rule.strokeColor ?? strokeColor]),\n elseOutline,\n ];\n if (colorValue === strokeColor && outlineValue === strokeColor) {\n return strokeColor;\n }\n return [\"case\", [\"==\", [\"geometry-type\"], \"Polygon\"], outlineValue, colorValue];\n }) as VectorColorValue;\n}\n\n/**\n * Resolves the 3D-extrusion height for a layer style into a MapLibre value: a\n * plain meters number, or a data-driven expression. In advanced mode a valid\n * `extrusionHeightExpression` wins; otherwise the height is the chosen property\n * scaled by `extrusionHeightScale` (`[\"*\", [\"to-number\", [\"get\", prop], 0],\n * scale]`), or a flat `0` when no property is set (so the layer renders flat\n * rather than erroring). Shared by the map's fill-extrusion paint and the\n * Add Vector Layer control's extrusion mapping so both extrude identically.\n *\n * @param style - The layer style.\n * @returns The extrusion height as a number or a MapLibre expression array.\n */\nexport function extrusionHeightValue(style: LayerStyle): number | unknown[] {\n const advancedExpression = parseJsonExpression(\n styleValue(style, \"extrusionAdvancedStyleEnabled\")\n ? styleValue(style, \"extrusionHeightExpression\")\n : \"\",\n );\n if (advancedExpression) return advancedExpression;\n const property = styleValue(style, \"extrusionHeightProperty\").trim();\n if (!property) return 0;\n const scale = styleValue(style, \"extrusionHeightScale\");\n return [\"*\", [\"to-number\", [\"get\", property], 0], scale];\n}\n\n/**\n * Resolves the 3D-extrusion color for a layer style: a data-driven expression\n * when the layer's symbology mode produces one (categorized/graduated/rule/\n * expression) or an advanced `extrusionColorExpression` is set, otherwise the\n * flat `extrusionColor`. Mirrors the fill-color contract so an extruded layer\n * honors the same attribute-driven styling.\n *\n * @param style - The layer style.\n * @returns A flat color string or a MapLibre color expression array.\n */\nexport function extrusionColorValue(style: LayerStyle): VectorColorValue {\n const flat = styleValue(style, \"extrusionColor\");\n const vectorExpression = vectorColorExpression(style, flat);\n if (vectorExpression !== flat) return vectorExpression;\n const advancedExpression = parseJsonExpression(\n styleValue(style, \"extrusionAdvancedStyleEnabled\")\n ? styleValue(style, \"extrusionColorExpression\")\n : \"\",\n );\n return (advancedExpression as VectorColorValue | null) ?? flat;\n}\n","import {\n Color,\n featureFilter,\n latest,\n normalizePropertyExpression,\n} from \"@maplibre/maplibre-gl-style-spec\";\n\n/** The geometry kinds a MapLibre style expression tells apart (`[\"geometry-type\"]`). */\nexport type StyleGeometryKind = \"Point\" | \"LineString\" | \"Polygon\";\n\n/** A feature as a style layer's expressions read it. */\nexport interface StyleFeatureInput {\n id?: string | number;\n properties: Record<string, unknown> | null | undefined;\n kind: StyleGeometryKind;\n}\n\n/** A style layer's filter and property values, evaluated as MapLibre would. */\nexport interface StyleLayerEvaluator {\n /** Whether the layer's `filter` keeps the feature at `zoom`. */\n passes: (zoom: number, feature: StyleFeatureInput) => boolean;\n /**\n * A paint or layout property's value for a feature at `zoom`: the layer's\n * value (a literal, a legacy function or an expression) or the spec's\n * default. A color comes back as `[r, g, b, a]` with channels 0-255 and\n * alpha 0-1; formatted text as its plain string. `undefined` for a property\n * the spec does not know or an expression that does not compile.\n */\n value: (\n group: \"paint\" | \"layout\",\n name: string,\n zoom: number,\n feature: StyleFeatureInput,\n ) => unknown;\n}\n\ninterface StyleLayerInput {\n id?: string;\n type: string;\n filter?: unknown;\n paint?: Record<string, unknown>;\n layout?: Record<string, unknown>;\n}\n\nconst GEOMETRY_TYPE: Record<StyleGeometryKind, number> = { Point: 1, LineString: 2, Polygon: 3 };\n\ntype Evaluate = (globals: { zoom: number }, feature: unknown) => unknown;\n\n/**\n * Compile a MapLibre style layer's filter and properties for evaluation\n * outside MapLibre, for a renderer that draws the layer itself (the ArcGIS\n * renderer drawing a plugin's own GeoJSON overlay, say). Uses the style spec's\n * own evaluator, so data-driven and zoom expressions, legacy functions and\n * defaults behave as they do on MapLibre. Each property compiles once.\n *\n * @param layer - The style layer (its `type`, `filter`, `paint`, `layout`).\n * @returns The layer's evaluator.\n */\nexport function createStyleLayerEvaluator(layer: StyleLayerInput): StyleLayerEvaluator {\n const root = `layers[${layer.id ?? \"\"}]`;\n let filter: ((zoom: number, feature: unknown) => boolean) | null;\n try {\n const compiled = featureFilter(layer.filter as never, `${root}.filter`);\n filter = (zoom, feature) => compiled.filter({ zoom } as never, feature as never);\n } catch {\n // MapLibre refuses the layer outright; draw nothing rather than everything.\n filter = () => false;\n }\n const compiled = new Map<string, Evaluate | null>();\n const compile = (group: \"paint\" | \"layout\", name: string): Evaluate | null => {\n const key = `${group}/${name}`;\n if (compiled.has(key)) return compiled.get(key)!;\n const spec = (latest as unknown as Record<string, Record<string, unknown>>)[\n `${group}_${layer.type}`\n ]?.[name] as { default?: unknown } | undefined;\n let evaluate: Evaluate | null = null;\n if (spec) {\n const raw = layer[group]?.[name];\n try {\n const expression = normalizePropertyExpression(\n (raw ?? spec.default) as never,\n `${root}.${group}.${name}`,\n spec as never,\n );\n evaluate = (globals, feature) => expression.evaluate(globals as never, feature as never);\n } catch {\n evaluate = null;\n }\n }\n compiled.set(key, evaluate);\n return evaluate;\n };\n const input = (feature: StyleFeatureInput) => ({\n type: GEOMETRY_TYPE[feature.kind],\n id: feature.id,\n properties: feature.properties ?? {},\n });\n return {\n passes: (zoom, feature) => {\n try {\n return filter(zoom, input(feature));\n } catch {\n return false;\n }\n },\n value: (group, name, zoom, feature) => {\n const evaluate = compile(group, name);\n if (!evaluate) return undefined;\n let result: unknown;\n try {\n result = evaluate({ zoom }, input(feature));\n } catch {\n return undefined;\n }\n if (result instanceof Color) {\n const [r, g, b, a] = result.rgb;\n return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a];\n }\n if (result && typeof result === \"object\" && \"sections\" in result) return String(result);\n return result;\n },\n };\n}\n","/**\n * The document language, so formatted numbers and dates follow the UI locale.\n *\n * Shared by the Identify popup's value formatting and by label number\n * formatting, which both fall back to it when the author has not pinned an\n * explicit locale. Returns `undefined` outside a browser (tests, the headless\n * project authoring paths), which leaves `Intl` on its own default.\n */\nexport function documentLocale(): string | undefined {\n const lang = typeof document !== \"undefined\" ? document.documentElement.lang.trim() : \"\";\n return lang || undefined;\n}\n","import type { LabelStyle } from \"./types\";\n\n/**\n * Locale-aware number formatting for map labels (issue #2336).\n *\n * A numeric attribute rendered straight into a label reads as `1234567.5`,\n * which is hard to compare at a glance. QGIS solves this with\n * `format_number(field, places, language)`; the equivalent here is three\n * {@link LabelStyle} fields — {@link LabelStyle.numberFormatEnabled},\n * {@link LabelStyle.numberDecimals} and {@link LabelStyle.numberLocale} —\n * whose effect this module renders two ways:\n *\n * - {@link labelFieldTextField} builds the MapLibre `text-field` expression the\n * 2D map and the Mapbox/MapLibre style export both use, so the map and the\n * exported style agree.\n * - {@link formatLabelNumber} does the same in JavaScript for the paths that\n * resolve label text themselves: the Cesium globe and the duplicate-label\n * collapsing modes.\n *\n * Formatting applies to {@link LabelStyle.field} only. A label\n * {@link LabelStyle.expression} formats its own output (with `number-format`,\n * offered in the Expression Builder), so wrapping it here would fight the\n * author.\n */\n\n/**\n * Locales offered for {@link LabelStyle.numberLocale}, alongside the default\n * empty value that follows the app's own language.\n *\n * Deliberately short, and every entry groups with a character the map's glyph\n * stack can draw: ASCII `,`/`.` or U+00A0. Locales whose CLDR grouping uses\n * the narrow no-break space U+202F (`fr-FR`) or non-Latin digits (`ar-EG`)\n * are left out because a missing glyph renders as a blank box on the map\n * rather than as a separator. An author who needs one of those can still\n * write `[\"number-format\", …]` in the label expression.\n */\nexport const LABEL_NUMBER_LOCALES = [\"en-US\", \"de-DE\", \"ru-RU\", \"hi-IN\"] as const;\n\n/**\n * Magnitude at which the text-field stops treating a value as a finite number.\n *\n * JSON carries no `Infinity`, so the expression cannot test against it\n * directly; this is the largest power of ten a double holds, which doubles as\n * the `to-number` fallback so an unconvertible value fails the same test. A\n * real attribute at or above it labels as plain text instead of a formatted\n * number -- a trade worth making for values already past 1e308.\n */\nconst NON_FINITE_SENTINEL = 1e308;\n\n/** Sample value used to preview a locale's separators in the Style panel. */\nexport const LABEL_NUMBER_SAMPLE = 1234567.5;\n\n/**\n * Cached `Intl.NumberFormat`s, keyed by locale + decimals. `formatLabelNumber`\n * runs once per labeled feature (per Cesium entity, per deduplicated point), so\n * constructing a formatter on every call is the wrong order of magnitude.\n *\n * The key space is small: {@link LABEL_NUMBER_LOCALES} plus the app's own\n * language, times eleven decimal settings, times whatever a hand-edited project\n * pins.\n */\nconst formatterCache = new Map<string, Intl.NumberFormat>();\n\n/** Locale tags already accepted or rejected, so each is tested once. */\nconst localeSupport = new Map<string, boolean>();\n\n/**\n * A formatter for this locale/decimals pair. Grouping is always on: it is the\n * whole point of the setting. The locale must already have been vetted by\n * {@link resolveLabelNumberLocale}, so this never throws.\n */\nfunction labelNumberFormatter(locale: string | undefined, decimals: number): Intl.NumberFormat {\n const digits = clampLabelDecimals(decimals);\n const key = `${locale ?? \"\"}\\u0000${digits}`;\n const cached = formatterCache.get(key);\n if (cached) return cached;\n const formatter = new Intl.NumberFormat(locale, {\n useGrouping: true,\n minimumFractionDigits: digits,\n maximumFractionDigits: digits,\n });\n formatterCache.set(key, formatter);\n return formatter;\n}\n\n/**\n * Whether every character a locale's grouping produces is one the map's glyph\n * stack can draw: ASCII, or the no-break space U+00A0.\n *\n * {@link LABEL_NUMBER_LOCALES} is curated on exactly this basis, but the picker\n * is not the only way a locale reaches the renderer. A hand-edited project can\n * pin any tag, and the default (`numberLocale: \"\"`) follows the app's own\n * language, so a French or Arabic UI would otherwise put U+202F or Arabic-Indic\n * digits into the `text-field` and label the map with blank boxes. Testing the\n * rendered sample rather than the tag covers both routes and needs no list to\n * keep in sync.\n */\nfunction glyphSafeLocale(tag: string): boolean {\n try {\n const format = new Intl.NumberFormat(tag, {\n useGrouping: true,\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n // Both signs: sv-SE, fi-FI and nb-NO group with U+00A0 (safe) but write\n // negatives with U+2212 MINUS SIGN, so a positive-only check would pass\n // them and then blank-box every negative label.\n for (const sample of [\n format.format(LABEL_NUMBER_SAMPLE),\n format.format(-LABEL_NUMBER_SAMPLE),\n ]) {\n for (const char of sample) {\n const code = char.codePointAt(0) ?? 0;\n if (code >= 0x80 && code !== 0x00a0) return false;\n }\n }\n return true;\n } catch {\n // A malformed BCP 47 tag (`\"en_US\"`, `\"not a locale\"`) makes Intl throw a\n // RangeError. It would propagate out of the Cesium labeler and the dedup\n // builder and take down the layer's labels, and MapLibre evaluates its own\n // Intl.NumberFormat per feature, so the expression path is exposed too.\n return false;\n }\n}\n\n/** Clamp a decimal-places setting to the range the UI and `Intl` both accept. */\nexport function clampLabelDecimals(decimals: number): number {\n if (!Number.isFinite(decimals)) return 0;\n return Math.max(0, Math.min(10, Math.trunc(decimals)));\n}\n\n/**\n * The first locale tag that both `Intl` accepts and the map can draw, or\n * `undefined` to leave `Intl` and MapLibre on the runtime default.\n *\n * Every path resolves its locale through here -- the JavaScript formatter, the\n * MapLibre expression, and the dedup cache key -- so all three agree on which\n * locale is actually in effect.\n */\nexport function resolveLabelNumberLocale(...tags: (string | undefined)[]): string | undefined {\n for (const tag of tags) {\n if (!tag) continue;\n let usable = localeSupport.get(tag);\n if (usable === undefined) {\n usable = glyphSafeLocale(tag);\n localeSupport.set(tag, usable);\n }\n if (usable) return tag;\n }\n return undefined;\n}\n\n/**\n * Format one label value as a number, or `null` when the setting is off or the\n * value is not a finite number (a text attribute keeps its own rendering).\n *\n * @param value - The raw attribute value.\n * @param labels - The layer's label configuration.\n * @param fallbackLocale - Locale used when {@link LabelStyle.numberLocale} is\n * empty; the caller passes the app's language. Omitted means the runtime\n * default, matching how popups format their numbers.\n */\nexport function formatLabelNumber(\n value: unknown,\n labels: Pick<LabelStyle, \"numberFormatEnabled\" | \"numberDecimals\" | \"numberLocale\">,\n fallbackLocale?: string,\n): string | null {\n if (!labels.numberFormatEnabled) return null;\n if (typeof value !== \"number\" || !Number.isFinite(value)) return null;\n const locale = resolveLabelNumberLocale(labels.numberLocale, fallbackLocale);\n return labelNumberFormatter(locale, labels.numberDecimals).format(value);\n}\n\n/**\n * The MapLibre expression that renders {@link LabelStyle.field} as text, with\n * number formatting applied when it is on. Returns `\"\"` when no field is set,\n * which callers treat as \"no label text\".\n *\n * Number formatting is guarded by a `typeof` test rather than applied blindly\n * so a mixed or text column still labels normally; only real numbers take the\n * `number-format` branch.\n */\nexport function labelFieldTextField(\n labels: Pick<LabelStyle, \"field\" | \"numberFormatEnabled\" | \"numberDecimals\" | \"numberLocale\">,\n fallbackLocale?: string,\n): unknown[] | \"\" {\n const field = labels.field;\n if (!field) return \"\";\n const asText = [\"to-string\", [\"coalesce\", [\"get\", field], \"\"]];\n if (!labels.numberFormatEnabled) return asText;\n const locale = resolveLabelNumberLocale(labels.numberLocale, fallbackLocale);\n const digits = clampLabelDecimals(labels.numberDecimals);\n const options: Record<string, unknown> = {};\n if (locale) options.locale = locale;\n // MapLibre's `number-format` ignores a falsy option, so zero fraction digits\n // cannot be requested that way — it would silently fall back to the spec\n // default of up to three. Rounding first gives an integer, which then prints\n // with no fraction digits at all.\n // `to-number` carries a fallback so it can never throw: MapLibre hands a NaN\n // property through as null, and an unguarded conversion raised \"Could not\n // convert null to number\" from inside the render. The fallback is the same\n // sentinel the finiteness test rejects, so a value that could not convert\n // takes the plain-text branch rather than labelling as the fallback itself.\n const numeric = [\"to-number\", [\"get\", field], NON_FINITE_SENTINEL];\n const number = digits > 0 ? numeric : [\"round\", numeric];\n if (digits > 0) {\n options[\"min-fraction-digits\"] = digits;\n options[\"max-fraction-digits\"] = digits;\n }\n return [\n \"case\",\n [\n \"all\",\n [\"==\", [\"typeof\", [\"get\", field]], \"number\"],\n // Finite only, matching formatLabelNumber's Number.isFinite guard.\n // Without it the map rendered Infinity as U+221E -- a glyph the label\n // font need not carry, so a tofu box -- while the JavaScript path fell\n // back to the plain string, which is the disagreement the parity tests\n // exist to prevent. NaN was worse: it reached the render as null and\n // threw out of the conversion.\n [\"<\", [\"abs\", numeric], NON_FINITE_SENTINEL],\n ],\n [\"number-format\", number, options],\n asText,\n ];\n}\n\n/**\n * Render {@link LABEL_NUMBER_SAMPLE} the way a locale/decimals pair would, so\n * the Style panel can name each choice by the separators it actually produces\n * (\"1,234,567.50\") instead of by an opaque language tag.\n *\n * A tag `Intl` rejects (or one the map cannot draw) falls back to the runtime\n * locale's formatting rather than to an unformatted number, matching what\n * {@link formatLabelNumber} and the emitted expression do with the same tag.\n */\nexport function formatLabelNumberSample(locale: string, decimals: number): string {\n return labelNumberFormatter(resolveLabelNumberLocale(locale), decimals).format(\n LABEL_NUMBER_SAMPLE,\n );\n}\n","/**\n * Paint ownership for plugin-registered native layers.\n *\n * A plugin can register a MapLibre `CustomLayerInterface` (a WebGL layer that\n * draws itself, e.g. a Zarr renderer that reprojects chunks on the GPU) through\n * `registerExternalNativeLayer`. Such a layer has **no MapLibre paint\n * properties**, so GeoLibre's Style panel editors (raster opacity/brightness/\n * saturation/contrast/hue, or the vector paint editors) cannot reach it: every\n * `setPaintProperty` call is dropped and the control looks functional while\n * doing nothing (opengeos/GeoLibre#1445).\n *\n * The registration therefore declares `paintMode: \"plugin\"`, which lands in the\n * store layer as `metadata.paintMode` so the UI can hide the editors it cannot\n * apply, and may additionally supply a {@link ExternalNativePaintBridge} so the\n * generic controls GeoLibre keeps (opacity, visibility) are forwarded to the\n * custom layer's own API.\n *\n * The bridge holds live functions, so it cannot live in `metadata` (that is\n * serialized into `.geolibre.json`). It is kept in this module-level registry\n * instead, keyed by layer id, and the layer record only carries the\n * serializable `paintMode` flag.\n */\nexport interface ExternalNativePaintBridge {\n /**\n * Apply an opacity in [0, 1] to the plugin's layer (e.g.\n * `zarrLayer.setOpacity(value)`). When supplied, GeoLibre keeps its Opacity\n * sliders live for the layer and calls this whenever the value changes.\n */\n setOpacity?: (opacity: number) => void;\n /**\n * Apply the panel's show/hide state. MapLibre already honors `visibility` on\n * a custom layer, so this is only needed by layers that also want to release\n * resources or stop fetching while hidden.\n */\n setVisibility?: (visible: boolean) => void;\n}\n\n/** Serializable paint-ownership marker stored in `GeoLibreLayer.metadata`. */\nexport type ExternalNativePaintMode = \"geolibre\" | \"plugin\";\n\n/**\n * Whether a layer is drawn by a **custom render layer** its control created — a\n * deck.gl overlay, 3D Tiles, or Add Vector Layer's own MapLibre layers — rather\n * than by GeoLibre's layer sync. `metadata.customLayerType` is what marks one,\n * and it is the flag `syncExternalNativeLayer` branches on: for these layers the\n * sync only *moves and filters* native layers that already exist, so nothing\n * appears on the map unless the owning control (re)creates them.\n *\n * Lives here, in core, so the map's dispatch and the consumers that need to\n * reason about it — the Layer Library's \"can this be re-added and rendered?\"\n * gate (issue #1520) — share one definition and cannot drift.\n *\n * Distinct from {@link pluginOwnsPaint}: that asks who applies *paint* to an\n * existing layer, this asks who creates the layer at all.\n *\n * @param layer - A store layer (metadata only).\n * @returns True when only the owning control can create the layer's map output.\n */\nexport function controlRendersLayer(layer: { metadata: Record<string, unknown> }): boolean {\n return typeof layer.metadata.customLayerType === \"string\";\n}\n\n/**\n * True when the plugin that registered this layer paints it itself, so GeoLibre\n * must not offer (or apply) MapLibre paint properties for it.\n */\nexport function pluginOwnsPaint(layer: { metadata: Record<string, unknown> }): boolean {\n return layer.metadata.paintMode === \"plugin\";\n}\n\nconst paintBridges = new Map<string, ExternalNativePaintBridge>();\n\n/** Register (or replace) the paint bridge a plugin supplied for a layer. */\nexport function setExternalNativePaintBridge(\n layerId: string,\n bridge: ExternalNativePaintBridge | undefined,\n): void {\n if (bridge && (bridge.setOpacity || bridge.setVisibility)) {\n paintBridges.set(layerId, bridge);\n } else {\n paintBridges.delete(layerId);\n }\n}\n\n/** Drop a layer's paint bridge (on unregister, or when the layer is removed). */\nexport function clearExternalNativePaintBridge(layerId: string): void {\n paintBridges.delete(layerId);\n}\n\n/** The paint bridge for a layer, if its plugin supplied one. */\nexport function getExternalNativePaintBridge(\n layerId: string,\n): ExternalNativePaintBridge | undefined {\n return paintBridges.get(layerId);\n}\n\n/**\n * True when a plugin-painted layer bridged `setOpacity`, so an Opacity slider\n * would actually reach the renderer. It says nothing about GeoLibre-painted\n * layers, which have no bridge: the Style and Layers panels combine it with\n * `!pluginOwnsPaint(layer)` to decide whether to show the slider at all.\n */\nexport function supportsBridgedOpacity(layerId: string): boolean {\n return typeof paintBridges.get(layerId)?.setOpacity === \"function\";\n}\n\n/**\n * True when the primary renderer draws this plugin-painted layer itself, from\n * the store record, and so applies `layer.opacity` without any plugin bridge.\n *\n * A Zarr layer is a MapLibre custom layer owned by the Zarr control on the 2D\n * map, but the ArcGIS view and the Cesium globe render it natively (an ArcGIS\n * layer's `opacity`, a Cesium `ImageryLayer.alpha`) and mount no control to\n * register a bridge. Without this the Opacity slider would be hidden there\n * although it works (opengeos/GeoLibre#2261).\n *\n * @param layer - A store layer.\n * @param renderer - The primary renderer (`useAppStore().primaryRenderer`).\n * @returns Whether an Opacity slider reaches the layer through the renderer.\n */\nexport function rendererAppliesOpacity(\n layer: { type: string },\n renderer: string | undefined,\n): boolean {\n // @geolibre/core sits below @geolibre/map and cannot read its capabilities;\n // these are the renderers whose `nativeZarr` is set, which a test pins.\n // eslint-disable-next-line local/no-renderer-kind-checks -- see above\n return (renderer === \"arcgis\" || renderer === \"cesium\") && layer.type === \"zarr\";\n}\n\n/**\n * Whether `layer` is an Add Vector Layer layer GeoLibre adopted\n * (`maplibre-gl-vector-adopted`, ADOPTED_VECTOR_SOURCE_KIND in\n * @geolibre/plugins) that has no features yet. A project saves a URL- or\n * path-backed adopted layer without them, and the vector control reads them\n * again on reopen. Until then its `source.url` may name GeoParquet or\n * GeoPackage, so a renderer must wait rather than load that URL as GeoJSON.\n *\n * @param layer - A store layer.\n * @returns True while the adopted layer awaits its features.\n */\nexport function isAdoptedVectorAwaitingFeatures(layer: {\n metadata: Record<string, unknown>;\n geojson?: unknown;\n}): boolean {\n return layer.metadata.sourceKind === \"maplibre-gl-vector-adopted\" && !layer.geojson;\n}\n","/**\n * Pure helpers for the Attribute Form designer (#1322): coercing raw form\n * strings into widget-typed values, validating values against per-field rules\n * (required, numeric bounds, value maps, boolean constraint expressions), and\n * evaluating conditional field visibility.\n *\n * The designer UI (layer properties → Attributes Form) authors an\n * {@link AttributeFormConfig} stored on the layer; the attribute editing\n * surfaces (attribute table inline editor, Field Collection capture form) call\n * these helpers so both enforce the same rules. Everything here is side-effect\n * free so it can be unit tested without a DOM or the app store.\n *\n * Constraint and visibility expressions are MapLibre expressions compiled and\n * evaluated through `expressions.ts` (the same engine as the Expression\n * Builder), always with `expectedType: \"boolean\"`.\n */\nimport type { Feature } from \"geojson\";\nimport { evaluateMapExpression } from \"./expressions\";\nimport type {\n AttributeFormConfig,\n AttributeFormFieldConfig,\n AttributeFormValueMapEntry,\n} from \"./types\";\n\n/** Options threaded to expression evaluation (constraints, visibility). */\nexport interface AttributeFormEvalOptions {\n /** Map zoom for `[\"zoom\"]` in expressions; defaults to 0. */\n zoom?: number;\n /**\n * Constraints a data service enforces regardless of the form's\n * presentation (ArcGIS domains). A field the form hides is still checked\n * against its entry here, so a visibility rule cannot let an invalid value\n * reach the service.\n */\n serviceForm?: AttributeFormConfig;\n /**\n * Real feature backing the properties, when the caller has one — its\n * geometry feeds `[\"geometry-type\"]` and geometry-aware operators. Its\n * `properties` are ignored in favor of the candidate record being\n * validated.\n */\n feature?: Feature;\n}\n\n/** Why a field's value was rejected; codes map to localized messages. */\nexport interface AttributeFormFieldError {\n code: \"required\" | \"number\" | \"integer\" | \"text\" | \"range\" | \"valueMap\" | \"constraint\";\n /**\n * For `constraint`: the author's `constraintDescription`, or the\n * expression's own error text when it failed to evaluate.\n */\n message?: string;\n /** Bounds echoed back for `range` error messages. */\n min?: number;\n max?: number;\n}\n\n/** Outcome of validating a whole properties record against a form config. */\nexport interface AttributeFormValidation {\n ok: boolean;\n /** Field name → error for every failing, visible, configured field. */\n errors: Record<string, AttributeFormFieldError>;\n}\n\n/** Look up the configuration for a field, if any. */\nexport function getAttributeFormField(\n form: AttributeFormConfig | undefined,\n field: string,\n): AttributeFormFieldConfig | undefined {\n return form?.fields.find((entry) => entry.field === field);\n}\n\n/** The label attribute forms should show for a configured field. */\nexport function attributeFormFieldLabel(config: AttributeFormFieldConfig): string {\n return config.alias?.trim() || config.field;\n}\n\n/**\n * Parse the designer's value-map text — one entry per line, `value=label` or\n * just `value` — into entries. Blank lines and duplicate values are dropped.\n */\nexport function parseValueMapText(text: string): AttributeFormValueMapEntry[] {\n const seen = new Set<string>();\n const entries: AttributeFormValueMapEntry[] = [];\n for (const line of text.split(\"\\n\")) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n const eq = trimmed.indexOf(\"=\");\n const value = (eq >= 0 ? trimmed.slice(0, eq) : trimmed).trim();\n const label = eq >= 0 ? trimmed.slice(eq + 1).trim() : \"\";\n if (!value || seen.has(value)) continue;\n seen.add(value);\n entries.push(label && label !== value ? { value, label } : { value });\n }\n return entries;\n}\n\n/** Serialize value-map entries back into the designer's editable text form. */\nexport function valueMapToText(entries: readonly AttributeFormValueMapEntry[] | undefined): string {\n return (entries ?? [])\n .map((entry) =>\n entry.label && entry.label !== entry.value ? `${entry.value}=${entry.label}` : entry.value,\n )\n .join(\"\\n\");\n}\n\n/** Display label for a stored value under a value-map widget. */\nexport function valueMapLabelFor(config: AttributeFormFieldConfig, value: unknown): string {\n const text = value == null ? \"\" : String(value);\n const entry = config.valueMap?.find((candidate) => candidate.value === text);\n return entry?.label ?? text;\n}\n\n/**\n * Coerce a raw form string into the typed value the widget stores on the\n * feature. An empty/blank string means \"no value\" (`null`). A `number`/`range`\n * string that does not parse is returned verbatim so validation can flag it\n * instead of silently dropping the input.\n */\nexport function coerceAttributeFormValue(config: AttributeFormFieldConfig, raw: string): unknown {\n const trimmed = raw.trim();\n if (trimmed === \"\") return null;\n if (config.valueType) {\n // A declared type wins over the widget heuristics below. A value-map pick\n // is kept verbatim so a code with surrounding spaces still matches.\n const picked =\n config.widget === \"valueMap\" && config.valueMap?.some((entry) => entry.value === raw)\n ? raw\n : trimmed;\n if (config.valueType === \"string\") return picked;\n const parsed = Number(picked);\n return Number.isFinite(parsed) ? parsed : raw;\n }\n if (config.widget === \"number\" || config.widget === \"range\") {\n const parsed = Number(trimmed);\n return Number.isFinite(parsed) ? parsed : raw;\n }\n if (config.widget === \"checkbox\") {\n const normalized = trimmed.toLowerCase();\n if (normalized === \"true\") return true;\n if (normalized === \"false\") return false;\n return raw;\n }\n // A value map whose entries are all canonical numeric codes stores numbers,\n // so an edited row keeps the same property type as untouched rows (strict-\n // equality style/filter expressions would otherwise silently miss edited\n // features). Canonical means the string round-trips through Number (\"1\",\n // \"-2\", \"3.5\") — zero-padded or exotic forms like \"01\" or \"1e3\" are\n // identifiers, not numbers, and stay strings so the membership check in\n // validateAttributeFormField keeps matching the entry verbatim.\n if (\n config.widget === \"valueMap\" &&\n config.valueMap?.length &&\n config.valueMap.every((entry) => String(Number(entry.value)) === entry.value)\n ) {\n const parsed = Number(trimmed);\n if (Number.isFinite(parsed)) return parsed;\n }\n // text, date (ISO yyyy-mm-dd string), and valueMap store the string verbatim.\n return trimmed;\n}\n\n/**\n * Whether a configured field is currently shown, given the form's candidate\n * properties. An empty, invalid, or erroring visibility expression fails open\n * (visible) so a typo cannot hide data entry.\n */\nexport function isAttributeFormFieldVisible(\n config: AttributeFormFieldConfig,\n properties: Record<string, unknown>,\n options: AttributeFormEvalOptions = {},\n): boolean {\n const source = config.visibilityExpression?.trim();\n if (!source) return true;\n const result = evaluateMapExpression(source, {\n feature: featureForEvaluation(properties, options.feature),\n zoom: options.zoom,\n expectedType: \"boolean\",\n });\n return result.kind === \"value\" ? result.value === true : true;\n}\n\n/**\n * Validate one configured field against the candidate properties record (the\n * field's own candidate value lives at `properties[config.field]`). Returns\n * `null` when the value passes. Checks run in order: required, widget typing,\n * numeric bounds, value-map membership, then the constraint expression — the\n * constraint sees the whole record, so cross-field rules work.\n */\nexport function validateAttributeFormField(\n config: AttributeFormFieldConfig,\n properties: Record<string, unknown>,\n options: AttributeFormEvalOptions = {},\n): AttributeFormFieldError | null {\n const value = properties[config.field];\n const empty = value == null || value === \"\";\n\n if (empty) {\n // An unchecked checkbox is a valid false-like state, not a missing value —\n // requiring it would block every save with a confusing \"value required\".\n if (config.required && config.widget !== \"checkbox\") return { code: \"required\" };\n } else {\n if (config.valueType === \"string\" && typeof value !== \"string\") return { code: \"text\" };\n if (config.valueType === \"number\" || config.valueType === \"integer\") {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return { code: \"number\" };\n if (config.valueType === \"integer\" && !Number.isSafeInteger(value))\n return { code: \"integer\" };\n }\n if (config.widget === \"number\" || config.widget === \"range\") {\n const numeric = typeof value === \"number\" ? value : Number(value);\n if (!Number.isFinite(numeric)) return { code: \"number\" };\n if (\n (config.min != null && numeric < config.min) ||\n (config.max != null && numeric > config.max)\n ) {\n return { code: \"range\", min: config.min, max: config.max };\n }\n }\n if (\n config.widget === \"valueMap\" &&\n config.valueMap?.length &&\n !config.valueMap.some((entry) => entry.value === String(value))\n ) {\n return { code: \"valueMap\" };\n }\n }\n\n const source = config.constraintExpression?.trim();\n if (source) {\n const result = evaluateMapExpression(source, {\n feature: featureForEvaluation(properties, options.feature),\n zoom: options.zoom,\n expectedType: \"boolean\",\n });\n if (result.kind === \"error\") {\n // An unevaluable constraint cannot be verified, so the value is\n // rejected — surfacing the compile/runtime message rather than\n // silently accepting data the author meant to gate.\n return { code: \"constraint\", message: (result.errors ?? []).join(\"; \") };\n }\n if (result.kind === \"value\" && result.value !== true) {\n return {\n code: \"constraint\",\n ...(config.constraintDescription ? { message: config.constraintDescription } : {}),\n };\n }\n }\n\n return null;\n}\n\n/**\n * Validate a whole candidate properties record against a form config. Hidden\n * fields (visibility expression currently false) skip the form's own rules —\n * a field the form does not show cannot block a save — but are still checked\n * against `options.serviceForm`, whose constraints the data service enforces.\n */\nexport function validateAttributeFormValues(\n form: AttributeFormConfig | undefined,\n properties: Record<string, unknown>,\n options: AttributeFormEvalOptions = {},\n): AttributeFormValidation {\n const errors: Record<string, AttributeFormFieldError> = {};\n const checked = new Set<string>();\n for (const config of form?.fields ?? []) {\n if (!isAttributeFormFieldVisible(config, properties, options)) continue;\n checked.add(config.field);\n const error = validateAttributeFormField(config, properties, options);\n if (error) errors[config.field] = error;\n }\n for (const config of options.serviceForm?.fields ?? []) {\n if (checked.has(config.field)) continue;\n const error = validateAttributeFormField(config, properties, options);\n if (error) errors[config.field] = error;\n }\n return { ok: Object.keys(errors).length === 0, errors };\n}\n\n/**\n * Combine a layer's designer-authored form with the constraints its data\n * service publishes (for example ArcGIS coded-value and range domains) into\n * the form the editing surfaces use. The author's presentation choices\n * (alias, visibility, constraint expression, a narrower value map or bounds)\n * survive, but never widen what the service accepts: a service value map\n * forces the value-map widget and keeps only author entries the service also\n * lists (all service entries when none overlap), bounds take the tighter of\n * each side, and the service's declared value type always applies.\n *\n * @param form - The layer's designer-authored config, if any.\n * @param service - Service-derived configs, if the layer has any.\n * @returns The effective config, or `form` itself when there is no service form.\n */\nexport function mergeAttributeFormConfigs(\n form: AttributeFormConfig | undefined,\n service: AttributeFormConfig | undefined,\n): AttributeFormConfig | undefined {\n if (!service?.fields.length) return form;\n const serviceFields = new Map(service.fields.map((entry) => [entry.field, entry]));\n const fields: AttributeFormFieldConfig[] = [];\n for (const own of form?.fields ?? []) {\n const constraint = serviceFields.get(own.field);\n serviceFields.delete(own.field);\n fields.push(constraint ? mergeAttributeFormField(own, constraint) : own);\n }\n fields.push(...serviceFields.values());\n return { fields };\n}\n\nfunction mergeAttributeFormField(\n own: AttributeFormFieldConfig,\n service: AttributeFormFieldConfig,\n): AttributeFormFieldConfig {\n const merged: AttributeFormFieldConfig = { ...own, valueType: service.valueType };\n if (!own.alias?.trim() && service.alias) merged.alias = service.alias;\n if (service.required) merged.required = true;\n if (service.widget === \"valueMap\" && service.valueMap) {\n const allowed = new Map(service.valueMap.map((entry) => [entry.value, entry]));\n const narrowed =\n own.widget === \"valueMap\"\n ? (own.valueMap ?? [])\n .filter((entry) => allowed.has(entry.value))\n .map((entry) => ({\n value: entry.value,\n label: entry.label ?? allowed.get(entry.value)!.label,\n }))\n : [];\n merged.widget = \"valueMap\";\n merged.valueMap = narrowed.length ? narrowed : service.valueMap;\n delete merged.min;\n delete merged.max;\n delete merged.step;\n return merged;\n }\n if (own.widget !== \"number\" && own.widget !== \"range\") {\n merged.widget = service.widget;\n delete merged.valueMap;\n }\n merged.min = tighter(own.min, service.min, Math.max);\n merged.max = tighter(own.max, service.max, Math.min);\n if (merged.min === undefined) delete merged.min;\n if (merged.max === undefined) delete merged.max;\n return merged;\n}\n\nfunction tighter(\n a: number | undefined,\n b: number | undefined,\n pick: (a: number, b: number) => number,\n): number | undefined {\n if (a == null) return b;\n if (b == null) return a;\n return pick(a, b);\n}\n\n/**\n * Build the feature handed to expression evaluation: the caller's real\n * feature geometry when available (so `[\"geometry-type\"]` works), always with\n * the candidate properties being validated.\n */\nfunction featureForEvaluation(\n properties: Record<string, unknown>,\n feature: Feature | undefined,\n): Feature {\n return {\n type: \"Feature\",\n geometry: feature?.geometry ?? { type: \"Point\", coordinates: [0, 0] },\n ...(feature?.id !== undefined ? { id: feature.id } : {}),\n properties: properties as Feature[\"properties\"],\n };\n}\n","/**\n * Pure helpers for the popup and tooltip designer (#2113): resolving which\n * fields a layer's Identify popup (or hover tooltip) shows and in what order,\n * and formatting each value the way the author asked for.\n *\n * The designer UI (Style panel → Popup) authors a {@link LayerPopupConfig} on\n * the layer; `MapCanvas` renders the popup from these helpers so the click\n * popup and the hover tooltip agree on labels, order and formatting.\n * Everything here is side-effect free so it can be unit tested without a DOM\n * or the app store.\n *\n * Two rules the rest of the app depends on:\n *\n * - {@link GeoLibreLayer.fieldVisibility} is authoritative. A field marked\n * `\"hidden\"` or `\"excluded\"` never reaches a popup, even when a popup config\n * names it explicitly — the config selects from what is visible, it cannot\n * re-expose what the author hid.\n * - A layer with no config behaves exactly as it did before the designer\n * existed: every property, in the feature's own key order, unformatted.\n */\nimport type { Feature } from \"geojson\";\nimport { compileFeatureExpression, type CompiledFeatureExpression } from \"./expressions\";\nimport { PHOTO_FULL_PROPERTY } from \"./photo\";\nimport type {\n FieldVisibility,\n LayerPopupConfig,\n PopupDateFormat,\n PopupFieldConfig,\n PopupFieldFormat,\n PopupFieldKind,\n} from \"./types\";\n\n/** Match an inline base64 raster image (excludes SVG, which can carry scripts). */\nconst INLINE_IMAGE_DATA_URL = /^data:image\\/(?!svg)[\\w.+-]+;base64,/i;\n\n/** One resolved popup row, ready for the renderer. */\nexport interface PopupRow {\n /** Feature property key. */\n field: string;\n /** Label to print in the key column. */\n label: string;\n /** The raw property value, for kinds that need it (image `src`, link `href`). */\n value: unknown;\n /** The formatted value text. */\n text: string;\n /** How the renderer should draw the value. */\n kind: PopupFieldKind;\n /** Link text for a `\"link\"` row; falls back to the value. */\n linkLabel?: string;\n}\n\n/**\n * A trimmed non-empty string, or `undefined` for anything else.\n *\n * Popup configs arrive from untrusted JSON — a hand-edited `.geolibre.json`, an\n * imported layer-library bundle, an MCP-authored project — so a field typed as\n * `string` may hold a number, an object or null. `value.trim()` on one of those\n * throws and takes the whole popup render with it, so every string read here\n * goes through this.\n */\nfunction trimmedString(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/** Whether the Identify popup should open for a layer. Default: yes. */\nexport function isPopupClickEnabled(popup: LayerPopupConfig | undefined): boolean {\n return popup?.click !== false;\n}\n\n/** Whether a hover tooltip should follow the pointer. Default: no. */\nexport function isPopupHoverEnabled(popup: LayerPopupConfig | undefined): boolean {\n return popup?.hover === true;\n}\n\n/**\n * Bounds for {@link LayerPopupConfig.maxWidth}, in CSS pixels. The floor is\n * the popup's own minimum width (`min-w-[18rem]`), below which the setting\n * would be overruled by the layout and read as broken; the ceiling keeps a\n * hand-edited project from authoring a popup that blankets the map.\n */\nexport const POPUP_MAX_WIDTH_RANGE = { min: 288, max: 1200 } as const;\n\n/**\n * Bounds for {@link LayerPopupConfig.imageHeight}, in CSS pixels. Below the\n * floor a thumbnail carries no information; the ceiling matches\n * {@link POPUP_MAX_WIDTH_RANGE}, since a taller picture than that is what the\n * lightbox is for.\n */\nexport const POPUP_IMAGE_HEIGHT_RANGE = { min: 40, max: 1200 } as const;\n\n/**\n * Coerce an author-supplied pixel size to a usable number, or `undefined`.\n *\n * Popup configs arrive from untrusted JSON, so the value may be a string, null\n * or NaN. Anything that is not a finite positive number means \"unset\" (keep\n * the default) rather than an error that would take the popup render down,\n * and a number outside `range` is clamped rather than dropped — a project\n * asking for a 4000px popup wants the widest one it can have.\n */\nfunction pixelSize(\n value: unknown,\n range: { readonly min: number; readonly max: number },\n): number | undefined {\n const size = typeof value === \"number\" ? value : Number(trimmedString(value) ?? NaN);\n if (!Number.isFinite(size) || size <= 0) return undefined;\n return Math.round(Math.max(range.min, Math.min(range.max, size)));\n}\n\n/**\n * The widest the click popup may draw for a layer, in CSS pixels, or\n * `undefined` to keep the renderer's default.\n */\nexport function resolvePopupMaxWidth(popup: LayerPopupConfig | undefined): number | undefined {\n return pixelSize(popup?.maxWidth, POPUP_MAX_WIDTH_RANGE);\n}\n\n/**\n * The tallest an `\"image\"` field may draw inside the popup, in CSS pixels, or\n * `undefined` to keep the stylesheet's default.\n */\nexport function resolvePopupImageHeight(popup: LayerPopupConfig | undefined): number | undefined {\n return pixelSize(popup?.imageHeight, POPUP_IMAGE_HEIGHT_RANGE);\n}\n\n/** The label a popup shows for a configured field. */\nexport function popupFieldLabel(config: PopupFieldConfig): string {\n return trimmedString(config.label) ?? config.field;\n}\n\n/**\n * Property keys that never belong in a popup: GeoLibre's own internal columns\n * and the full-resolution companion of a photo thumbnail (an internal twin of\n * a value already shown, and often multiple megabytes of data URL).\n */\nexport function isInternalPopupField(key: string): boolean {\n return key === PHOTO_FULL_PROPERTY || key.startsWith(\"__geolibre_\");\n}\n\n/**\n * The property keys a popup may show, before any popup config narrows them:\n * the feature's own keys minus the internal ones and anything the author\n * marked `\"hidden\"` or `\"excluded\"`.\n */\nexport function visiblePopupFields(\n properties: Record<string, unknown>,\n fieldVisibility?: Record<string, FieldVisibility>,\n): string[] {\n return Object.keys(properties).filter(\n (key) => !isInternalPopupField(key) && !fieldVisibility?.[key],\n );\n}\n\n/** Options for {@link formatPopupValue} and {@link resolvePopupRows}. */\nexport interface PopupFormatOptions {\n /**\n * BCP 47 tag for number/date formatting. Omitted means the runtime default,\n * which is what the browser uses for every other locale-aware rendering.\n */\n locale?: string;\n}\n\nfunction formatNumber(value: number, format: PopupFieldFormat, locale?: string): string {\n const options: Intl.NumberFormatOptions = {\n useGrouping: format.thousands === true,\n };\n if (format.decimals != null && Number.isFinite(format.decimals)) {\n const digits = Math.max(0, Math.min(20, Math.trunc(format.decimals)));\n options.minimumFractionDigits = digits;\n options.maximumFractionDigits = digits;\n }\n return new Intl.NumberFormat(locale, options).format(value);\n}\n\n/**\n * Coerce a property value to a Date. A number is epoch milliseconds (what a\n * GeoJSON timestamp column normally holds); a string goes through `Date`'s own\n * parser. Anything that does not parse comes back as `null` so the caller can\n * fall back to the raw text rather than printing \"Invalid Date\".\n */\nfunction toDate(value: unknown): Date | null {\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;\n if (typeof value === \"number\" && Number.isFinite(value)) {\n const fromEpoch = new Date(value);\n return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch;\n }\n if (typeof value === \"string\" && value.trim() !== \"\") {\n const parsed = new Date(value);\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n }\n return null;\n}\n\nfunction formatDate(date: Date, dateFormat: PopupDateFormat, locale?: string): string {\n switch (dateFormat) {\n case \"datetime\":\n return date.toLocaleString(locale);\n case \"time\":\n return date.toLocaleTimeString(locale);\n case \"iso\":\n return date.toISOString();\n case \"year\":\n // Local, like the date/datetime/time cases above — only `iso` is\n // deliberately UTC, because that is what the format itself means. Reading\n // the year in UTC while the neighbouring formats read local would make\n // \"Year\" disagree with \"Date\" for the same timestamp near a year\n // boundary (2026-01-01T02:00:00Z is Dec 31 2025 in UTC-8).\n return String(date.getFullYear());\n case \"date\":\n default:\n return date.toLocaleDateString(locale);\n }\n}\n\n/** Stringify a value the way the untyped popup always has. */\nexport function stringifyPopupValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"object\") return JSON.stringify(value);\n return String(value);\n}\n\n/**\n * Render one value as text under a field's kind and format. `\"auto\"` and\n * `\"image\"` values are returned verbatim (the renderer draws them itself);\n * every other kind is stringified, formatted, then wrapped in the configured\n * prefix/suffix.\n */\nexport function formatPopupValue(\n value: unknown,\n config: Pick<PopupFieldConfig, \"kind\" | \"format\"> = {},\n options: PopupFormatOptions = {},\n): string {\n const kind = config.kind ?? \"auto\";\n const format = config.format ?? {};\n if (kind === \"auto\" || kind === \"image\") return stringifyPopupValue(value);\n\n // A missing value stays missing under every kind. Without this a `number`\n // field would coerce null (and \"\") through `Number(\"\")` to a confident 0,\n // reporting a population of zero for a city that simply has no figure.\n if (value == null || (typeof value === \"string\" && value.trim() === \"\")) return \"\";\n\n let body: string;\n if (kind === \"number\") {\n const numeric = typeof value === \"number\" ? value : Number(stringifyPopupValue(value));\n body = Number.isFinite(numeric)\n ? formatNumber(numeric, format, options.locale)\n : stringifyPopupValue(value);\n } else if (kind === \"date\") {\n const date = toDate(value);\n body = date\n ? formatDate(date, format.dateFormat ?? \"date\", options.locale)\n : stringifyPopupValue(value);\n } else {\n body = stringifyPopupValue(value);\n }\n\n // An empty value gets no affixes: \"$\" alone, or a bare \" km\", reads as data\n // that is present when it is not.\n if (body === \"\") return \"\";\n return `${format.prefix ?? \"\"}${body}${format.suffix ?? \"\"}`;\n}\n\n/** Whether a row would draw as a picture rather than as text. */\nfunction rendersAsImage(kind: PopupFieldKind, value: unknown): boolean {\n return kind === \"image\" || (kind === \"auto\" && isInlineImageValue(value));\n}\n\n/**\n * The properties a popup may read, with the author's hidden and excluded\n * fields and GeoLibre's internal columns removed.\n *\n * Expressions get this rather than the raw record. `[\"get\", \"ssn\"]` in a title\n * or body would otherwise reach a column the author hid, printing it into the\n * popup — and onto the hover tooltip, which needs no click at all. The rest of\n * this module already honors `fieldVisibility` field by field; an expression\n * has no field list to filter, so the filtering has to happen to its input.\n */\nexport function visiblePopupProperties(\n properties: Record<string, unknown>,\n fieldVisibility?: Record<string, FieldVisibility>,\n): Record<string, unknown> {\n const visible: Record<string, unknown> = {};\n for (const key of visiblePopupFields(properties, fieldVisibility)) {\n visible[key] = properties[key];\n }\n return visible;\n}\n\n/** Options for {@link resolvePopupRows}. */\nexport interface ResolvePopupRowsOptions extends PopupFormatOptions {\n popup?: LayerPopupConfig;\n fieldVisibility?: Record<string, FieldVisibility>;\n /** Resolve the hover subset (fields flagged `hover`) instead of the full list. */\n hover?: boolean;\n}\n\n/**\n * The rows a popup (or hover tooltip) should render for one feature's\n * properties.\n *\n * With no `popup.fields`, every visible property is returned in the feature's\n * own key order and left untyped, which is exactly what the popup did before\n * the designer existed. With a list, only the listed fields appear, in the\n * listed order, under their labels and formats — and a listed field the\n * feature does not carry is skipped rather than printed empty, so one config\n * can cover a layer whose features have ragged properties.\n *\n * `hover: true` narrows the list to fields flagged `hover`. A hover request\n * with no flagged field returns nothing: an author who turned the tooltip on\n * without choosing a field gets no tip rather than the whole table following\n * the pointer.\n */\nexport function resolvePopupRows(\n properties: Record<string, unknown>,\n options: ResolvePopupRowsOptions = {},\n): PopupRow[] {\n const { popup, fieldVisibility, hover = false, locale } = options;\n // Untrusted JSON again: `fields` may be absent, a non-array, or hold entries\n // whose `field` is not a string. Anything unusable is dropped here so the\n // loop below can treat every entry as a real field name.\n const configured = (Array.isArray(popup?.fields) ? popup.fields : []).filter(\n (config): config is PopupFieldConfig =>\n Boolean(config) && typeof (config as PopupFieldConfig).field === \"string\",\n );\n\n if (configured.length === 0) {\n if (hover) return [];\n return visiblePopupFields(properties, fieldVisibility).map((field) => ({\n field,\n label: field,\n value: properties[field],\n text: stringifyPopupValue(properties[field]),\n kind: \"auto\" as const,\n }));\n }\n\n const rows: PopupRow[] = [];\n const seen = new Set<string>();\n for (const config of configured) {\n if (hover && config.hover !== true) continue;\n // fieldVisibility wins: a popup config can order and relabel what is\n // visible, it can never re-expose a field the author hid or excluded.\n if (fieldVisibility?.[config.field]) continue;\n if (isInternalPopupField(config.field)) continue;\n if (!(config.field in properties)) continue;\n if (seen.has(config.field)) continue;\n const value = properties[config.field];\n // A hover tip is a glance, and it re-renders on every animation frame the\n // pointer is over the feature. An image row has no useful text form — its\n // value is a multi-kilobyte data URL — so it would print that URL as the\n // tip's body, over and over. Skip it and let the click popup show the\n // picture.\n if (hover && rendersAsImage(config.kind ?? \"auto\", value)) continue;\n seen.add(config.field);\n rows.push({\n field: config.field,\n label: popupFieldLabel(config),\n value,\n text: formatPopupValue(value, config, { locale }),\n kind: config.kind ?? \"auto\",\n ...(trimmedString(config.format?.linkLabel)\n ? { linkLabel: trimmedString(config.format?.linkLabel) }\n : {}),\n });\n }\n return rows;\n}\n\n/**\n * Compiled title/body expressions, keyed by zoom and source.\n *\n * `createHoverTooltipElement` calls {@link resolvePopupTitle} once per\n * animation frame for as long as the pointer sits on a hovered feature, and\n * compiling means a `JSON.parse` plus a full style-spec validation. Without\n * this the same unchanged source string would be re-parsed sixty times a\n * second. Zoom is part of the key because a compiled expression bakes it in\n * for `[\"zoom\"]`; it is constant through a hover, so the cache still hits.\n */\nconst EXPRESSION_CACHE = new Map<string, CompiledFeatureExpression>();\n/** Bound on the cache, so a project full of layers cannot grow it without end. */\nconst EXPRESSION_CACHE_LIMIT = 64;\n\nfunction compiledPopupExpression(source: string, zoom: number): CompiledFeatureExpression {\n const key = `${zoom}\\u0000${source}`;\n const cached = EXPRESSION_CACHE.get(key);\n if (cached) return cached;\n const compiled = compileFeatureExpression(source, { zoom });\n // Map iterates in insertion order, so the first key is the oldest entry.\n if (EXPRESSION_CACHE.size >= EXPRESSION_CACHE_LIMIT) {\n const oldest = EXPRESSION_CACHE.keys().next().value;\n if (oldest !== undefined) EXPRESSION_CACHE.delete(oldest);\n }\n EXPRESSION_CACHE.set(key, compiled);\n return compiled;\n}\n\n/**\n * Evaluate a popup expression against a feature's properties, or return\n * `undefined` when it does not compile or throws. Both failures mean the same\n * thing to every caller here: fall back to what the popup would show without\n * the expression.\n */\nfunction evaluatePopupExpression(\n source: string,\n properties: Record<string, unknown>,\n options: PopupExpressionOptions,\n): unknown {\n const compiled = compiledPopupExpression(source, options.zoom ?? 0);\n if (!compiled.ok || !compiled.evaluate) return undefined;\n try {\n return compiled.evaluate(\n featureFor(visiblePopupProperties(properties, options.fieldVisibility), options.feature),\n );\n } catch {\n return undefined;\n }\n}\n\n/** Build the synthetic feature an expression is evaluated against. */\nfunction featureFor(properties: Record<string, unknown>, feature?: Feature | null): Feature {\n if (feature) return { ...feature, properties };\n return { type: \"Feature\", geometry: null as never, properties };\n}\n\n/** Options for {@link resolvePopupTitle} and {@link resolvePopupBody}. */\nexport interface PopupExpressionOptions {\n /**\n * The author's field visibility, so an expression cannot read a hidden or\n * excluded column. Omitting it means nothing is hidden.\n */\n fieldVisibility?: Record<string, FieldVisibility>;\n /** The real feature, when the caller has one — feeds `[\"geometry-type\"]`. */\n feature?: Feature | null;\n /** Map zoom for `[\"zoom\"]`; defaults to 0. */\n zoom?: number;\n}\n\n/**\n * The heading a popup should show: the title expression's value, else the\n * title field's value, else the layer name. An expression that fails to\n * compile or evaluate, and a title field that is empty or hidden, both fall\n * back to the layer name rather than leaving the popup headless.\n */\nexport function resolvePopupTitle(\n layerName: string,\n properties: Record<string, unknown>,\n popup: LayerPopupConfig | undefined,\n options: PopupExpressionOptions = {},\n): string {\n return resolveConfiguredPopupTitle(properties, popup, options) ?? layerName;\n}\n\n/**\n * The title the author's own configuration produced, or `null` when there is\n * none — no title field or expression, or one that evaluated to nothing, or a\n * field the author hid.\n *\n * {@link resolvePopupTitle} is this plus the layer-name fallback. The hover\n * tooltip needs the two apart: a tip with no fields and no configured title\n * would be a box repeating the layer name, so it is suppressed — but that\n * decision has to rest on whether a title was *configured*, not on whether the\n * resolved text happens to equal the layer name. A city layer named \"Olympia\"\n * whose feature is also called \"Olympia\" is a real title, not a fallback.\n */\nexport function resolveConfiguredPopupTitle(\n properties: Record<string, unknown>,\n popup: LayerPopupConfig | undefined,\n options: PopupExpressionOptions = {},\n): string | null {\n const source = trimmedString(popup?.titleExpression);\n if (source) {\n const text = stringifyPopupValue(evaluatePopupExpression(source, properties, options)).trim();\n if (text) return text;\n }\n const field = trimmedString(popup?.titleField);\n if (field && !options.fieldVisibility?.[field] && !isInternalPopupField(field)) {\n const text = stringifyPopupValue(properties[field]).trim();\n if (text) return text;\n }\n return null;\n}\n\n/**\n * The popup body text when the author supplied a body expression, or `null`\n * when there is none (or it failed) and the field rows should render instead.\n */\nexport function resolvePopupBody(\n properties: Record<string, unknown>,\n popup: LayerPopupConfig | undefined,\n options: PopupExpressionOptions = {},\n): string | null {\n const source = trimmedString(popup?.bodyExpression);\n if (!source) return null;\n const preview = evaluatePopupExpression(source, properties, options);\n if (preview === undefined) return null;\n const text = stringifyPopupValue(preview);\n return text.trim() ? text : null;\n}\n\n/**\n * Whether a value should render as an inline image thumbnail under `\"auto\"`.\n * Kept here so the renderer and the designer's preview agree on what counts.\n */\nexport function isInlineImageValue(value: unknown): value is string {\n return typeof value === \"string\" && INLINE_IMAGE_DATA_URL.test(value);\n}\n\n/**\n * Whether a URL is safe to put in a popup `href`/`src`. Only http(s) and\n * inline raster data URLs: a `javascript:` or `data:text/html` value coming\n * from an untrusted GeoJSON must never become a live link.\n */\nexport function isSafePopupUrl(value: unknown, allowDataImage = false): value is string {\n if (typeof value !== \"string\") return false;\n const trimmed = value.trim();\n if (/^https?:\\/\\//i.test(trimmed)) return true;\n return allowDataImage && INLINE_IMAGE_DATA_URL.test(trimmed);\n}\n","import type { GeoLibreLayer, LayerQuickFilter } from \"./types\";\n\n/**\n * Quick filters (issue #2114): per-layer, data-driven filter controls that\n * compile down to the MapLibre filter a layer already supports.\n *\n * The persisted record is the *control state* (field, kind, chosen values),\n * never the compiled expression. Storing the compiled output would make a\n * filter unreadable and un-editable the moment it is saved, which is exactly\n * the wall this feature exists to remove: a rule filter, Select by Expression,\n * and the attribute table's filter box all already accept a hand-written\n * MapLibre expression.\n *\n * Compilation happens at sync time in `@geolibre/map`'s layer sync, which\n * combines the result with the transient `timeFilter`, the embed API's\n * `embedFilter`, and the rule-based visibility filter under one `[\"all\", …]`,\n * so a host page's filter and a user's filter narrow the layer together\n * instead of clobbering each other.\n *\n * Every clause reads the property in place, so the same expression is correct\n * for a GeoJSON source and for a tile-backed source whose tiles decode later.\n */\n\n/** Day length in milliseconds, used to make a date filter's end day inclusive. */\nconst DAY_MS = 86_400_000;\n\n/** The `YYYY-MM-DD` prefix length every ISO date comparison slices to. */\nconst ISO_DAY_LENGTH = 10;\n\n/** Whether a filter is switched on (the field defaults to enabled). */\nfunction isEnabled(filter: LayerQuickFilter): boolean {\n return filter.enabled !== false;\n}\n\n/** A validated date bound: its canonical `YYYY-MM-DD` text and UTC midnight. */\ninterface IsoDayBound {\n day: string;\n ms: number;\n}\n\n/**\n * Validate a `YYYY-MM-DD` bound, returning its canonical text and UTC midnight,\n * or `null` when the text is blank, malformed, or not a real calendar day.\n *\n * The round-trip check is what rejects a day that does not exist: `Date.parse`\n * happily reads `2026-02-30` as March 2, so shape-checking alone would compile\n * a bound the user never chose. Rejecting here means a half-typed or impossible\n * bound compiles to no clause at all, leaving that side unconstrained rather\n * than filtering on a date nobody asked for.\n */\nfunction parseIsoDayBound(day: string | null | undefined): IsoDayBound | null {\n if (typeof day !== \"string\") return null;\n const trimmed = day.trim();\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(trimmed)) return null;\n const ms = Date.parse(`${trimmed}T00:00:00Z`);\n if (!Number.isFinite(ms)) return null;\n return new Date(ms).toISOString().slice(0, ISO_DAY_LENGTH) === trimmed\n ? { day: trimmed, ms }\n : null;\n}\n\n/** A finite number, or `null` for every other input (including `NaN`). */\nfunction finite(value: number | null | undefined): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * `[\"get\", field]` coerced for comparison, guarded by `[\"has\", field]` where a\n * missing property would otherwise coerce into range (`to-number` turns a\n * missing property into `0`, and `to-string` into `\"\"`, either of which can sit\n * inside a one-sided bound).\n */\nfunction presenceGuard(field: string): unknown[] {\n return [\"has\", field];\n}\n\nfunction compileCategorical(filter: LayerQuickFilter): unknown[] | null {\n const values = filter.values ?? [];\n if (values.length === 0) return null;\n // `in` keeps the values at their original type, so a numeric code and its\n // string spelling never collapse into one another the way `match` labels do.\n return [\"in\", [\"get\", filter.field], [\"literal\", values]];\n}\n\nfunction compileRange(filter: LayerQuickFilter): unknown[] | null {\n const first = finite(filter.min);\n const second = finite(filter.max);\n if (first === null && second === null) return null;\n // Order the bounds rather than trusting which box they were typed into. The\n // slider's thumbs cannot cross, but the two number inputs commit\n // independently, so a bound is briefly inverted on the way to any value whose\n // first digit is smaller (typing `200000` into a max already showing `800000`\n // passes through `2`). Compiling that literally yields an unsatisfiable\n // filter, which empties the layer with nothing on screen to explain why.\n const min = first !== null && second !== null ? Math.min(first, second) : first;\n const max = first !== null && second !== null ? Math.max(first, second) : second;\n const value = [\"to-number\", [\"get\", filter.field]];\n const clauses: unknown[] = [presenceGuard(filter.field)];\n if (min !== null) clauses.push([\">=\", value, min]);\n if (max !== null) clauses.push([\"<=\", value, max]);\n return [\"all\", ...clauses];\n}\n\nfunction compileDate(filter: LayerQuickFilter): unknown[] | null {\n // Both branches read the *validated* bounds, never the raw strings: with one\n // bound valid and the other malformed the filter still compiles, and pushing\n // the raw text would compare against a value no real date can satisfy — which\n // hides every feature instead of leaving that side open.\n const first = parseIsoDayBound(filter.start);\n const second = parseIsoDayBound(filter.end);\n if (first === null && second === null) return null;\n // Ordered for the same reason as `compileRange`: a date typed into the wrong\n // box (or a from/to pair edited out of order) should read as the interval\n // between them, not as an empty layer.\n const inverted = first !== null && second !== null && first.ms > second.ms;\n const start = inverted ? second : first;\n const end = inverted ? first : second;\n const clauses: unknown[] = [presenceGuard(filter.field)];\n\n if ((filter.dateKind ?? \"iso\") === \"iso\") {\n // Compare the leading `YYYY-MM-DD` slice on both sides: ISO text sorts\n // chronologically, and slicing makes a datetime comparable to a plain day\n // bound without parsing. Both bounds are inclusive because the slice drops\n // the time of day.\n const value = [\"slice\", [\"to-string\", [\"get\", filter.field]], 0, ISO_DAY_LENGTH];\n if (start !== null) clauses.push([\">=\", value, start.day]);\n if (end !== null) clauses.push([\"<=\", value, end.day]);\n return [\"all\", ...clauses];\n }\n\n // Epoch numbers: the end bound is exclusive at the *next* day's midnight, so\n // the chosen end day is kept in full rather than only its midnight instant.\n const scale = filter.dateKind === \"epochS\" ? 0.001 : 1;\n const value = [\"to-number\", [\"get\", filter.field]];\n if (start !== null) clauses.push([\">=\", value, start.ms * scale]);\n if (end !== null) clauses.push([\"<\", value, (end.ms + DAY_MS) * scale]);\n return [\"all\", ...clauses];\n}\n\nfunction compileText(filter: LayerQuickFilter): unknown[] | null {\n const needle = filter.text?.trim().toLowerCase() ?? \"\";\n if (needle === \"\") return null;\n // Case-insensitive on both sides: a viewer typing \"portland\" should not have\n // to guess how the data capitalizes it.\n const haystack = [\"downcase\", [\"to-string\", [\"get\", filter.field]]];\n switch (filter.operator ?? \"contains\") {\n case \"equals\":\n return [\"==\", haystack, needle];\n case \"startsWith\":\n return [\"==\", [\"index-of\", needle, haystack], 0];\n default:\n return [\"!=\", [\"index-of\", needle, haystack], -1];\n }\n}\n\n/**\n * Compile one quick filter to a MapLibre filter expression, or `null` when it\n * places no constraint (switched off, no field, or nothing chosen yet). A\n * control that has been added but not yet answered must never hide features.\n *\n * @param filter - The persisted control state.\n * @returns A MapLibre filter expression, or `null`.\n */\nexport function compileQuickFilter(filter: LayerQuickFilter): unknown[] | null {\n if (!isEnabled(filter) || typeof filter.field !== \"string\" || filter.field === \"\") return null;\n switch (filter.kind) {\n case \"categorical\":\n return compileCategorical(filter);\n case \"range\":\n return compileRange(filter);\n case \"date\":\n return compileDate(filter);\n case \"text\":\n return compileText(filter);\n default:\n return null;\n }\n}\n\n/**\n * Compile a layer's quick filters into a single MapLibre filter expression.\n * Multiple controls narrow the data together (`[\"all\", …]`), matching how a\n * viewer reads a stack of filters. Returns `null` when no control constrains\n * anything, so the common path adds no filter at all.\n *\n * @param filters - The layer's persisted quick filters.\n * @returns A MapLibre filter expression, or `null`.\n */\nexport function compileQuickFilters(\n filters: readonly LayerQuickFilter[] | undefined,\n): unknown[] | null {\n if (!filters || filters.length === 0) return null;\n const clauses: unknown[] = [];\n for (const filter of filters) {\n const compiled = compileQuickFilter(filter);\n if (compiled) clauses.push(compiled);\n }\n if (clauses.length === 0) return null;\n return clauses.length === 1 ? (clauses[0] as unknown[]) : [\"all\", ...clauses];\n}\n\n/**\n * The quick filters that actually constrain the layer right now. Drives the\n * \"filtered\" badge on the layer row and its count, so an active filter is\n * never mistaken for missing data.\n *\n * @param layer - The layer to inspect.\n * @returns The constraining filters, in their configured order.\n */\nexport function activeQuickFilters(layer: GeoLibreLayer): LayerQuickFilter[] {\n return (layer.quickFilters ?? []).filter((filter) => compileQuickFilter(filter) !== null);\n}\n\n/**\n * Whether the layer is currently narrowed by at least one quick filter.\n *\n * @param layer - The layer to inspect.\n * @returns `true` when a quick filter constrains the rendered features.\n */\nexport function hasActiveQuickFilter(layer: GeoLibreLayer): boolean {\n return (layer.quickFilters ?? []).some((filter) => compileQuickFilter(filter) !== null);\n}\n\n/**\n * Clear every chosen value while keeping the controls themselves, so \"Clear\n * filters\" is one action that leaves the layer's configured controls in place\n * for the next question.\n *\n * @param filters - The layer's persisted quick filters.\n * @returns The same controls with their selections emptied.\n */\nexport function clearQuickFilterValues(\n filters: readonly LayerQuickFilter[] | undefined,\n): LayerQuickFilter[] {\n return (filters ?? []).map((filter) => ({\n ...filter,\n values: filter.kind === \"categorical\" ? [] : filter.values,\n min: filter.kind === \"range\" ? null : filter.min,\n max: filter.kind === \"range\" ? null : filter.max,\n start: filter.kind === \"date\" ? null : filter.start,\n end: filter.kind === \"date\" ? null : filter.end,\n text: filter.kind === \"text\" ? \"\" : filter.text,\n }));\n}\n","import type { GeoLibreLayer } from \"./types\";\nimport { compileQuickFilters } from \"./quick-filters\";\n\n/** The layer fields needed to inspect project-authored feature filters. */\ntype LayerFilterSource = Pick<GeoLibreLayer, \"filterExpression\" | \"quickFilters\">;\n\n/**\n * Return a layer's persisted expression filter when it has a usable array\n * shape, otherwise return null.\n *\n * @param layer - The layer whose expression filter should be read.\n * @returns The active expression filter, or null when none is configured.\n */\nexport function activeLayerFilterExpression(layer: LayerFilterSource): unknown[] | null {\n const expression = layer.filterExpression;\n return Array.isArray(expression) && expression.length > 0 ? expression : null;\n}\n\n/**\n * Compile all project-authored filters for a layer into one MapLibre boolean\n * expression. An explicit expression and Quick Filter controls narrow the\n * layer together.\n *\n * @param layer - The layer whose authored filters should be composed.\n * @returns A MapLibre filter expression, or null when no authored filter is active.\n */\nexport function compileLayerFilters(layer: LayerFilterSource): unknown[] | null {\n const filters = [\n activeLayerFilterExpression(layer),\n compileQuickFilters(layer.quickFilters),\n ].filter((filter): filter is unknown[] => filter !== null);\n if (filters.length === 0) return null;\n return filters.length === 1 ? filters[0] : [\"all\", ...filters];\n}\n\n/**\n * Whether a project-authored expression or Quick Filter currently narrows a\n * layer. This drives the filtered indicator and clear action in the layer UI.\n *\n * @param layer - The layer to inspect.\n * @returns True when at least one persisted filter is active.\n */\nexport function hasActiveLayerFilter(layer: LayerFilterSource): boolean {\n return compileLayerFilters(layer) !== null;\n}\n","// The deployment capability model (issue #1673).\n//\n// A *deployment* — a hosted container, a kiosk terminal, a classroom instance,\n// an embed — declares which privileged actions the build it serves is allowed\n// to offer. This is deliberately coarse: it names whole capabilities (\"may add\n// data at all\"), not individual menu items, so a locked-down deployment cannot\n// be defeated by a menu item someone forgot to list.\n//\n// It is *not* the interface profile (`ui-profile.ts` in the desktop app), which\n// hides items to reduce clutter and which the user can turn back on. A denied\n// capability is a statement by whoever stood the deployment up, and the UI\n// never offers a way to grant it back.\n//\n// The client-side gate is one half of the model: it stops the app advertising\n// and invoking what the deployment forbids. The serving layer still has to\n// refuse the corresponding requests, because a client-side-only kiosk mode is a\n// suggestion that anyone with devtools declines.\n\n/** A privileged action a deployment can grant or withhold. */\nexport type DeploymentCapability =\n /** Create, save, or otherwise author the project itself. */\n | \"project:edit\"\n /** Bring new data into the map from files, URLs, or services. */\n | \"data:add\"\n /** Run processing/analysis tools (Whitebox, SQL, vector/raster, ML). */\n | \"processing:run\"\n /** Get data or renderings back out (export image, standalone HTML, …). */\n | \"export:data\"\n /** Install, load, or manage plugins. */\n | \"plugins:install\"\n /** Reach the application settings. */\n | \"settings:manage\";\n\n/** Every capability, in a stable order for docs and tests. */\nexport const DEPLOYMENT_CAPABILITIES: readonly DeploymentCapability[] = [\n \"project:edit\",\n \"data:add\",\n \"processing:run\",\n \"export:data\",\n \"plugins:install\",\n \"settings:manage\",\n];\n\n/**\n * The default grant: everything. `full` must stay the default so an existing\n * deployment that configures nothing behaves exactly as it did before.\n */\nexport const ALL_DEPLOYMENT_CAPABILITIES: ReadonlySet<DeploymentCapability> = new Set(\n DEPLOYMENT_CAPABILITIES,\n);\n\n/** Whether an arbitrary string names a capability we know how to enforce. */\nexport function isDeploymentCapability(value: string): value is DeploymentCapability {\n return (DEPLOYMENT_CAPABILITIES as readonly string[]).includes(value);\n}\n\n/**\n * The reserved token that grants nothing, for a deployment that wants the map\n * and nothing else.\n *\n * Needed because a blank value cannot mean it: the deployment env reader treats\n * an empty string as unset (a `-e VAR=` produces one), and unset has to keep\n * meaning \"full\" so existing deployments are unchanged. Without a spelling for\n * \"none\", the most locked-down configuration would be the one you cannot write.\n */\nexport const NO_DEPLOYMENT_CAPABILITIES = \"none\";\n\n/**\n * Parse a comma-separated capability list from deployment configuration.\n *\n * Unknown tokens are dropped rather than throwing: a newer container image may\n * be configured with a capability this build does not know about, and the safe\n * reading of an unrecognized grant is \"not granted\".\n *\n * Fails closed on purpose. An operator who set the variable at all meant to\n * restrict something, so a list that parses to nothing grants nothing — only an\n * absent (or blank) value falls back to the full set, which is the caller's job\n * to detect.\n *\n * @param raw - The configured value, e.g. `\"data:add, export:data\"`.\n * @returns The granted capabilities, with duplicates and unknown names removed.\n */\nexport function parseDeploymentCapabilities(raw: string): ReadonlySet<DeploymentCapability> {\n const granted = new Set<DeploymentCapability>();\n if (raw.trim().toLowerCase() === NO_DEPLOYMENT_CAPABILITIES) return granted;\n for (const token of raw.split(\",\")) {\n const name = token.trim();\n if (isDeploymentCapability(name)) granted.add(name);\n }\n return granted;\n}\n","/**\n * What this deployment and this session may do: deployment capabilities\n * (issue #1673) and the application role / privilege model (issue #1672).\n * Excluded from the project file and from undo history.\n */\nimport { ALL_DEPLOYMENT_CAPABILITIES, type DeploymentCapability } from \"../deployment-capabilities\";\nimport {\n createDefaultAppCapabilities,\n hasAppPrivilege,\n normalizeAppPrivileges,\n resolveRolePrivileges,\n} from \"../capabilities\";\nimport type { AppCapabilities, AppPrivilege, AppRole } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface CapabilitiesSlice {\n /**\n * What this *deployment* is allowed to do (issue #1673). Set once at startup\n * from the deployment configuration; never from a project file, a URL\n * parameter, or anything else the visitor controls, and never edited from the\n * UI. Defaults to the full set so an unconfigured build behaves as before.\n *\n * Excluded from the project file and from undo history: it describes the\n * server that served the app, not the document being edited.\n */\n deploymentCapabilities: ReadonlySet<DeploymentCapability>;\n /**\n * Ephemeral application capability model (issue #1672). Gating role and\n * privileges for the current session/deployment. Excluded from the project file\n * and undo history.\n */\n capabilities: AppCapabilities;\n\n /**\n * Narrow what this deployment may do. Intended for the startup path only —\n * calling it later would leave already-rendered surfaces stale.\n */\n setDeploymentCapabilities: (capabilities: Iterable<DeploymentCapability>) => void;\n /**\n * Assign an application role (e.g. \"viewer\", \"editor\", \"publisher\", \"administrator\", \"custom\"),\n * deriving the effective privileges and optional reason.\n */\n setAppRole: (\n role: AppRole,\n options?: { customPrivileges?: AppPrivilege[]; reason?: string },\n ) => void;\n /** Set explicit custom privileges and an optional reason. */\n setAppPrivileges: (privileges: AppPrivilege[], reason?: string) => void;\n /** Grant an individual application privilege. */\n grantAppPrivilege: (privilege: AppPrivilege) => void;\n /** Revoke an individual application privilege with an optional reason. */\n revokeAppPrivilege: (privilege: AppPrivilege, reason?: string) => void;\n /** Reset application capabilities back to the default unconstrained Administrator role. */\n resetAppCapabilities: () => void;\n /** Check if the current capabilities grant the requested privilege. */\n hasAppPrivilege: (privilege: AppPrivilege) => boolean;\n}\n\nexport const createCapabilitiesSlice: SliceCreator<CapabilitiesSlice> = (set, get) => ({\n deploymentCapabilities: ALL_DEPLOYMENT_CAPABILITIES,\n capabilities: createDefaultAppCapabilities(),\n\n setDeploymentCapabilities: (capabilities) =>\n set({ deploymentCapabilities: new Set(capabilities) }),\n\n // A new role or privilege list is a new policy, so the per-privilege reasons\n // recorded against the old one go with it — carrying them forward would\n // explain a grant that is no longer withheld for that cause.\n setAppRole: (role, options) => {\n const privileges = resolveRolePrivileges(role, options?.customPrivileges);\n set({\n capabilities: {\n role,\n privileges,\n reason: options?.reason,\n },\n });\n },\n\n setAppPrivileges: (privileges, reason) => {\n set({\n capabilities: {\n role: \"custom\",\n privileges: normalizeAppPrivileges(privileges) ?? [],\n reason,\n },\n });\n },\n\n // An ad-hoc grant or revoke makes the set no longer the bundle its role\n // names, so the role becomes \"custom\" — the same thing setAppPrivileges\n // does for an explicit list. Leaving it as \"editor\" while the privileges\n // are not the editor bundle would mislead anything that branches on the\n // role rather than checking a privilege.\n grantAppPrivilege: (privilege) => {\n const current = get().capabilities;\n if (current.privileges.includes(privilege)) return;\n const { [privilege]: _granted, ...privilegeReasons } = current.privilegeReasons ?? {};\n set({\n capabilities: {\n ...current,\n role: \"custom\",\n privileges: [...current.privileges, privilege],\n privilegeReasons,\n },\n });\n },\n\n // The reason is filed against this privilege, not against the whole set:\n // revoking a second privilege for a different cause must not relabel the\n // first one's explanation. `reason` stays the fallback for the rest.\n //\n // Re-revoking an already-withheld privilege is not a no-op when it carries\n // a new reason: restating why something is denied is a real operation, and\n // an early return would silently keep the stale explanation on screen.\n revokeAppPrivilege: (privilege, reason) => {\n const current = get().capabilities;\n const held = current.privileges.includes(privilege);\n if (!held && !reason) return;\n set({\n capabilities: {\n ...current,\n role: held ? \"custom\" : current.role,\n privileges: held ? current.privileges.filter((p) => p !== privilege) : current.privileges,\n privilegeReasons: reason\n ? { ...current.privilegeReasons, [privilege]: reason }\n : current.privilegeReasons,\n },\n });\n },\n\n resetAppCapabilities: () => {\n set({ capabilities: createDefaultAppCapabilities() });\n },\n\n hasAppPrivilege: (privilege) => {\n return hasAppPrivilege(get().capabilities, privilege);\n },\n});\n","/**\n * Ephemeral live-collaboration session state (issue #307): never saved with the\n * project and never tracked by undo history.\n */\nimport type {\n CollabInvite,\n CollaborationChatMessage,\n CollaborationParticipant,\n CollaborationPresence,\n CollaborationState,\n} from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\n/**\n * A fresh, inactive collaboration slice (no live session). Frozen (like\n * DEFAULT_LEGEND_CONFIG) to guard against accidental in-place mutation; store\n * actions always produce new objects via spread, so the frozen default is only\n * ever read.\n */\nexport const DEFAULT_COLLABORATION_STATE: CollaborationState = Object.freeze({\n isActive: false,\n connecting: false,\n sessionId: null,\n clientId: null,\n role: null,\n mode: \"co-edit\",\n selfName: \"\",\n selfColor: \"\",\n participants: Object.freeze([] as CollaborationParticipant[]) as CollaborationParticipant[],\n presence: Object.freeze({} as Record<string, CollaborationPresence>) as Record<\n string,\n CollaborationPresence\n >,\n followHost: false,\n chat: Object.freeze([] as CollaborationChatMessage[]) as CollaborationChatMessage[],\n requireIdentity: false,\n identitySupported: false,\n lockedLayerIds: Object.freeze([] as string[]) as string[],\n invites: Object.freeze([] as CollabInvite[]) as CollabInvite[],\n error: null,\n});\n\n// Cap the in-store chat log so a long session can't grow it without bound. This\n// is intentionally larger than the relay's persisted history (50): the live\n// session accumulates messages locally, while the relay only retains the tail\n// for late joiners.\nconst MAX_COLLABORATION_CHAT = 200;\n\nexport interface CollaborationSlice {\n // Ephemeral live-collaboration session state (issue #307). Deliberately\n // excluded from the project file (project.ts never reads it) and from undo\n // history (partialize never lists it).\n collaboration: CollaborationState;\n\n setCollaboration: (patch: Partial<CollaborationState>) => void;\n updateCollaborationPresence: (clientId: string, presence: CollaborationPresence | null) => void;\n /** Append a chat message to the session log (bounded; #754). */\n addCollaborationChat: (message: CollaborationChatMessage) => void;\n resetCollaboration: () => void;\n}\n\nexport const createCollaborationSlice: SliceCreator<CollaborationSlice> = (set) => ({\n collaboration: DEFAULT_COLLABORATION_STATE,\n\n setCollaboration: (patch) => set((s) => ({ collaboration: { ...s.collaboration, ...patch } })),\n // Add or remove a single remote participant's presence without rebuilding\n // the whole map on every cursor move. Passing `null` drops the entry (on\n // participant leave).\n updateCollaborationPresence: (clientId, presence) =>\n set((s) => {\n const next = { ...s.collaboration.presence };\n if (presence === null) {\n delete next[clientId];\n } else {\n next[clientId] = presence;\n }\n return { collaboration: { ...s.collaboration, presence: next } };\n }),\n addCollaborationChat: (message) =>\n set((s) => {\n // Default to [] in case an older relay left the slice undefined.\n const current = s.collaboration.chat ?? [];\n // Ignore a duplicate id: the server broadcasts to the sender too, and a\n // reconnect can replay recent history, so de-dupe defensively.\n if (current.some((m) => m.id === message.id)) return s;\n const chat = [...current, message].slice(-MAX_COLLABORATION_CHAT);\n return { collaboration: { ...s.collaboration, chat } };\n }),\n resetCollaboration: () =>\n // Also close the Collaborate dialog: an unexpected disconnect resets the\n // slice without a user-initiated leave(), and leaving the dialog open\n // would drop the user onto the start/join form with no context.\n set((s) => ({\n collaboration: DEFAULT_COLLABORATION_STATE,\n ui: { ...s.ui, collaborateDialogOpen: false },\n })),\n});\n","/**\n * Per-session selection and live readouts: the selected layer and features,\n * the Identify target, the attribute-table filter, and the pointer / camera /\n * GPS readouts the status bar shows. None of it is saved with the project or\n * tracked by undo history.\n */\nimport type { SliceCreator } from \"./types\";\n\n/**\n * Latest live device-GPS fix published by the GPS Tracking tool (issue #1316),\n * read by the status bar readout. Device state, not project state: excluded\n * from undo history (partialize never lists it) and from project files, and\n * deliberately left untouched on project switches.\n */\nexport interface GpsStatusFix {\n lng: number;\n lat: number;\n /** Horizontal accuracy radius in meters. */\n accuracy: number;\n /** Satellites used for the fix, or null when the provider does not report it. */\n satellites: number | null;\n /** Ground speed in m/s, or null when the device doesn't report one. */\n speed: number | null;\n /** Fix time in epoch milliseconds. */\n timestamp: number;\n}\n\n/** Reserved Identify target for querying every visible queryable layer at once. */\nexport const IDENTIFY_ALL_LAYERS_ID = \"__geolibre_identify_all_layers__\";\n\n/** The Identify fields of the store, as set together by an Identify change. */\nexport interface IdentifyState {\n identifyLayerId: string | null;\n identifyLayerIds: string[] | null;\n}\n\n/**\n * Resolve an Identify target against the layers that exist.\n *\n * Shared by the scripting `setIdentify` command and by project load, which\n * applies a project's saved `interaction.identify` (issue #2688).\n *\n * @param target - A layer id, `\"all\"`, a list of layer ids, or null/undefined.\n * @param layerIds - Ids of the layers currently in the project.\n * @returns The store fields to set. Ids that name no layer are dropped; a list\n * that keeps one id becomes a single-layer target, and one that keeps several\n * becomes the all-layers mode restricted to those ids.\n */\nexport function resolveIdentifyTarget(\n target: string | readonly string[] | null | undefined,\n layerIds: Iterable<string>,\n): IdentifyState {\n const off: IdentifyState = { identifyLayerId: null, identifyLayerIds: null };\n if (target === null || target === undefined) return off;\n if (target === \"all\") return { identifyLayerId: IDENTIFY_ALL_LAYERS_ID, identifyLayerIds: null };\n const known = new Set(layerIds);\n if (typeof target === \"string\") {\n return known.has(target) ? { identifyLayerId: target, identifyLayerIds: null } : off;\n }\n const ids = [...new Set(target)].filter((id) => known.has(id));\n if (ids.length === 0) return off;\n if (ids.length === 1) return { identifyLayerId: ids[0], identifyLayerIds: null };\n return { identifyLayerId: IDENTIFY_ALL_LAYERS_ID, identifyLayerIds: ids };\n}\n\n/**\n * Whether the all-layers Identify mode may query a layer.\n *\n * @param layerId - The candidate layer's id.\n * @param identifyLayerIds - The store's `identifyLayerIds` restriction.\n * @returns True when there is no restriction or the layer is in it.\n */\nexport function identifyAllIncludes(\n layerId: string,\n identifyLayerIds: readonly string[] | null | undefined,\n): boolean {\n return !identifyLayerIds || identifyLayerIds.includes(layerId);\n}\n\n/**\n * Drop removed layers from the Identify state.\n *\n * @param state - The current Identify fields.\n * @param removed - Ids of the layers being removed.\n * @returns The Identify fields with those layers gone. A restricted all-layers\n * mode left with one layer narrows to it, and with none turns Identify off.\n */\nexport function identifyStateWithoutLayers(\n state: IdentifyState,\n removed: ReadonlySet<string>,\n): IdentifyState {\n if (state.identifyLayerId !== null && removed.has(state.identifyLayerId)) {\n return { identifyLayerId: null, identifyLayerIds: null };\n }\n // Return fresh fields, never `state` itself: callers spread the result into\n // a store update and pass the whole store as `state`.\n const unchanged = {\n identifyLayerId: state.identifyLayerId,\n identifyLayerIds: state.identifyLayerIds,\n };\n if (!state.identifyLayerIds) return unchanged;\n const ids = state.identifyLayerIds.filter((id) => !removed.has(id));\n if (ids.length === state.identifyLayerIds.length) return unchanged;\n if (ids.length === 0) return { identifyLayerId: null, identifyLayerIds: null };\n if (ids.length === 1) return { identifyLayerId: ids[0], identifyLayerIds: null };\n return { identifyLayerId: state.identifyLayerId, identifyLayerIds: ids };\n}\n\nexport interface SessionSlice {\n /** Local viewing switch; saved layer popup settings are left untouched. */\n hoverTooltipsEnabled: boolean;\n setHoverTooltipsEnabled: (enabled: boolean) => void;\n selectedLayerId: string | null;\n selectedFeatureId: string | null;\n /**\n * Full set of selected feature ids. The attribute table extends the single\n * selection to many rows via Ctrl/Cmd (toggle) and Shift (range). The anchor\n * — `selectedFeatureId` — is the primary/last-clicked feature used for map\n * fit, DuckDB highlight, and scripting, and is always one of these ids (or\n * `null` when the set is empty). A single click leaves exactly one id here.\n */\n selectedFeatureIds: string[];\n /**\n * Store-layer id targeted by Identify, or {@link IDENTIFY_ALL_LAYERS_ID} for\n * the map-level mode that queries every visible queryable layer — vector,\n * DuckDB query, WMS, COG, NetCDF image and time-slider raster alike.\n */\n identifyLayerId: string | null;\n /**\n * Layers the all-layers Identify mode is limited to, or null for every\n * visible queryable layer. Set only alongside {@link IDENTIFY_ALL_LAYERS_ID},\n * by a script or project that names several layers (issue #2688), and\n * cleared by every `setIdentifyLayer` call so the in-app buttons keep their\n * plain meaning.\n */\n identifyLayerIds: string[] | null;\n /** Transient map-selection gesture activity, published by the primary renderer. */\n featureSelectionActive: boolean;\n setFeatureSelectionActive: (active: boolean) => void;\n pointerCoords: [number, number] | null;\n /**\n * Ground elevation in true metres under the pointer, for the status bar\n * (issue #1813). Null when it cannot be resolved — the pointer is off the\n * map, terrain is off and the remote lookup has not answered (or failed), or\n * the active body is not Earth. Set alongside `pointerCoords` by MapCanvas,\n * which owns the map instance the terrain sample comes from.\n */\n pointerElevation: number | null;\n /**\n * Camera height above sea level in metres — Google Earth Pro's \"Eye alt\"\n * (issue #1816). Derived from the camera, so deliberately *not* part of\n * `mapView`: that shape is persisted into the project file, and a stored\n * altitude could only drift from the center/zoom/pitch it is computed from.\n * Null before the map loads, or when MapLibre cannot report it.\n */\n cameraAltitude: number | null;\n /** Live GPS fix for the status bar, or null while GPS tracking is off. */\n gpsStatus: GpsStatusFix | null;\n attributeFilter: string;\n\n setPointerCoords: (coords: [number, number] | null) => void;\n setPointerElevation: (elevation: number | null) => void;\n setCameraAltitude: (altitude: number | null) => void;\n setGpsStatus: (fix: GpsStatusFix | null) => void;\n selectLayer: (id: string | null) => void;\n selectFeature: (id: string | null) => void;\n /**\n * Replace the multi-selection with `ids`. The anchor (`selectedFeatureId`)\n * becomes `anchorId` when provided, otherwise the last id in the list (or\n * `null` when the list is empty).\n */\n selectFeatures: (ids: string[], anchorId?: string | null) => void;\n setIdentifyLayer: (id: string | null) => void;\n /** Set both Identify fields at once (see {@link resolveIdentifyTarget}). */\n setIdentifyState: (state: IdentifyState) => void;\n setAttributeFilter: (filter: string) => void;\n}\n\nexport const createSessionSlice: SliceCreator<SessionSlice> = (set) => ({\n hoverTooltipsEnabled: true,\n setHoverTooltipsEnabled: (enabled) => set({ hoverTooltipsEnabled: enabled }),\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n identifyLayerId: null,\n identifyLayerIds: null,\n featureSelectionActive: false,\n setFeatureSelectionActive: (active) => set({ featureSelectionActive: active }),\n pointerCoords: null,\n pointerElevation: null,\n cameraAltitude: null,\n gpsStatus: null,\n attributeFilter: \"\",\n\n setPointerCoords: (coords) =>\n set(coords ? { pointerCoords: coords } : { pointerCoords: null, pointerElevation: null }),\n setPointerElevation: (elevation) => set({ pointerElevation: elevation }),\n setCameraAltitude: (altitude) => set({ cameraAltitude: altitude }),\n setGpsStatus: (fix) => set({ gpsStatus: fix }),\n\n selectLayer: (id) =>\n set({\n selectedLayerId: id,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n }),\n // `\"\"` is a valid feature id; only `null` clears the selection.\n selectFeature: (id) =>\n set({ selectedFeatureId: id, selectedFeatureIds: id === null ? [] : [id] }),\n selectFeatures: (ids, anchorId) =>\n set({\n selectedFeatureIds: ids,\n // Enforce the documented invariant for every caller: the anchor is\n // always a member of the set (or null when empty). A supplied anchor\n // that isn't in `ids` falls back to the last id rather than pointing\n // the map fit / calculator sample at an unselected feature.\n selectedFeatureId:\n anchorId != null && ids.includes(anchorId) ? anchorId : (ids.at(-1) ?? null),\n }),\n setIdentifyLayer: (id) => set({ identifyLayerId: id, identifyLayerIds: null }),\n setIdentifyState: ({ identifyLayerId, identifyLayerIds }) =>\n set({ identifyLayerId, identifyLayerIds }),\n setAttributeFilter: (filter) => set({ attributeFilter: filter }),\n});\n","/**\n * Reference scrubbing shared by the actions that delete layers (`removeLayer`\n * and `removeLayerGroup` with `removeChildren`), so a deleted layer never\n * leaves a dangling id in a story map, a secondary pane, or a join.\n */\nimport { cascadeLayerJoinRefresh } from \"../joins\";\nimport { removedLayerIdSet } from \"../layer-ref-scrub\";\nimport type { GeoLibreLayer, SecondaryMapView, StoryMap } from \"../types\";\n\n/**\n * Strip storymap chapter enter/exit opacity rows that reference any of the\n * removed layer ids. Returns the same reference when nothing changes so\n * callers can avoid unnecessary storymap churn.\n */\nexport function scrubStorymapLayerRefs(\n storymap: StoryMap | null,\n layerIds: string | Iterable<string>,\n): StoryMap | null {\n if (!storymap) return null;\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return storymap;\n let changed = false;\n const chapters = storymap.chapters.map((chapter) => {\n const onChapterEnter = chapter.onChapterEnter.filter((change) => !removed.has(change.layerId));\n const onChapterExit = chapter.onChapterExit.filter((change) => !removed.has(change.layerId));\n if (\n onChapterEnter.length === chapter.onChapterEnter.length &&\n onChapterExit.length === chapter.onChapterExit.length\n ) {\n return chapter;\n }\n changed = true;\n return { ...chapter, onChapterEnter, onChapterExit };\n });\n return changed ? { ...storymap, chapters } : storymap;\n}\n\n/**\n * Drop per-pane visibility overrides for removed layer ids so stale keys do\n * not accumulate (or serialize) in secondary panes after deletion.\n */\nexport function scrubSecondaryPaneLayerVisibility(\n panes: SecondaryMapView[],\n layerIds: string | Iterable<string>,\n): SecondaryMapView[] {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return panes;\n let anyChanged = false;\n const next = panes.map((pane) => {\n let changed = false;\n const layerVisibility: Record<string, boolean> = {};\n for (const [id, visible] of Object.entries(pane.layerVisibility)) {\n if (removed.has(id)) {\n changed = true;\n continue;\n }\n layerVisibility[id] = visible;\n }\n if (!changed) return pane;\n anyChanged = true;\n return { ...pane, layerVisibility };\n });\n return anyChanged ? next : panes;\n}\n\n/** Re-derive layers whose joins consumed any of the removed sources. */\nexport function cascadeJoinRefreshForRemoved(\n layers: GeoLibreLayer[],\n layerIds: string | Iterable<string>,\n): GeoLibreLayer[] {\n let current = layers;\n for (const id of removedLayerIdSet(layerIds)) {\n current = cascadeLayerJoinRefresh(current, id);\n }\n return current;\n}\n","/**\n * Layer-panel folders: create, rename, nest, reorder, sort and delete groups,\n * and move layers between them. `layerGroups` is tracked by undo history\n * except for the `collapsed` flag (see `undo-history.ts`).\n */\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n DEFAULT_LAYER_GROUP_OPACITY,\n normalizeGroupContiguity,\n reorderLayerGroupInPanel,\n sortLayerGroupInPanel,\n type LayerGroupSortOrder,\n} from \"../layer-groups\";\nimport {\n scrubCommentsForRemovedLayers,\n scrubLegendForRemovedLayers,\n scrubWidgetsForRemovedLayers,\n} from \"../layer-ref-scrub\";\nimport { scrubPrintLayoutForRemovedLayers } from \"../print-layout-config\";\nimport { identifyStateWithoutLayers } from \"./session-slice\";\nimport type { LayerGroup } from \"../types\";\nimport {\n cascadeJoinRefreshForRemoved,\n scrubSecondaryPaneLayerVisibility,\n scrubStorymapLayerRefs,\n} from \"./layer-removal\";\nimport type { SliceCreator } from \"./types\";\n\n/**\n * Pick the lowest `Group N` name not already taken, so default names stay\n * unique while still preferring small numbers — starting the search at 1 (not\n * `length + 1`) avoids skipping free low numbers when some groups carry custom\n * names. Group counts are small, so the linear scan is negligible.\n */\nfunction nextDefaultGroupName(groups: LayerGroup[]): string {\n const existing = new Set(groups.map((g) => g.name));\n let n = 1;\n while (existing.has(`Group ${n}`)) n++;\n return `Group ${n}`;\n}\n\nexport interface LayerGroupsSlice {\n layerGroups: LayerGroup[];\n\n addLayerGroup: (name?: string, layerIds?: string[]) => string;\n removeLayerGroup: (id: string, options?: { removeChildren?: boolean }) => void;\n renameLayerGroup: (id: string, name: string) => void;\n setLayerGroupVisibility: (id: string, visible: boolean) => void;\n setLayerGroupOpacity: (id: string, opacity: number) => void;\n toggleLayerGroupCollapsed: (id: string) => void;\n moveLayerToGroup: (\n layerId: string,\n groupId: string | null,\n beforeLayerId?: string | null,\n ) => void;\n moveLayersToGroup: (\n layerIds: string[],\n groupId: string | null,\n beforeLayerId?: string | null,\n ) => void;\n moveLayerGroupToGroup: (id: string, parentId: string | null) => void;\n reorderLayerGroup: (id: string, direction: \"up\" | \"down\") => void;\n /**\n * Sort a group's direct children by name, A to Z or Z to A (top of panel\n * first), collating by `locale` (the app's display language) when given.\n */\n sortLayerGroup: (id: string, order: LayerGroupSortOrder, locale?: string) => void;\n}\n\nexport const createLayerGroupsSlice: SliceCreator<LayerGroupsSlice> = (set, get) => ({\n layerGroups: [],\n\n addLayerGroup: (name, layerIds) => {\n const id = uuidv4();\n set((s) => {\n const group: LayerGroup = {\n id,\n name: name?.trim() || nextDefaultGroupName(s.layerGroups),\n collapsed: false,\n visible: true,\n opacity: DEFAULT_LAYER_GROUP_OPACITY,\n };\n const ids = new Set(layerIds ?? []);\n const layers =\n ids.size > 0\n ? normalizeGroupContiguity(\n s.layers.map((l) => (ids.has(l.id) ? { ...l, groupId: id } : l)),\n )\n : s.layers;\n return {\n layers,\n layerGroups: [...s.layerGroups, group],\n isDirty: true,\n };\n });\n return id;\n },\n\n removeLayerGroup: (id, options) =>\n set((s) => {\n const removeChildren = options?.removeChildren ?? false;\n const removedGroup = s.layerGroups.find((g) => g.id === id);\n if (!removedGroup) return s;\n const groupIds = new Set([id]);\n if (removeChildren) {\n let changed = true;\n while (changed) {\n changed = false;\n for (const group of s.layerGroups) {\n if (group.parentId && groupIds.has(group.parentId) && !groupIds.has(group.id)) {\n groupIds.add(group.id);\n changed = true;\n }\n }\n }\n }\n const removedIds = new Set(\n s.layers.filter((l) => l.groupId && groupIds.has(l.groupId)).map((l) => l.id),\n );\n let layers = removeChildren\n ? s.layers.filter((l) => !l.groupId || !groupIds.has(l.groupId))\n : s.layers.map((l) => (l.groupId === id ? { ...l, groupId: undefined } : l));\n // Match removeLayer: refreshing joins and scrubbing secondary-pane /\n // storymap refs for every deleted child so group delete cannot leave\n // stale joined columns or dangling visibility overrides behind.\n if (removeChildren && removedIds.size > 0) {\n layers = cascadeJoinRefreshForRemoved(layers, removedIds);\n }\n const selectionRemoved =\n removeChildren && s.selectedLayerId !== null && removedIds.has(s.selectedLayerId);\n return {\n layers,\n layerGroups: s.layerGroups\n .filter((g) => !groupIds.has(g.id))\n .map((g) => (g.parentId === id ? { ...g, parentId: removedGroup.parentId } : g)),\n secondaryMapViews: removeChildren\n ? scrubSecondaryPaneLayerVisibility(s.secondaryMapViews, removedIds)\n : s.secondaryMapViews,\n storymap: removeChildren ? scrubStorymapLayerRefs(s.storymap, removedIds) : s.storymap,\n widgets: removeChildren ? scrubWidgetsForRemovedLayers(s.widgets, removedIds) : s.widgets,\n comments: removeChildren\n ? scrubCommentsForRemovedLayers(s.comments, removedIds)\n : s.comments,\n legend: removeChildren ? scrubLegendForRemovedLayers(s.legend, removedIds) : s.legend,\n printLayout: removeChildren\n ? scrubPrintLayoutForRemovedLayers(s.printLayout, removedIds)\n : s.printLayout,\n selectedLayerId: selectionRemoved\n ? (layers[layers.length - 1]?.id ?? null)\n : s.selectedLayerId,\n selectedFeatureId: selectionRemoved ? null : s.selectedFeatureId,\n selectedFeatureIds: selectionRemoved ? [] : s.selectedFeatureIds,\n // Children kept at the top level are still in the project, so they\n // keep their place in the Identify target.\n ...(removeChildren\n ? identifyStateWithoutLayers(s, removedIds)\n : { identifyLayerId: s.identifyLayerId, identifyLayerIds: s.identifyLayerIds }),\n ui: removeChildren\n ? {\n ...s.ui,\n selectByExpressionLayerId:\n s.ui.selectByExpressionLayerId !== null &&\n removedIds.has(s.ui.selectByExpressionLayerId)\n ? null\n : s.ui.selectByExpressionLayerId,\n selectByLocationLayerId:\n s.ui.selectByLocationLayerId !== null &&\n removedIds.has(s.ui.selectByLocationLayerId)\n ? null\n : s.ui.selectByLocationLayerId,\n loadEditorFeaturesLayerId:\n s.ui.loadEditorFeaturesLayerId !== null &&\n removedIds.has(s.ui.loadEditorFeaturesLayerId)\n ? null\n : s.ui.loadEditorFeaturesLayerId,\n }\n : s.ui,\n isDirty: true,\n };\n }),\n\n renameLayerGroup: (id, name) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, name } : g)),\n isDirty: true,\n })),\n\n setLayerGroupVisibility: (id, visible) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, visible } : g)),\n isDirty: true,\n })),\n\n setLayerGroupOpacity: (id, opacity) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) =>\n g.id === id ? { ...g, opacity: Math.min(Math.max(opacity, 0), 1) } : g,\n ),\n isDirty: true,\n })),\n\n // Collapsing/expanding a folder is a UI-panel preference, not a data\n // edit: it is still persisted in the project (folders reopen collapsed),\n // but it does not mark the project dirty and is excluded from undo (see\n // the equality comparator in undo-history.ts) so Ctrl-Z never toggles a\n // folder.\n toggleLayerGroupCollapsed: (id) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, collapsed: !g.collapsed } : g)),\n })),\n\n moveLayerToGroup: (layerId, groupId, beforeLayerId = null) =>\n get().moveLayersToGroup([layerId], groupId, beforeLayerId),\n\n moveLayersToGroup: (layerIds, groupId, beforeLayerId = null) =>\n set((s) => {\n if (groupId && !s.layerGroups.some((g) => g.id === groupId)) return s;\n const requestedIds = new Set(layerIds);\n const moving = s.layers.filter(\n (layer) =>\n requestedIds.has(layer.id) &&\n (beforeLayerId !== null || (layer.groupId ?? null) !== groupId),\n );\n if (moving.length === 0) return s;\n const movingIds = new Set(moving.map((layer) => layer.id));\n const without = s.layers.filter((layer) => !movingIds.has(layer.id));\n const updated = moving.map((layer) => ({\n ...layer,\n groupId: groupId ?? undefined,\n }));\n let index: number;\n if (beforeLayerId && !movingIds.has(beforeLayerId)) {\n const at = without.findIndex((layer) => layer.id === beforeLayerId);\n index = at < 0 ? without.length : at;\n } else if (groupId) {\n let last = -1;\n without.forEach((layer, layerIndex) => {\n if (layer.groupId === groupId) last = layerIndex;\n });\n index = last < 0 ? without.length : last + 1;\n } else {\n index = without.length;\n }\n const next = [...without];\n next.splice(index, 0, ...updated);\n const normalized = normalizeGroupContiguity(next);\n const unchanged = normalized.every(\n (layer, layerIndex) =>\n layer.id === s.layers[layerIndex]?.id && layer.groupId === s.layers[layerIndex]?.groupId,\n );\n if (unchanged) return s;\n return { layers: normalized, isDirty: true };\n }),\n\n moveLayerGroupToGroup: (id, parentId) =>\n set((s) => {\n if (parentId === id) return s;\n const group = s.layerGroups.find((g) => g.id === id);\n if (!group) return s;\n if (parentId && !s.layerGroups.some((g) => g.id === parentId)) return s;\n // Walking upward from the proposed parent must never reach the group\n // being moved, otherwise the assignment would create a cycle.\n const byId = new Map(s.layerGroups.map((g) => [g.id, g]));\n let ancestorId = parentId ?? undefined;\n const visited = new Set<string>();\n while (ancestorId && !visited.has(ancestorId)) {\n if (ancestorId === id) return s;\n visited.add(ancestorId);\n ancestorId = byId.get(ancestorId)?.parentId;\n }\n const nextParentId = parentId ?? undefined;\n if (group.parentId === nextParentId) return s;\n return {\n layerGroups: s.layerGroups.map((g) => (g.id === id ? { ...g, parentId: nextParentId } : g)),\n isDirty: true,\n };\n }),\n\n // A folder that owns no layers has no position in `layers` to move, so\n // the panel order it takes part in lives across both arrays and the move\n // writes both (GeoLibre#1739).\n reorderLayerGroup: (id, direction) =>\n set((s) => {\n const moved = reorderLayerGroupInPanel(s.layers, s.layerGroups, id, direction);\n if (!moved) return s;\n return { layers: moved.layers, layerGroups: moved.groups, isDirty: true };\n }),\n\n sortLayerGroup: (id, order, locale) =>\n set((s) => {\n const sorted = sortLayerGroupInPanel(s.layers, s.layerGroups, id, order, locale);\n if (!sorted) return s;\n return { layers: sorted.layers, layerGroups: sorted.groups, isDirty: true };\n }),\n});\n","/**\n * A bare Android SAF document ID, as the browser `File.name` carries after a\n * native import: a known volume prefix (`primary:`, `home:`, `raw:`, `msf:`, or a\n * removable `XXXX-XXXX:` volume), or any provider prefix followed by a folder\n * separator. Ordinary encoded filenames such as `Report%3AQ1.geojson` do not match.\n */\nconst SAF_DOCUMENT_ID =\n /^(?:(?:primary|home|raw|msf|[0-9a-f]{4}-[0-9a-f]{4})%3a|[^%/\\\\:]+%3a.*%2f)/i;\n\n/** Extract a display filename without changing the source used for file access. */\nexport function localFileName(path: string): string {\n const isUri = /^(?:content|file):\\/\\//i.test(path);\n const value = isUri ? path.split(/[?#]/)[0] : path;\n const segment = value.split(/[/\\\\]/).pop() ?? value;\n // Do not decode ordinary filenames: percent escapes can be literal text.\n if (!isUri && (!SAF_DOCUMENT_ID.test(segment) || /[/\\\\]/.test(path))) {\n return segment;\n }\n let decoded = segment;\n try {\n decoded = decodeURIComponent(segment);\n } catch {\n return segment;\n }\n // Strip the provider's volume prefix before taking the final path component.\n return (\n decoded\n .replace(/^[^/\\\\:]+:/, \"\")\n .split(/[/\\\\]/)\n .pop() ?? decoded\n );\n}\n\n/**\n * Whether a layer name was derived from its local source filename (with or\n * without the extension) rather than supplied explicitly by a caller.\n */\nexport function isSourceDerivedLayerName(name: string, sourcePath: string): boolean {\n const fileName = localFileName(sourcePath);\n return name === fileName || name === fileName.replace(/\\.[^.]+$/, \"\");\n}\n\n/** Keep imported layers distinguishable, including within a multi-file import. */\nexport function uniqueImportedLayerName(name: string, names: Iterable<string>): string {\n const existing = new Set(names);\n let candidate = name;\n let suffix = 2;\n while (existing.has(candidate)) candidate = `${name}_${suffix++}`;\n return candidate;\n}\n","import type { FeatureCollection } from \"geojson\";\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer, type LayerStyle } from \"./types\";\n\n/**\n * Initial styling for a newly added vector layer (issue #1519).\n *\n * {@link DEFAULT_LAYER_STYLE} stays exactly what it is — the schema fallback\n * that fills gaps when a partial style is read back from a project file. It is\n * deliberately NOT changed here: every load path spreads it under a saved\n * style, so editing it would silently restyle existing projects.\n *\n * What this module adds is a separate decision made only at *add* time: give\n * each new layer its own color, and pick sizes that suit the geometry it\n * actually contains, so a stack of freshly loaded layers is legible without\n * anyone opening the Style panel.\n */\n\n/**\n * Qualitative colors cycled across newly added layers, chosen to stay distinct\n * at low opacity over both light and dark basemaps.\n *\n * Distinct from the classification palette the Style panel uses for the classes\n * *within* one layer: this one separates layers from each other, and the two\n * are free to diverge.\n */\nexport const LAYER_PALETTE = [\n \"#3b82f6\", // blue — matches the historical default, so the first layer looks unchanged\n \"#ef4444\", // red\n \"#22c55e\", // green\n \"#a855f7\", // purple\n \"#f97316\", // orange\n \"#06b6d4\", // cyan\n \"#ec4899\", // pink\n \"#84cc16\", // lime\n] as const;\n\n/** How much darker an outline is than its fill (0 = black, 1 = unchanged). */\nconst STROKE_DARKEN = 0.55;\n\n/** Darken a `#rrggbb` color toward black by `factor`. */\nexport function darkenHex(hex: string, factor: number): string {\n const match = /^#([0-9a-f]{6})$/i.exec(hex.trim());\n if (!match) return hex;\n const value = Number.parseInt(match[1], 16);\n const channels = [(value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff];\n return `#${channels\n .map((channel) =>\n Math.max(0, Math.min(255, Math.round(channel * factor)))\n .toString(16)\n .padStart(2, \"0\"),\n )\n .join(\"\")}`;\n}\n\n/** Case-insensitive palette lookup, returning the canonical (lowercase) entry. */\nfunction paletteEntryOf(color: string | undefined): string | undefined {\n const normalized = (color ?? \"\").trim().toLowerCase();\n return LAYER_PALETTE.find((entry) => entry === normalized);\n}\n\n/**\n * True when a style's colors are a palette assignment {@link initialLayerStyle}\n * made: a palette fill *paired with* the outline derived from it.\n *\n * The pairing is what makes this precise. Testing the fill alone — or the layer\n * type — would miscount every layer built straight from\n * {@link DEFAULT_LAYER_STYLE}, whose `fillColor` happens to be\n * `LAYER_PALETTE[0]`: rasters, tilesets, video overlays, and the non-spatial\n * attribute tables the delimited-text source adds. None of those paint from a\n * collection, so none should reserve blue and push the next added layer to red.\n * They carry the schema's own `strokeColor`, not one derived from the fill, so\n * this test leaves them out.\n */\nfunction wearsPaletteColor(style: LayerStyle | undefined): boolean {\n const entry = paletteEntryOf(style?.fillColor);\n if (!entry) return false;\n return (style?.strokeColor ?? \"\").trim().toLowerCase() === darkenHex(entry, STROKE_DARKEN);\n}\n\n/**\n * Pick the next layer color: the first palette entry no current layer is\n * already using, so deleting a layer frees its color back up rather than\n * leaving the cycle permanently offset. Falls back to cycling by layer count\n * once every entry is in use.\n *\n * @param layers - The project's current layers. Only those actually wearing a\n * palette assignment count; see {@link wearsPaletteColor}.\n * @returns A `#rrggbb` color from {@link LAYER_PALETTE}.\n */\nexport function nextLayerPaletteColor(layers: readonly GeoLibreLayer[]): string {\n const styled = layers.filter((layer) => wearsPaletteColor(layer.style));\n const used = new Set(styled.map((layer) => layer.style.fillColor.trim().toLowerCase()));\n const free = LAYER_PALETTE.find((color) => !used.has(color));\n return free ?? LAYER_PALETTE[styled.length % LAYER_PALETTE.length];\n}\n\n/** The geometry family a layer is mostly made of. */\nexport type DominantGeometry = \"point\" | \"line\" | \"polygon\" | \"mixed\";\n\n/** Features sampled when deciding a collection's dominant geometry. */\nconst GEOMETRY_SAMPLE_SIZE = 500;\n\n/**\n * Classify a collection by the geometry family most of its features carry,\n * reading at most {@link GEOMETRY_SAMPLE_SIZE} features so a very large layer\n * does not pay a full scan for a styling default.\n *\n * @param geojson - The collection to inspect.\n * @returns The dominant family, or `\"mixed\"` when nothing has a clear majority\n * (or the collection is empty).\n */\nexport function dominantGeometry(geojson: FeatureCollection | undefined): DominantGeometry {\n const features = geojson?.features ?? [];\n if (!features.length) return \"mixed\";\n\n const counts = { point: 0, line: 0, polygon: 0 };\n const sampled = Math.min(features.length, GEOMETRY_SAMPLE_SIZE);\n for (let index = 0; index < sampled; index++) {\n switch (features[index]?.geometry?.type) {\n case \"Point\":\n case \"MultiPoint\":\n counts.point += 1;\n break;\n case \"LineString\":\n case \"MultiLineString\":\n counts.line += 1;\n break;\n case \"Polygon\":\n case \"MultiPolygon\":\n counts.polygon += 1;\n break;\n default:\n break;\n }\n }\n\n if (!counts.point && !counts.line && !counts.polygon) return \"mixed\";\n const [family, count] = Object.entries(counts).sort((a, b) => b[1] - a[1])[0] as [\n \"point\" | \"line\" | \"polygon\",\n number,\n ];\n // A clear majority of everything sampled, not just of the geometries this\n // switch understands: two points among three GeometryCollections is not a\n // point layer, so `sampled` is the denominator rather than the matched total.\n // And a majority, not merely the largest slice — a 40/35/25 split has no\n // geometry whose sizing suits the layer.\n return count / sampled > 0.5 ? family : \"mixed\";\n}\n\n/**\n * Per-geometry size and opacity overrides.\n *\n * One set of numbers cannot suit all three families: an opacity that lets a\n * basemap read through a polygon makes a point look washed out, and a stroke\n * heavy enough to see a line is a heavy outline on a polygon.\n */\nconst GEOMETRY_DEFAULTS: Record<DominantGeometry, Partial<LayerStyle>> = {\n // Solid and slightly smaller: a point is read as a dot, not a translucent blob.\n point: { circleRadius: 5, fillOpacity: 0.9, strokeWidth: 1 },\n // A line's stroke *is* the symbol, so it carries the weight.\n line: { strokeWidth: 2.5, fillOpacity: 1 },\n // Translucent enough that the basemap underneath stays readable.\n polygon: { fillOpacity: 0.45, strokeWidth: 1.5 },\n mixed: {},\n};\n\nexport interface InitialLayerStyleOptions {\n /** The layer's data, used to pick geometry-appropriate sizes. */\n geojson?: FeatureCollection;\n /** Current project layers, so the new layer gets an unused palette color. */\n layers?: readonly GeoLibreLayer[];\n /** Style values that win over the computed defaults (e.g. a restored style). */\n overrides?: Partial<LayerStyle>;\n}\n\n/**\n * Build the style a newly added vector layer starts with: the schema defaults,\n * plus its own palette color and geometry-appropriate sizing.\n *\n * @param options - Data and project context to derive the style from.\n * @returns A complete {@link LayerStyle}.\n */\nexport function initialLayerStyle(options: InitialLayerStyleOptions = {}): LayerStyle {\n const { geojson, layers = [], overrides } = options;\n const fillColor = nextLayerPaletteColor(layers);\n return {\n ...DEFAULT_LAYER_STYLE,\n fillColor,\n strokeColor: darkenHex(fillColor, STROKE_DARKEN),\n ...GEOMETRY_DEFAULTS[dominantGeometry(geojson)],\n ...overrides,\n };\n}\n\n/**\n * True when a layer still wears exactly what {@link initialLayerStyle} gave it.\n *\n * The renderer mode alone is too weak a test for \"untouched\": nudging the fill,\n * outline, or opacity leaves the mode on `\"single\"`, and a project restored\n * from disk can carry deliberate single-symbol styling. Both cases should stop\n * the Style panel volunteering suggestions, which is what this gates.\n *\n * The fill color cannot be compared against a fixed value — it is whichever\n * palette entry the layer drew — so the test is that it is *still a palette\n * entry* with the outline derived from it, which a hand-picked color will not\n * satisfy.\n *\n * @param style - The layer's current style.\n * @param geojson - Its data, so the geometry-derived sizes can be recomputed.\n * @returns `true` when nothing about the as-added look has been changed.\n */\nexport function isInitialLayerStyle(style: LayerStyle, geojson?: FeatureCollection): boolean {\n // Authored symbology, whatever the paint fields say.\n if (style.simpleStyleEnabled) return false;\n if (style.vectorStyleMode !== \"single\") return false;\n if (style.pointRenderer !== \"single\") return false;\n if ((style.vectorRules?.length ?? 0) > 0) return false;\n\n if (!wearsPaletteColor(style)) return false;\n\n const expected = { ...DEFAULT_LAYER_STYLE, ...GEOMETRY_DEFAULTS[dominantGeometry(geojson)] };\n return (\n style.fillOpacity === expected.fillOpacity &&\n style.strokeWidth === expected.strokeWidth &&\n style.circleRadius === expected.circleRadius\n );\n}\n","// Copy/paste layer symbology between layers (issue #1339), QGIS-style. The\n// transient clipboard slot and its two store actions live in `store.ts`; this\n// module holds the UI-free data layer so both the desktop app and tests consume\n// the same extract/apply logic.\n\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer, type LayerStyle } from \"./types\";\nimport { isStyleLibraryTargetLayer } from \"./style-library\";\n\n/**\n * `metadata.sourceKind` marking a deck.gl COG raster managed by the\n * maplibre-gl-raster control. Mirrors `RASTER_SOURCE_KIND` in\n * `@geolibre/plugins` (which `@geolibre/core` must not depend on). The clipboard\n * test imports the real constant directly from the module that defines it\n * (`packages/plugins/src/plugins/raster-layer-sync.ts`, not the package barrel,\n * whose browser-only plugins crash under the Node test runner) and asserts a\n * layer tagged with it classifies as `\"raster\"`, so a rename of the upstream\n * constant makes the test fail here rather than silently dropping raster\n * copy/paste.\n */\nconst RASTER_SOURCE_KIND = \"maplibre-gl-raster\";\n\n/**\n * Which copy/paste style family a layer belongs to. Vector-styled layers and\n * deck.gl rasters store their symbology in different places (the {@link\n * LayerStyle} bag vs `metadata.rasterState`), so a paste only applies between\n * layers of the same family.\n */\nexport type LayerStyleClipboardKind = \"vector\" | \"raster\";\n\n/**\n * The `metadata.rasterState` keys a raster style paste carries over: the\n * appearance of the render (colormap, stretch range, gamma, nodata), but not\n * the data selection (`mode`/`bands`/`index`), which stays with the target so\n * pasting a style never points a raster at a band it does not have.\n */\nexport const RASTER_APPEARANCE_STATE_KEYS = [\n \"colormap\",\n \"reversed\",\n \"rescale\",\n \"nodata\",\n \"stretch\",\n \"gamma\",\n] as const;\n\n/**\n * Fields every clipboard entry carries, regardless of style family. Layer\n * `opacity` is deliberately excluded: like QGIS \"Paste Style\" and GeoLibre's\n * own Style Manager, copy/paste transfers symbology only and leaves the\n * target's opacity alone (opacity is a per-layer render setting, not style).\n *\n * {@link LayerStyle.blendMode} is deliberately *included*, even though the\n * Layers panel puts the Blend menu beside the opacity slider. The two differ in\n * kind despite sitting together: opacity is a transient \"turn this down so I\n * can see through it\" knob, while a blend mode is a cartographic decision about\n * how the layer reads against the map -- colour over a hillshade is a look, and\n * copying a look should bring it. QGIS agrees; its layer style, and so its\n * Paste Style, carries the blending mode. `tests/layer-style-clipboard.test.ts`\n * pins both halves of this so neither is changed by accident.\n */\ninterface CopiedLayerStyleBase {\n /** Source layer name, surfaced in the paste tooltip and status message. */\n sourceName: string;\n}\n\n/** A copied vector layer style: the whole {@link LayerStyle} bag. */\nexport interface CopiedVectorStyle extends CopiedLayerStyleBase {\n kind: \"vector\";\n /** Deep-cloned full vector layer style. */\n style: LayerStyle;\n}\n\n/**\n * A copied deck.gl raster style: the `metadata.rasterState` appearance and the\n * classification symbology. There is no `style` field — the vector\n * {@link LayerStyle} bag holds nothing a deck.gl raster renders from.\n */\nexport interface CopiedRasterStyle extends CopiedLayerStyleBase {\n kind: \"raster\";\n /** Raster visualization state (`metadata.rasterState`). */\n rasterState?: Record<string, unknown>;\n /** Whether the source carried a `metadata.rasterSymbology`. */\n hasRasterSymbology: boolean;\n /** Raster classification symbology (`metadata.rasterSymbology`). */\n rasterSymbology?: unknown;\n}\n\n/**\n * A layer's symbology captured for the paste clipboard. A discriminated union\n * on {@link LayerStyleClipboardKind} so raster-only fields cannot be read off a\n * vector entry (or vice versa); a paste only applies between layers of the same\n * family.\n */\nexport type CopiedLayerStyle = CopiedVectorStyle | CopiedRasterStyle;\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * The copy/paste style family of a layer, or `null` when the layer has no\n * copyable symbology. Deck.gl rasters are `\"raster\"`; the vector-styled layers\n * the Style Manager targets are `\"vector\"`.\n *\n * @param layer - The layer to classify.\n * @returns The style family, or `null`.\n */\nexport function copyableLayerStyleKind(layer: GeoLibreLayer): LayerStyleClipboardKind | null {\n if (layer.metadata.sourceKind === RASTER_SOURCE_KIND) return \"raster\";\n if (isStyleLibraryTargetLayer(layer.type)) return \"vector\";\n return null;\n}\n\n/**\n * Snapshot a layer's style for the clipboard, deep-cloned so later edits to the\n * live layer cannot mutate the copied entry.\n *\n * @param layer - The source layer to copy from.\n * @returns The clipboard entry, or `null` when the layer has no copyable\n * symbology.\n */\nexport function extractCopiedLayerStyle(layer: GeoLibreLayer): CopiedLayerStyle | null {\n const kind = copyableLayerStyleKind(layer);\n if (!kind) return null;\n if (kind === \"vector\") {\n return {\n kind,\n sourceName: layer.name,\n style: structuredClone({ ...DEFAULT_LAYER_STYLE, ...layer.style }),\n };\n }\n // A raster entry never carries the vector `style` bag, so the full-style\n // clone is skipped here.\n const rasterState = layer.metadata.rasterState;\n const rasterSymbology = layer.metadata.rasterSymbology;\n // Only a real symbology object counts as \"has symbology\". A missing or null\n // value (the latter only reachable via a hand-edited project file) both mean\n // \"none\", so the paste clears the target's symbology rather than writing a\n // literal null onto it — symmetric with the delete path in applyCopiedLayerStyle.\n const hasRasterSymbology = isPlainObject(rasterSymbology);\n return {\n kind,\n sourceName: layer.name,\n rasterState: isPlainObject(rasterState) ? structuredClone(rasterState) : undefined,\n hasRasterSymbology,\n ...(hasRasterSymbology ? { rasterSymbology: structuredClone(rasterSymbology) } : {}),\n };\n}\n\n/**\n * Build the {@link GeoLibreLayer} patch that pastes a copied style onto a\n * target layer, or `null` when the target's style family does not match the\n * clipboard entry's (so the caller can disable the paste action).\n *\n * @param target - The layer to paste onto.\n * @param copied - The clipboard entry.\n * @returns A partial layer patch to merge, or `null` when incompatible.\n */\nexport function applyCopiedLayerStyle(\n target: GeoLibreLayer,\n copied: CopiedLayerStyle,\n): Partial<GeoLibreLayer> | null {\n if (copyableLayerStyleKind(target) !== copied.kind) return null;\n if (copied.kind === \"vector\") {\n // The whole style is applied verbatim, including attribute-bound fields\n // (`vectorStyleProperty` and its stops/rules, `labels.field`,\n // `extrusionHeightProperty`, `proportionalSizeProperty`, `diagramFields`).\n // This is deliberate QGIS \"Paste Style\" behavior and matches the Style\n // Manager's full-\"style\" preset: a target lacking those attributes just\n // renders that facet inert (no classification / labels), which the user\n // re-points, rather than the paste silently dropping parts of the style.\n return { style: structuredClone(copied.style) };\n }\n // Merge the appearance keys onto the target's existing rasterState, keeping\n // its own `mode`/`bands`/`index` so the paste restyles the layer without\n // repointing it at the source's band selection.\n const targetState = isPlainObject(target.metadata.rasterState) ? target.metadata.rasterState : {};\n const source = copied.rasterState ?? {};\n const expectedRescaleLength = rescaleEntryCount(targetState);\n const mergedState: Record<string, unknown> = { ...targetState };\n for (const key of RASTER_APPEARANCE_STATE_KEYS) {\n if (!(key in source)) continue;\n // `rescale` is a per-channel min/max array whose length is set by the\n // render mode (one entry for single/index, one per band for rgb), not by\n // `bands.length` (index mode holds two operand bands but a single-entry\n // rescale). Copying a rescale whose length disagrees with the target's mode\n // — e.g. an RGB source's 3 entries onto a single-band target — would leave a\n // rescale that no longer matches the preserved render mode, the same \"band\n // it does not have\" hazard one level down. Skip it then and let the target\n // keep its own stretch. A null source rescale (auto) is safe to carry over.\n if (\n key === \"rescale\" &&\n Array.isArray(source.rescale) &&\n source.rescale.length !== expectedRescaleLength\n ) {\n continue;\n }\n mergedState[key] = structuredClone(source[key]);\n }\n const metadata: Record<string, unknown> = { ...target.metadata, rasterState: mergedState };\n if (copied.hasRasterSymbology) {\n metadata.rasterSymbology = structuredClone(copied.rasterSymbology);\n } else {\n delete metadata.rasterSymbology;\n }\n return { metadata };\n}\n\n/**\n * Number of `rescale` entries a rasterState of this render mode expects: one\n * per band for `\"rgb\"`, a single entry for `\"single\"` and `\"index\"` (the\n * latter stretches one derived index even though it reads two operand bands).\n */\nfunction rescaleEntryCount(state: Record<string, unknown>): number {\n if (state.mode === \"rgb\") return Array.isArray(state.bands) ? state.bands.length : 3;\n return 1;\n}\n","// Export every layer's style to one JSON file and apply such a file back by\n// layer name: Project → Export → Export Layer Styles, Project → Import →\n// Import Layer Styles, and the Startup setting that restyles each newly added\n// layer whose name matches an entry. A style here is exactly what copy/paste\n// style carries (see `layer-style-clipboard.ts`), so an exported entry restyles\n// a layer the same way pasting a copied style onto it would.\n\nimport {\n applyCopiedLayerStyle,\n type CopiedLayerStyle,\n copyableLayerStyleKind,\n extractCopiedLayerStyle,\n type LayerStyleClipboardKind,\n RASTER_APPEARANCE_STATE_KEYS,\n} from \"./layer-style-clipboard\";\nimport { sanitizeLayerStylePatch } from \"./style-library\";\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer, type LayerStyle } from \"./types\";\n\n/** `type` tag identifying a layer styles file. */\nexport const LAYER_STYLES_FILE_TYPE = \"geolibre-layer-styles\";\n\n/** Format version written by {@link serializeLayerStylesFile}. */\nexport const LAYER_STYLES_FILE_VERSION = 1;\n\n/** One layer's style in a layer styles file, keyed by the layer's name. */\nexport type LayerStyleFileEntry =\n | {\n /** Name of the layer the style was exported from; the match key. */\n layerName: string;\n kind: \"vector\";\n /** The full vector {@link LayerStyle} bag. */\n style: LayerStyle;\n }\n | {\n layerName: string;\n kind: \"raster\";\n /** Raster visualization state (`metadata.rasterState`). */\n rasterState?: Record<string, unknown>;\n /** Raster classification symbology (`metadata.rasterSymbology`). */\n rasterSymbology?: Record<string, unknown>;\n };\n\n/** The on-disk layer styles document. */\nexport interface LayerStylesFile {\n type: typeof LAYER_STYLES_FILE_TYPE;\n version: typeof LAYER_STYLES_FILE_VERSION;\n styles: LayerStyleFileEntry[];\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Capture the style of every layer that has one. Layers without copyable\n * symbology (basemap tiles, 3D tiles, ...) are skipped. Layers that share a\n * name are all written, in stack order; the first one wins when the file is\n * applied (see {@link findLayerStyleEntryIndex}).\n *\n * @param layers - The layers to export, in store (bottom-to-top) order.\n * @returns The style entries.\n */\nexport function extractLayerStyleEntries(layers: readonly GeoLibreLayer[]): LayerStyleFileEntry[] {\n const entries: LayerStyleFileEntry[] = [];\n for (const layer of layers) {\n const copied = extractCopiedLayerStyle(layer);\n if (!copied) continue;\n if (copied.kind === \"vector\") {\n entries.push({ layerName: layer.name, kind: \"vector\", style: copied.style });\n } else {\n entries.push({\n layerName: layer.name,\n kind: \"raster\",\n ...(copied.rasterState ? { rasterState: copied.rasterState } : {}),\n ...(isPlainObject(copied.rasterSymbology)\n ? { rasterSymbology: copied.rasterSymbology }\n : {}),\n });\n }\n }\n return entries;\n}\n\n/**\n * Serialize style entries into the JSON written by Export Layer Styles.\n *\n * @param entries - The entries to write.\n * @returns Pretty-printed file content.\n */\nexport function serializeLayerStylesFile(entries: readonly LayerStyleFileEntry[]): string {\n const file: LayerStylesFile = {\n type: LAYER_STYLES_FILE_TYPE,\n version: LAYER_STYLES_FILE_VERSION,\n styles: [...entries],\n };\n return JSON.stringify(file, null, 2);\n}\n\nconst RASTER_STRETCHES: readonly unknown[] = [\"linear\", \"log\", \"sqrt\"];\nconst RASTER_NODATA_MODES: readonly unknown[] = [\"off\", \"auto\"];\n\nfunction isFiniteNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\n/**\n * Validators for the raster appearance keys a style file may carry, by the\n * types `maplibre-gl-raster`'s `RasterLayerState` declares for them.\n */\nconst RASTER_APPEARANCE_VALIDATORS: Record<\n (typeof RASTER_APPEARANCE_STATE_KEYS)[number],\n (value: unknown) => boolean\n> = {\n colormap: (value) => typeof value === \"string\" && value.length > 0,\n reversed: (value) => typeof value === \"boolean\",\n rescale: (value) =>\n value === null ||\n (Array.isArray(value) &&\n // An empty array is not a meaningful rescale (as in savedRasterState).\n value.length > 0 &&\n value.every(\n (range) =>\n Array.isArray(range) &&\n range.length === 2 &&\n isFiniteNumber(range[0]) &&\n isFiniteNumber(range[1]),\n )),\n nodata: (value) => isFiniteNumber(value) || RASTER_NODATA_MODES.includes(value),\n stretch: (value) => RASTER_STRETCHES.includes(value),\n gamma: (value) => isFiniteNumber(value) && value > 0,\n};\n\n/**\n * Keep only the raster appearance keys a style applies, each with a value of\n * the type the raster renderer expects. The data selection (`mode`, `bands`,\n * ...) is dropped too: a style never applies it.\n */\nfunction sanitizeRasterAppearanceState(state: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const key of RASTER_APPEARANCE_STATE_KEYS) {\n if (key in state && RASTER_APPEARANCE_VALIDATORS[key](state[key])) {\n out[key] = structuredClone(state[key]);\n }\n }\n return out;\n}\n\n/**\n * Coerce untrusted entries (a hand-edited file, or a copy kept in settings)\n * into clean ones. An entry without a name or a known kind is dropped; a\n * vector style is sanitized and completed against the default style, and a\n * raster state keeps only well-typed appearance keys, so applying an entry\n * never hands a renderer an unknown, missing or wrong-typed field.\n *\n * @param value - The raw `styles` array.\n * @returns The usable entries.\n */\nexport function normalizeLayerStyleEntries(value: unknown): LayerStyleFileEntry[] {\n if (!Array.isArray(value)) return [];\n const entries: LayerStyleFileEntry[] = [];\n for (const item of value) {\n if (!isPlainObject(item)) continue;\n const layerName = typeof item.layerName === \"string\" ? item.layerName : \"\";\n if (!layerName.trim()) continue;\n if (item.kind === \"vector\") {\n if (!isPlainObject(item.style)) continue;\n entries.push({\n layerName,\n kind: \"vector\",\n style: { ...structuredClone(DEFAULT_LAYER_STYLE), ...sanitizeLayerStylePatch(item.style) },\n });\n } else if (item.kind === \"raster\") {\n entries.push({\n layerName,\n kind: \"raster\",\n ...(isPlainObject(item.rasterState)\n ? { rasterState: sanitizeRasterAppearanceState(item.rasterState) }\n : {}),\n // The symbology's schema lives in `@geolibre/plugins`, which validates\n // it on every read (`savedRasterSymbology`), as it does for a project\n // file; a malformed one renders as unclassified there.\n ...(isPlainObject(item.rasterSymbology)\n ? { rasterSymbology: structuredClone(item.rasterSymbology) }\n : {}),\n });\n }\n }\n return entries;\n}\n\n/**\n * Parse a file written by {@link serializeLayerStylesFile}.\n *\n * @param json - The file content.\n * @returns The normalized entries.\n * @throws Error when the content is not a layer styles file, comes from a\n * newer format version, or holds no usable entries.\n */\nexport function parseLayerStylesFile(json: string): LayerStyleFileEntry[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"Not a valid layer styles file (invalid JSON).\");\n }\n if (!isPlainObject(parsed) || parsed.type !== LAYER_STYLES_FILE_TYPE) {\n throw new Error(\"Not a valid layer styles file.\");\n }\n // Refuse a newer format rather than misread it with this version's rules.\n if (parsed.version !== LAYER_STYLES_FILE_VERSION) {\n throw new Error(\"Unsupported layer styles file version.\");\n }\n const entries = normalizeLayerStyleEntries(parsed.styles);\n if (entries.length === 0) {\n throw new Error(\"The layer styles file holds no usable styles.\");\n }\n return entries;\n}\n\nfunction nameKey(name: string): string {\n return name.trim().toLocaleLowerCase();\n}\n\n/**\n * Find the entry for a layer name: an exact match first, then one that differs\n * only in case or surrounding whitespace, so `Roads` still picks up a style\n * saved for `roads`. The first match in file order wins.\n *\n * @param entries - The style entries.\n * @param layerName - The layer name to look up.\n * @param kind - When given, only entries of this style family are considered,\n * so a raster style saved under a shared name does not shadow the vector one.\n * @returns The matching entry's index in `entries`, or -1.\n */\nexport function findLayerStyleEntryIndex(\n entries: readonly LayerStyleFileEntry[],\n layerName: string,\n kind?: LayerStyleClipboardKind,\n): number {\n const ofKind = (entry: LayerStyleFileEntry) => !kind || entry.kind === kind;\n const exact = entries.findIndex((entry) => ofKind(entry) && entry.layerName === layerName);\n if (exact >= 0) return exact;\n const key = nameKey(layerName);\n return entries.findIndex((entry) => ofKind(entry) && nameKey(entry.layerName) === key);\n}\n\nfunction toCopiedLayerStyle(entry: LayerStyleFileEntry): CopiedLayerStyle {\n if (entry.kind === \"vector\") {\n return { kind: \"vector\", sourceName: entry.layerName, style: entry.style };\n }\n return {\n kind: \"raster\",\n sourceName: entry.layerName,\n rasterState: entry.rasterState,\n hasRasterSymbology: entry.rasterSymbology !== undefined,\n ...(entry.rasterSymbology !== undefined ? { rasterSymbology: entry.rasterSymbology } : {}),\n };\n}\n\n/** A layer restyled from a style entry, and the entry it took. */\nexport interface LayerStyleMatch {\n /** Index of the applied entry in the entries array. */\n entryIndex: number;\n /** The patch that applies it. */\n patch: Partial<GeoLibreLayer>;\n}\n\n/**\n * Build the patch that restyles a layer from the entry matching its name, with\n * the same semantics as pasting a copied style: a vector style replaces the\n * whole style bag, a raster style merges its appearance keys and keeps the\n * layer's own band selection, and layer opacity is left alone.\n *\n * @param layer - The layer to restyle.\n * @param entries - The style entries.\n * @returns The matched entry and patch, or `null` when no entry of the layer's\n * style family matches its name (or the layer has no stylable symbology).\n */\nexport function matchLayerStyleEntry(\n layer: GeoLibreLayer,\n entries: readonly LayerStyleFileEntry[],\n): LayerStyleMatch | null {\n const kind = copyableLayerStyleKind(layer);\n if (!kind) return null;\n const entryIndex = findLayerStyleEntryIndex(entries, layer.name, kind);\n if (entryIndex < 0) return null;\n const patch = applyCopiedLayerStyle(layer, toCopiedLayerStyle(entries[entryIndex]));\n return patch ? { entryIndex, patch } : null;\n}\n","/**\n * The layer stack: adding, removing, editing, restyling and reordering layer\n * records, plus the copy/paste style clipboard. `layers` is tracked by undo\n * history. `removeLayer` also scrubs every other slice's references to the\n * deleted layer, so it reads and writes the whole state through `set`.\n */\nimport type { FeatureCollection } from \"geojson\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { isSourceDerivedLayerName, uniqueImportedLayerName } from \"../file-name\";\nimport { initialLayerStyle } from \"../layer-defaults\";\nimport { normalizeGroupContiguity } from \"../layer-groups\";\nimport {\n scrubCommentsForRemovedLayers,\n scrubLegendForRemovedLayers,\n scrubWidgetsForRemovedLayers,\n} from \"../layer-ref-scrub\";\nimport {\n applyCopiedLayerStyle,\n type CopiedLayerStyle,\n extractCopiedLayerStyle,\n} from \"../layer-style-clipboard\";\nimport { matchLayerStyleEntry, type LayerStyleFileEntry } from \"../layer-style-file\";\nimport { applyJoinsToLayer, cascadeLayerJoinRefresh } from \"../joins\";\nimport { normalizeLayerDescriptiveMetadata } from \"../layer-descriptive-metadata\";\nimport { withRasterSourceDefaults } from \"../raster-source-defaults\";\nimport { scrubPrintLayoutForRemovedLayers } from \"../print-layout-config\";\nimport { identifyStateWithoutLayers } from \"./session-slice\";\nimport {\n DEFAULT_LAYER_STYLE,\n shouldUseTiledRendering,\n type AddTileLayerOptions,\n type AttributeFormConfig,\n type EditorTrackingConfig,\n type GeoLibreLayer,\n type LayerJoin,\n type LayerDescriptiveMetadata,\n type LayerPopupConfig,\n type LayerQuickFilter,\n type LayerStyle,\n type LayerVirtualField,\n} from \"../types\";\nimport { featureCollectionToGeoArrow, type GeoArrowBatch } from \"../geoarrow\";\nimport { hasSimpleStyleProperties } from \"../vector-color\";\nimport {\n cascadeJoinRefreshForRemoved,\n scrubSecondaryPaneLayerVisibility,\n scrubStorymapLayerRefs,\n} from \"./layer-removal\";\nimport type { SliceCreator } from \"./types\";\n\n/**\n * The layer types {@link AppState.addTileLayer} accepts. Each renders through\n * the raster tile path, which is why the layer's `source.type` is always\n * `\"raster\"`.\n */\nconst RASTER_TILE_LAYER_TYPES: ReadonlySet<string> = new Set<\n NonNullable<AddTileLayerOptions[\"type\"]>\n>([\"xyz\", \"wms\", \"wmts\", \"raster\"]);\n\nexport interface LayersSlice {\n layers: GeoLibreLayer[];\n\n addLayer: (layer: GeoLibreLayer, beforeLayerId?: string | null) => void;\n removeLayer: (id: string) => void;\n updateLayer: (id: string, patch: Partial<GeoLibreLayer>) => void;\n setLayerVisibility: (id: string, visible: boolean) => void;\n setLayerOpacity: (id: string, opacity: number) => void;\n setLayerStyle: (id: string, style: Partial<LayerStyle>) => void;\n /**\n * Transient clipboard holding a layer's symbology, captured by\n * {@link copyLayerStyle} and applied by {@link pasteLayerStyle} (copy/paste\n * styles, issue #1339). Runtime-only: excluded from undo history\n * (`partialize` never lists it) and from the saved project. `null` until a\n * style is copied this session. Cleared by `newProject`/`loadProject` so a\n * paste can't apply an entry from a different project; it deliberately\n * survives undo/redo within a project (it holds a deep snapshot, not a live\n * layer reference, so a paste stays valid even if the source layer is undone\n * away — only the displayed source name may then be stale).\n */\n copiedLayerStyle: CopiedLayerStyle | null;\n /**\n * Snapshot the given layer's style into {@link copiedLayerStyle} so it can be\n * pasted onto a compatible layer. No-op when the layer is missing or has no\n * copyable symbology (leaving any prior clipboard entry untouched). Returns\n * `true` when a style was captured, so callers can skip the confirmation on a\n * no-op.\n */\n copyLayerStyle: (id: string) => boolean;\n /**\n * Apply the {@link copiedLayerStyle} clipboard entry onto the given layer.\n * No-op when the clipboard is empty, the layer is missing, or the entry's\n * style family does not match the target layer's. Returns `true` when the\n * style was applied, so callers can skip the confirmation on a no-op.\n */\n pasteLayerStyle: (id: string) => boolean;\n /**\n * Restyle layers from layer styles file entries matched by layer name\n * (Import Layer Styles, and the Startup setting's auto-apply). Each matched\n * layer is patched the way {@link pasteLayerStyle} would patch it, all in a\n * single store update so the import is one undo step.\n *\n * @param entries - The parsed style entries.\n * @param layerIds - Restrict the restyle to these layers; every layer when\n * omitted.\n * @returns Each restyled layer's id with the index of the entry it took, in\n * stack order.\n */\n applyLayerStyleEntries: (\n entries: readonly LayerStyleFileEntry[],\n layerIds?: readonly string[],\n ) => { layerId: string; entryIndex: number }[];\n /**\n * Replace a layer's persistent attribute joins and immediately re-derive its\n * joined columns (strip what the previous joins added, apply the new list).\n * Pass an empty array to detach every join and restore the base attributes.\n */\n setLayerJoins: (id: string, joins: LayerJoin[]) => void;\n /**\n * Replace the layer's Attribute Form designer configuration (per-field edit\n * widgets, constraints, conditional visibility). Pass `undefined` to remove\n * the form config entirely.\n */\n setLayerAttributeForm: (id: string, attributeForm: AttributeFormConfig | undefined) => void;\n /**\n * Replace the layer's popup/tooltip design (which fields the Identify popup\n * shows, in what order and under what labels, plus the hover tooltip). Pass\n * `undefined` to restore the default full-property dump.\n */\n setLayerPopup: (id: string, popup: LayerPopupConfig | undefined) => void;\n /**\n * Replace the layer's user-authored catalog metadata (title, abstract,\n * keywords, license, contact, lineage, temporal extent, links). The record\n * is normalized first; one that cleans to nothing (or `undefined`) removes\n * the metadata entirely. One call is one undo step.\n */\n setLayerDescriptiveMetadata: (id: string, metadata: LayerDescriptiveMetadata | undefined) => void;\n /**\n * Replace the layer's editor tracking configuration (whether creation/edit\n * author and timestamp columns are maintained, and under which names). Pass\n * `undefined` to drop the configuration entirely.\n */\n setLayerEditorTracking: (id: string, editorTracking: EditorTrackingConfig | undefined) => void;\n /**\n * Replace a layer's virtual fields and immediately re-derive its computed\n * columns (strip what the previous fields added, evaluate the new list).\n * Pass an empty array to detach every virtual field.\n */\n setLayerVirtualFields: (id: string, fields: LayerVirtualField[]) => void;\n /**\n * Replace a layer's quick filters (issue #2114). The controls persist with\n * the project and are compiled to a MapLibre filter at sync time, so this\n * only stores state — nothing re-derives the layer's data. Pass an empty\n * array to remove every control.\n */\n setLayerQuickFilters: (id: string, filters: LayerQuickFilter[]) => void;\n /** Set or clear the project-persisted expression filter for a layer. */\n setLayerFilterExpression: (id: string, expression: unknown[] | null) => void;\n reorderLayer: (id: string, direction: \"up\" | \"down\") => void;\n moveLayer: (id: string, targetIndex: number) => void;\n moveLayersRelative: (\n layerIds: string[],\n targetLayerId: string,\n position: \"above\" | \"below\",\n ) => void;\n addGeoJsonLayer: (\n name: string,\n geojson: FeatureCollection,\n sourcePath?: string,\n beforeLayerId?: string | null,\n extra?: { geoarrow?: GeoArrowBatch; metadata?: Record<string, unknown> },\n ) => string;\n /**\n * Add a georeferenced image overlay (a MapLibre `image` source rendered as a\n * raster layer) from an image URL and its four corner coordinates, and return\n * its id. Used for KML/KMZ `<GroundOverlay>` imports; the layer persists and\n * renders exactly like a Raster Georeferencer overlay. Corners are `[lng,\n * lat]` in top-left, top-right, bottom-right, bottom-left order.\n */\n addImageOverlayLayer: (\n name: string,\n source: { url: string; coordinates: [number, number][] },\n options?: {\n opacity?: number;\n bounds?: [number, number, number, number];\n sourcePath?: string;\n /** Initial visibility (default true); a time-slider frame past the first\n * starts hidden. */\n visible?: boolean;\n /** Epoch-ms time bounds of a KML `<TimeSpan>`/`<TimeStamp>` frame; the\n * Time Slider toggles this frame's visibility by the current date. */\n timeSpan?: { begin: number | null; end: number | null };\n /**\n * What produced the overlay, e.g. a NetCDF grid baked to pixels. Defaults\n * to the KML ground overlay this was first written for; panels gate their\n * per-source controls on it.\n */\n sourceKind?: string;\n /** Extra metadata merged onto the layer (e.g. a symbology record). */\n metadata?: Record<string, unknown>;\n },\n beforeLayerId?: string | null,\n ) => string;\n /**\n * Add a native raster tile layer (XYZ, WMS, or WMTS) from one or more tile\n * URL templates and return its id. The layer appears in the Layers panel and\n * persists with the project exactly like a layer added through the Add Data\n * dialog, so callers (e.g. an external plugin) do not have to touch MapLibre\n * directly. See {@link AddTileLayerOptions}.\n */\n addTileLayer: (\n name: string,\n options: AddTileLayerOptions,\n beforeLayerId?: string | null,\n ) => string;\n}\n\nexport const createLayersSlice: SliceCreator<LayersSlice> = (set, get) => ({\n layers: [],\n copiedLayerStyle: null,\n\n addLayer: (inputLayer, beforeLayerId = null) =>\n set((s) => {\n let layer = withRasterSourceDefaults(inputLayer);\n const layers = [...s.layers];\n // Plugin source identifiers (for example pmtiles://) are not local files.\n const { sourcePath } = layer;\n const localSource =\n sourcePath &&\n (!/^[a-z][a-z0-9+.-]*:\\/\\//i.test(sourcePath) || /^(content|file):\\/\\//i.test(sourcePath));\n // Only filename-derived names are deduplicated; an explicit name (an\n // embedded document title, a tool output label, a user-typed name)\n // is kept as supplied.\n if (localSource && isSourceDerivedLayerName(layer.name, sourcePath)) {\n layer = {\n ...layer,\n name: uniqueImportedLayerName(\n layer.name,\n layers.map((item) => item.name),\n ),\n };\n }\n const beforeIndex = beforeLayerId ? layers.findIndex((l) => l.id === beforeLayerId) : -1;\n const layerWithBeforeId =\n beforeLayerId && beforeIndex < 0\n ? { ...layer, beforeId: beforeLayerId }\n : { ...layer, beforeId: layer.beforeId };\n if (beforeIndex >= 0) {\n layers.splice(beforeIndex, 0, layerWithBeforeId);\n } else {\n layers.push(layerWithBeforeId);\n }\n return {\n layers,\n selectedLayerId: layer.id,\n isDirty: true,\n };\n }),\n\n removeLayer: (id) =>\n set((s) => ({\n // Re-derive any layer whose joins consumed the removed layer: with\n // the source gone the join resolves to nothing, so its previously\n // materialized columns strip away instead of staying frozen (the\n // join definition itself stays, shown as missing in the Joins UI).\n layers: cascadeJoinRefreshForRemoved(\n s.layers.filter((l) => l.id !== id),\n id,\n ),\n secondaryMapViews: scrubSecondaryPaneLayerVisibility(s.secondaryMapViews, id),\n // Drop storymap chapter enter/exit opacity rows that pointed at the\n // removed layer so they do not keep a dangling id across save/reload.\n storymap: scrubStorymapLayerRefs(s.storymap, id),\n widgets: scrubWidgetsForRemovedLayers(s.widgets, id),\n comments: scrubCommentsForRemovedLayers(s.comments, id),\n legend: scrubLegendForRemovedLayers(s.legend, id),\n // Clear a Print Layout data/atlas block built on the removed layer,\n // so a save that follows the delete cannot write a dangling id.\n printLayout: scrubPrintLayoutForRemovedLayers(s.printLayout, id),\n selectedLayerId:\n s.selectedLayerId === id\n ? (s.layers.find((l) => l.id !== id)?.id ?? null)\n : s.selectedLayerId,\n selectedFeatureId: s.selectedLayerId === id ? null : s.selectedFeatureId,\n selectedFeatureIds: s.selectedLayerId === id ? [] : s.selectedFeatureIds,\n ...identifyStateWithoutLayers(s, new Set([id])),\n ui: {\n ...s.ui,\n selectByExpressionLayerId:\n s.ui.selectByExpressionLayerId === id ? null : s.ui.selectByExpressionLayerId,\n selectByLocationLayerId:\n s.ui.selectByLocationLayerId === id ? null : s.ui.selectByLocationLayerId,\n loadEditorFeaturesLayerId:\n s.ui.loadEditorFeaturesLayerId === id ? null : s.ui.loadEditorFeaturesLayerId,\n },\n isDirty: true,\n })),\n\n updateLayer: (id, patch) =>\n set((s) => {\n let layers = s.layers.map((l) => {\n if (l.id !== id) return l;\n const updated = { ...l, ...patch };\n // If a patch explicitly replaces geojson but does not explicitly supply geoarrow,\n // clear the existing GeoArrow batch to avoid retaining stale geometry.\n if (patch.geojson !== undefined && !(\"geoarrow\" in patch)) {\n delete updated.geoarrow;\n } else if (\"geoarrow\" in patch) {\n if (patch.geoarrow === null || patch.geoarrow === undefined) {\n delete updated.geoarrow;\n } else {\n updated.geoarrow = patch.geoarrow;\n }\n }\n return updated;\n });\n // A geojson replacement re-derives the layer's derived columns\n // (persistent joins, then virtual fields): on the layer itself\n // (file reload, attribute edits, processing writes — derived\n // columns stay derived, QGIS-style), then transitively on every\n // layer whose joins consume the updated one, so editing a join\n // table refreshes its targets and their dependents in turn. The\n // `patch.joins`/`patch.virtualFields` guard exists for external\n // callers of this public store API (plugins, programmatic loads):\n // a patch that carries `geojson` alongside the definitions is taken\n // verbatim as already-derived state — no in-repo caller does this\n // today.\n if (patch.geojson !== undefined) {\n if (patch.joins === undefined && patch.virtualFields === undefined) {\n layers = layers.map((l) =>\n l.id === id && (l.joins?.length || l.virtualFields?.length)\n ? applyJoinsToLayer(l, layers)\n : l,\n );\n }\n layers = cascadeLayerJoinRefresh(layers, id);\n }\n return { layers, isDirty: true };\n }),\n\n setLayerJoins: (id, joins) =>\n set((s) => {\n let layers = s.layers.map((l) => (l.id === id ? applyJoinsToLayer(l, s.layers, joins) : l));\n // Changing this layer's joins changes its materialized columns, so\n // layers joining against it (directly or transitively) re-derive too.\n layers = cascadeLayerJoinRefresh(layers, id);\n return { layers, isDirty: true };\n }),\n\n setLayerAttributeForm: (id, attributeForm) => get().updateLayer(id, { attributeForm }),\n setLayerPopup: (id, popup) => get().updateLayer(id, { popup }),\n setLayerDescriptiveMetadata: (id, metadata) =>\n get().updateLayer(id, { descriptiveMetadata: normalizeLayerDescriptiveMetadata(metadata) }),\n\n setLayerEditorTracking: (id, editorTracking) => get().updateLayer(id, { editorTracking }),\n\n setLayerVirtualFields: (id, fields) =>\n set((s) => {\n let layers = s.layers.map((l) =>\n l.id === id ? applyJoinsToLayer(l, s.layers, undefined, fields) : l,\n );\n // Virtual columns are ordinary materialized properties, so a layer\n // joining against this one (directly or transitively) sees them and\n // must re-derive too.\n layers = cascadeLayerJoinRefresh(layers, id);\n return { layers, isDirty: true };\n }),\n\n setLayerQuickFilters: (id, filters) =>\n get().updateLayer(id, { quickFilters: filters.length > 0 ? filters : undefined }),\n\n setLayerFilterExpression: (id, expression) =>\n get().updateLayer(id, { filterExpression: expression ?? undefined }),\n\n setLayerVisibility: (id, visible) => get().updateLayer(id, { visible }),\n\n setLayerOpacity: (id, opacity) => get().updateLayer(id, { opacity }),\n\n setLayerStyle: (id, style) =>\n set((s) => ({\n layers: s.layers.map((l) => (l.id === id ? { ...l, style: { ...l.style, ...style } } : l)),\n isDirty: true,\n })),\n\n copyLayerStyle: (id) => {\n const layer = get().layers.find((l) => l.id === id);\n if (!layer) return false;\n const copied = extractCopiedLayerStyle(layer);\n // Leave any prior clipboard entry in place when this layer is not\n // copyable, so opening a non-stylable layer's menu never clears it.\n if (!copied) return false;\n set({ copiedLayerStyle: copied });\n return true;\n },\n\n pasteLayerStyle: (id) => {\n const s = get();\n const copied = s.copiedLayerStyle;\n if (!copied) return false;\n const layer = s.layers.find((l) => l.id === id);\n if (!layer) return false;\n const patch = applyCopiedLayerStyle(layer, copied);\n if (!patch) return false;\n // Go through updateLayer so the paste picks up any cross-cutting layer\n // update logic (it also sets isDirty); the patch never carries geojson,\n // so the join-cascade branch is a no-op.\n get().updateLayer(id, patch);\n return true;\n },\n\n applyLayerStyleEntries: (entries, layerIds) => {\n if (entries.length === 0) return [];\n const scope = layerIds ? new Set(layerIds) : null;\n const applied: { layerId: string; entryIndex: number }[] = [];\n const layers = get().layers.map((layer) => {\n if (scope && !scope.has(layer.id)) return layer;\n const match = matchLayerStyleEntry(layer, entries);\n if (!match) return layer;\n applied.push({ layerId: layer.id, entryIndex: match.entryIndex });\n return { ...layer, ...match.patch };\n });\n // A style patch never carries geojson, so unlike updateLayer there is no\n // join cascade to run.\n if (applied.length > 0) set({ layers, isDirty: true });\n return applied;\n },\n\n reorderLayer: (id, direction) =>\n set((s) => {\n const idx = s.layers.findIndex((l) => l.id === id);\n if (idx < 0) return s;\n const target = direction === \"up\" ? idx + 1 : idx - 1;\n if (target < 0 || target >= s.layers.length) return s;\n const next = [...s.layers];\n const [item] = next.splice(idx, 1);\n next.splice(target, 0, item);\n return { layers: next, isDirty: true };\n }),\n\n moveLayer: (id, targetIndex) =>\n set((s) => {\n const currentIndex = s.layers.findIndex((layer) => layer.id === id);\n if (currentIndex < 0) return s;\n const next = [...s.layers];\n const [layer] = next.splice(currentIndex, 1);\n const nextIndex = Math.min(Math.max(targetIndex, 0), next.length);\n next.splice(nextIndex, 0, layer);\n if (next.every((item, index) => item.id === s.layers[index]?.id)) {\n return s;\n }\n return { layers: next, isDirty: true };\n }),\n\n moveLayersRelative: (layerIds, targetLayerId, position) =>\n set((s) => {\n const requestedIds = new Set(layerIds);\n if (requestedIds.has(targetLayerId)) return s;\n const target = s.layers.find((layer) => layer.id === targetLayerId);\n if (!target) return s;\n const targetGroupId = target.groupId ?? null;\n // This is a pure reorder — `groupId` is never touched — so a requested\n // layer from another group can only be lifted out of its own block,\n // and `normalizeGroupContiguity` would then drag that group's\n // untouched members along to reunite it. Move only the layers that\n // already sit in the target's group.\n const moving = s.layers.filter(\n (layer) => requestedIds.has(layer.id) && (layer.groupId ?? null) === targetGroupId,\n );\n if (moving.length === 0) return s;\n const movingIds = new Set(moving.map((layer) => layer.id));\n const without = s.layers.filter((layer) => !movingIds.has(layer.id));\n const targetIndex = without.findIndex((layer) => layer.id === targetLayerId);\n if (targetIndex < 0) return s;\n // Store order is the reverse of panel display order, so \"above\" is\n // immediately after the target in this array.\n const insertIndex = position === \"above\" ? targetIndex + 1 : targetIndex;\n const next = [...without];\n next.splice(insertIndex, 0, ...moving);\n const normalized = normalizeGroupContiguity(next);\n if (normalized.every((layer, index) => layer.id === s.layers[index]?.id)) return s;\n return { layers: normalized, isDirty: true };\n }),\n\n addGeoJsonLayer: (name, geojson, sourcePath, beforeLayerId = null, extra) => {\n const id = uuidv4();\n // GeoJSON remains authoritative for editing. The optional batch adds\n // coordinate storage; it does not reduce the memory held by this layer.\n let geoarrow = extra?.geoarrow;\n if (!geoarrow && shouldUseTiledRendering(geojson)) {\n try {\n geoarrow = featureCollectionToGeoArrow(geojson) ?? undefined;\n } catch (err) {\n console.warn(\"[GeoLibre] GeoArrow batch conversion failed; falling back to GeoJSON\", err);\n }\n }\n const layer: GeoLibreLayer = {\n id,\n name,\n type: \"geojson\",\n source: { type: \"geojson\" },\n visible: true,\n opacity: 1,\n // Its own palette color and geometry-appropriate sizing (#1519), so a\n // stack of freshly added layers is legible without restyling each one.\n style: initialLayerStyle({\n geojson,\n layers: get().layers,\n overrides: {\n simpleStyleEnabled: hasSimpleStyleProperties(geojson),\n },\n }),\n metadata: { ...extra?.metadata },\n geojson,\n ...(geoarrow ? { geoarrow } : {}),\n sourcePath,\n };\n get().addLayer(layer, beforeLayerId);\n return id;\n },\n\n addImageOverlayLayer: (name, source, options, beforeLayerId = null) => {\n const id = uuidv4();\n const layer: GeoLibreLayer = {\n id,\n name,\n type: \"image\",\n source: {\n type: \"image\",\n url: source.url,\n coordinates: source.coordinates,\n },\n visible: options?.visible ?? true,\n opacity: options?.opacity ?? 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: options?.sourceKind ?? \"kml-ground-overlay\",\n ...(options?.bounds ? { bounds: options.bounds } : {}),\n ...(options?.timeSpan ? { timeSpan: options.timeSpan } : {}),\n ...(options?.metadata ?? {}),\n },\n ...(options?.sourcePath ? { sourcePath: options.sourcePath } : {}),\n };\n get().addLayer(layer, beforeLayerId);\n return id;\n },\n\n addTileLayer: (name, options, beforeLayerId = null) => {\n // Every layer this builds carries a raster source, so a non-raster\n // `type` from an untyped JS caller (e.g. \"vector-tiles\") would persist\n // a layer whose `type` and `source.type` disagree. Reject it instead of\n // silently mislabelling the source; vector tiles need their own style\n // layers and go through the vector-tile path, not this one.\n const type = options.type ?? \"xyz\";\n if (!RASTER_TILE_LAYER_TYPES.has(type)) {\n throw new Error(\n `addTileLayer: unsupported type \"${String(type)}\"; expected one of ` +\n `${[...RASTER_TILE_LAYER_TYPES].join(\", \")}. Only raster tile layers are supported.`,\n );\n }\n const id = uuidv4();\n // Trim each template and drop blanks, then reject a registration that\n // sanitizes down to nothing: syncRasterTileLayer returns early on an\n // empty tile list, so without this the store would persist and select a\n // layer that can never render.\n const tiles = options.tiles\n .filter((tile): tile is string => typeof tile === \"string\")\n .map((tile) => tile.trim())\n .filter((tile) => tile !== \"\");\n if (tiles.length === 0) {\n throw new Error(\"addTileLayer requires at least one non-empty tile URL template.\");\n }\n // MapLibre's addSource throws synchronously when maxzoom < minzoom, and\n // syncRasterTileLayer does not catch it, which would strand a layer in\n // the store with no rendered source. Reject the bad range up front.\n if (\n options.minzoom !== undefined &&\n options.maxzoom !== undefined &&\n options.minzoom > options.maxzoom\n ) {\n throw new Error(\n `addTileLayer: minzoom (${options.minzoom}) must be <= maxzoom (${options.maxzoom}).`,\n );\n }\n const layer: GeoLibreLayer = {\n id,\n name,\n type,\n source: {\n // Extra source fields (e.g. WMS layers/styles) merge first so the\n // required raster descriptor below always wins. `type` above is\n // validated as a raster kind, so \"raster\" here always agrees with it.\n ...(options.source ?? {}),\n type: \"raster\",\n tiles,\n tileSize: options.tileSize ?? 256,\n ...(options.url !== undefined ? { url: options.url } : {}),\n ...(options.attribution !== undefined ? { attribution: options.attribution } : {}),\n ...(options.bounds !== undefined ? { bounds: options.bounds } : {}),\n ...(options.minzoom !== undefined ? { minzoom: options.minzoom } : {}),\n ...(options.maxzoom !== undefined ? { maxzoom: options.maxzoom } : {}),\n ...(options.scheme !== undefined ? { scheme: options.scheme } : {}),\n },\n visible: options.visible ?? true,\n opacity: options.opacity ?? 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: { ...(options.metadata ?? {}) },\n };\n get().addLayer(layer, beforeLayerId);\n return id;\n },\n});\n","// The Layer Library's data layer (issue #1520): capturing a configured layer as\n// a reusable entry, planning how to re-add one to a project, the shareable JSON\n// bundle format, and untrusted-input normalization. UI-free so both the desktop\n// app and tests consume it directly. Mirrors the shape of `style-library.ts`,\n// which does the same job for the Style Manager.\n//\n// The guiding rule is that an entry stores the layer's **source specification,\n// not its data**: the library stays small and re-adding an entry always shows\n// the current contents of its source. Layers whose features exist only in\n// memory (drawn features, processing output) or only in a local file have no\n// re-fetchable source, so those embed their features behind a size cap.\n\nimport { cesiumIonAssetId } from \"./cesium-ion\";\nimport type { FeatureCollection } from \"geojson\";\nimport {\n DEFAULT_LAYER_STYLE,\n LAYER_TYPES,\n type AttributeFormConfig,\n type GeoLibreLayer,\n type LayerDescriptiveMetadata,\n type LayerJoin,\n type LayerLibraryEntry,\n type LayerPopupConfig,\n type LayerStyle,\n type LayerType,\n type LayerVirtualField,\n} from \"./types\";\nimport { sanitizeLayerStylePatch } from \"./style-library\";\nimport { normalizeLayerDescriptiveMetadata } from \"./layer-descriptive-metadata\";\nimport { hasPathTraversal, isAbsoluteFilesystemPath } from \"./paths\";\nimport { isCredentialFieldName, redactUrlCredentials } from \"./credentials\";\n\n/** `type` discriminator of an exported Layer Library bundle file. */\nexport const LAYER_LIBRARY_BUNDLE_TYPE = \"geolibre-layer-library\";\n\n/** Current bundle format version, for forward-compatible readers. */\nexport const LAYER_LIBRARY_BUNDLE_VERSION = 1;\n\n/**\n * Size ceiling for one library entry's serialized JSON, in bytes. The library\n * lives in IndexedDB and is meant to be a small, shareable index of *sources*,\n * so an entry that can only be stored by embedding a large feature set is\n * refused rather than silently turning the library into a data store.\n *\n * Applied to the whole entry (not just `geojson`), because an Add Vector Layer\n * entry can also carry a `metadata.embeddedGeoJSON` copy of its features.\n */\nexport const MAX_LAYER_LIBRARY_ENTRY_BYTES = 5 * 1024 * 1024;\n\n/**\n * Cap on how many entries a single normalization pass keeps. Enforced on the way\n * *in* — an imported bundle or a hand-edited database is untrusted, and without\n * a bound one file could push an unlimited number of records through this\n * synchronous pass and into IndexedDB. Mirrors `MAX_FAVORITES`, which caps the\n * Browser panel's other user-grown list on read as well as write.\n */\nexport const MAX_LAYER_LIBRARY_ENTRIES = 500;\n\n/**\n * Metadata keys dropped when capturing an entry. `resolvedUrl` is the dev-server\n * proxy rewrite of an XYZ template — a per-session artifact that must not be\n * baked into a saved source (the same reason `prepareLayerForSave` strips it).\n * `geometryEdited` tracks changes to the live layer, not a library source.\n */\nconst TRANSIENT_METADATA_KEYS = [\"resolvedUrl\", \"geometryEdited\"] as const;\n\nfunction nonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.trim() !== \"\";\n}\n\n/**\n * Whether a layer's features can be re-fetched from its source specification\n * alone, so an entry need not embed them: a tile/service template, a remote\n * file URL, or a GeoJSON source pointing at a URL. A `geojson` source whose\n * `data` is an inline FeatureCollection (not a URL string) deliberately does\n * not count.\n *\n * @param layer - The layer to inspect (source + metadata only).\n * @returns True when the source alone is enough to recreate the layer's data.\n */\nexport function hasRestorableLayerSource(\n layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">,\n): boolean {\n const source = layer.source ?? {};\n if (nonEmptyString(source.url)) return true;\n if (nonEmptyString(source.data)) return true;\n if (Array.isArray(source.tiles) && source.tiles.some(nonEmptyString)) return true;\n // A Cesium Ion asset is re-fetched from its id alone (under the cesium-ion\n // source kind, the same contract the globe loads it by).\n if (cesiumIonAssetId(layer) !== null) return true;\n return nonEmptyString((layer.metadata ?? {}).originalUrl);\n}\n\n/**\n * The absolute local path a layer's data can be **re-read** from, or undefined.\n *\n * A non-empty `sourcePath` is not enough on its own: a file picked in the\n * browser has no path, and both the vector and raster controls still record the\n * bare file *name* there for display. Each control marks a genuinely re-readable\n * absolute path differently, so both conventions are honored:\n *\n * - Vector / plain local GeoJSON: `metadata.localFileReloadable` alongside an\n * absolute `sourcePath` (`prepareLayerForSave`, `needsLocalFileReload`,\n * `restoreVectorLayers`).\n * - Raster / COG: `metadata.localFilePath` holds the absolute path outright,\n * and it is what `restoreRasterLayers` re-reads on restore. Without this\n * branch a locally-opened COG could not be saved at all, even though its\n * restore pass is fully able to replay it.\n *\n * Every candidate is re-validated, because a hand-edited project or an imported\n * bundle can set these fields to anything.\n */\nexport function layerLocalPath(\n layer: Pick<GeoLibreLayer, \"sourcePath\" | \"metadata\">,\n): string | undefined {\n const metadata = layer.metadata ?? {};\n if (isReReadablePath(metadata.localFilePath)) return metadata.localFilePath;\n if (metadata.localFileReloadable !== true) return undefined;\n return isReReadablePath(layer.sourcePath) ? layer.sourcePath : undefined;\n}\n\n/**\n * Whether a value is a path safe to hand to a host file read: a non-empty\n * string, absolute, with no `..` traversal. Takes `unknown` and does the\n * non-empty check itself so every path-like field — `sourcePath`,\n * `metadata.localFilePath`, and any added later — goes through one predicate;\n * splitting the two halves is what let those two fields drift apart in the\n * first place.\n *\n * Applied to captured *and* imported entries, because a shared bundle is\n * untrusted input: a crafted `needsLocalFile` entry could otherwise point at any\n * file on the importing user's disk, and clicking it in My Data would read that\n * file. Mirrors the client-side re-validation the project-restore paths already\n * do before calling into Tauri.\n */\nfunction isReReadablePath(value: unknown): value is string {\n return nonEmptyString(value) && isAbsoluteFilesystemPath(value) && !hasPathTraversal(value);\n}\n\nfunction featureCount(geojson: FeatureCollection | undefined): number {\n return Array.isArray(geojson?.features) ? geojson.features.length : 0;\n}\n\n/**\n * Whether \"Save to library\" applies to a layer at all — the cheap predicate the\n * layer actions menu gates on. Two things have to hold:\n *\n * 1. There is something to re-add *from*: a re-fetchable source, a local file\n * path, or features to embed. A control-painted layer's features live in its\n * control rather than the store record — a tiles-mode Add Vector Layer layer\n * has no `layer.geojson` at all — so the caller can answer for that case with\n * `hasMaterializableFeatures`, keeping this gate consistent with the capture\n * path, which reads those features through `options.features`.\n * 2. The host can actually render it again. For a control-painted layer\n * ({@link isExternalNativeLayerRecord}) that means either the map's own sync\n * rebuilds it from the record or the owning plugin's restore pass can be\n * re-run, which only the host knows — so the caller supplies\n * `canRestoreControlPainted`. Without it, control-painted layers are refused\n * rather than saved into an entry that would re-add unrendered.\n *\n * A layer that qualifies here can still fail to save because its features are\n * too large to embed ({@link MAX_LAYER_LIBRARY_ENTRY_BYTES}); that outcome needs\n * the full capture, so it is reported by {@link captureLayerLibraryEntry} rather\n * than hidden behind a disabled menu item.\n *\n * @param layer - The layer to test.\n * @param options - `canRestoreControlPainted` answers whether the host can\n * re-render a control-painted layer (the desktop app passes its\n * `canRestoreLibraryLayer`); `hasMaterializableFeatures` answers whether it can\n * read the layer's features out of its control (the app passes\n * `isEmbeddableLocalVectorLayer`, the same predicate its save handler uses).\n * @returns Whether the layer can be offered to the library.\n */\nexport function canSaveLayerToLibrary(\n layer: GeoLibreLayer,\n options: {\n canRestoreControlPainted?: (layer: GeoLibreLayer) => boolean;\n hasMaterializableFeatures?: (layer: GeoLibreLayer) => boolean;\n } = {},\n): boolean {\n const hasSomethingToReAddFrom =\n hasRestorableLayerSource(layer) ||\n layerLocalPath(layer) !== undefined ||\n featureCount(layer.geojson) > 0 ||\n options.hasMaterializableFeatures?.(layer) === true;\n if (!hasSomethingToReAddFrom) return false;\n if (!isExternalNativeLayerRecord(layer)) return true;\n return options.canRestoreControlPainted?.(layer) === true;\n}\n\n/**\n * The `source` to store for a layer, with a live XYZ layer's session-specific\n * endpoint rewound to the template it was resolved from.\n *\n * `prepareLayerForSave` does the same thing when writing a project file: an XYZ\n * layer's `source.tiles`/`source.url` can hold a *resolved* endpoint while\n * `metadata.originalUrl` keeps the template, and baking the resolved value into\n * a saved record makes it replay a stale host instead of re-resolving. A library\n * entry outlives a session by design, so it needs the same rewind — stripping\n * `metadata.resolvedUrl` alone would leave the resolved endpoint sitting in\n * `source`.\n */\nfunction sourceForCapture(layer: GeoLibreLayer): Record<string, unknown> {\n const source = layer.source ?? {};\n if (layer.type !== \"xyz\") return source;\n const originalUrl = nonEmptyString((layer.metadata ?? {}).originalUrl)\n ? layer.metadata.originalUrl\n : nonEmptyString(source.url)\n ? source.url\n : null;\n if (!originalUrl) return source;\n // A TileJSON layer keeps its own tile templates — see `prepareLayerForSave`.\n // The library re-adds a captured source verbatim, with no re-resolution step,\n // so collapsing `tiles` onto the document URL here would never load a tile.\n if (typeof (layer.metadata ?? {}).tilejsonUrl === \"string\") {\n return { ...source, url: originalUrl };\n }\n return { ...source, tiles: [originalUrl], url: originalUrl };\n}\n\n/** Why a layer could not be captured into the library. */\nexport type LayerLibraryCaptureFailure =\n /** Nothing to re-add from: no source URL, no local path, no features. */\n | \"no-source\"\n /** Its features are the only copy, and they exceed the per-entry size cap. */\n | \"features-too-large\"\n /**\n * The entry exceeds the cap without embedding any features — its style,\n * attribute form, joins, or virtual fields are simply that large. Reported\n * separately so the message does not blame the layer's data.\n */\n | \"config-too-large\";\n\n/** Outcome of {@link captureLayerLibraryEntry}. */\nexport type LayerLibraryCaptureResult =\n | { ok: true; entry: LayerLibraryEntry }\n | { ok: false; reason: LayerLibraryCaptureFailure };\n\n/**\n * Serialized size of a value in UTF-8 bytes — what IndexedDB and an exported\n * bundle actually cost. Measured with {@link TextEncoder} rather than\n * `JSON.stringify().length`, which counts UTF-16 code units: CJK, Arabic, and\n * Cyrillic text is 1 unit but 2-3 bytes per character, so a length-based check\n * would let an entry through at 2-3x the cap it is supposed to enforce.\n */\nfunction jsonByteLength(value: unknown): number {\n try {\n const json = JSON.stringify(value);\n return json === undefined ? 0 : new TextEncoder().encode(json).length;\n } catch {\n // A source or metadata value with a circular reference cannot be stored at\n // all, so treat it as over any cap.\n return Number.POSITIVE_INFINITY;\n }\n}\n\n/**\n * Whether a layer is painted by a plugin control rather than by GeoLibre's own\n * layer sync (Add Vector Layer, the deck.gl raster/COG control, 3D Tiles,\n * LiDAR, …). These recreate their map output from the layer record through\n * their plugin's restore pass, so an entry for one must carry the metadata that\n * pass reads — and re-adding it has to run that pass.\n *\n * @param layer - The layer to test (metadata only).\n * @returns True when a plugin control owns the layer's rendering.\n */\nexport function isExternalNativeLayerRecord(layer: Pick<GeoLibreLayer, \"metadata\">): boolean {\n return (layer.metadata ?? {}).externalNativeLayer === true;\n}\n\n/**\n * Capture a configured layer as a reusable Layer Library entry: its source\n * spec, layer type, full style (labels included), opacity, metadata, and the\n * per-layer configuration that makes it \"fully configured\" — joins, virtual\n * fields, and the attribute form. Project-specific placement (`id`,\n * `groupId`, `beforeId`) is deliberately not captured.\n *\n * Features are embedded only when the source cannot supply them (an in-memory\n * or local-file layer). Where they land depends on who paints the layer:\n * GeoLibre-rendered layers keep them in {@link LayerLibraryEntry.geojson},\n * while a control-painted layer gets them in `metadata.embeddedGeoJSON` — the\n * same field the project format's Embed/Share flow writes and the plugin's\n * restore pass reads.\n *\n * If the result exceeds {@link MAX_LAYER_LIBRARY_ENTRY_BYTES} the capture\n * degrades rather than failing outright: it falls back to a path-only entry\n * (`needsLocalFile`) when a local file can supply the features.\n *\n * @param layer - The live layer to save.\n * @param options - Fresh entry `id`, ISO `addedAt`, an optional `name` override\n * (defaults to the layer's name), and optional `features` to embed instead of\n * the layer's own `geojson` (the caller reads a control-painted layer's\n * current data from its control).\n * @returns The entry, or the reason it could not be captured.\n */\nexport function captureLayerLibraryEntry(\n layer: GeoLibreLayer,\n options: { id: string; addedAt: string; name?: string; features?: FeatureCollection },\n): LayerLibraryCaptureResult {\n const localPath = layerLocalPath(layer);\n const hasSource = hasRestorableLayerSource(layer);\n const supplied = featureCount(options.features) > 0 ? options.features : undefined;\n const features = supplied ?? (featureCount(layer.geojson) > 0 ? layer.geojson : undefined);\n if (!hasSource && !localPath && !features) {\n return { ok: false, reason: \"no-source\" };\n }\n\n const metadata: Record<string, unknown> = { ...(layer.metadata ?? {}) };\n for (const key of TRANSIENT_METADATA_KEYS) delete metadata[key];\n // A stale embedded copy from the project this layer was loaded from: the\n // capture below re-embeds current data (or drops the copy when the source can\n // re-fetch it), so never carry the old blob through.\n delete metadata.embeddedGeoJSON;\n\n const controlPainted = isExternalNativeLayerRecord(layer);\n // Embed features only when the source cannot re-fetch them; a layer with a\n // restorable source stays data-free so its entry keeps reflecting the source.\n const embed = !hasSource && features ? structuredClone(features) : undefined;\n if (embed && controlPainted) {\n metadata.embeddedGeoJSON = embed;\n // Mirrors the project Embed flow: with the data embedded, the restore pass\n // must replay it rather than a file path that may not exist on this machine.\n delete metadata.localFileReloadable;\n }\n\n const base: LayerLibraryEntry = {\n id: options.id,\n name: options.name?.trim() || layer.name,\n addedAt: options.addedAt,\n layerType: layer.type,\n source: structuredClone(sourceForCapture(layer)),\n style: structuredClone({ ...DEFAULT_LAYER_STYLE, ...layer.style }),\n opacity:\n typeof layer.opacity === \"number\" && Number.isFinite(layer.opacity) ? layer.opacity : 1,\n metadata: structuredClone(metadata),\n ...(localPath ? { sourcePath: localPath } : {}),\n ...(layer.joins?.length ? { joins: structuredClone(layer.joins) } : {}),\n ...(layer.virtualFields?.length ? { virtualFields: structuredClone(layer.virtualFields) } : {}),\n ...(layer.attributeForm ? { attributeForm: structuredClone(layer.attributeForm) } : {}),\n ...(layer.popup ? { popup: structuredClone(layer.popup) } : {}),\n ...(layer.descriptiveMetadata\n ? { descriptiveMetadata: structuredClone(layer.descriptiveMetadata) }\n : {}),\n };\n const withFeatures = embed && !controlPainted ? { ...base, geojson: embed } : base;\n\n if (jsonByteLength(withFeatures) <= MAX_LAYER_LIBRARY_ENTRY_BYTES) {\n return { ok: true, entry: withFeatures };\n }\n if (embed && localPath) {\n // Last resort for an oversized local file: keep the path and let the\n // desktop host re-read it at add time.\n const { embeddedGeoJSON: _dropped, ...leanMetadata } = withFeatures.metadata;\n const pathOnly: LayerLibraryEntry = {\n ...withFeatures,\n metadata: leanMetadata,\n needsLocalFile: true,\n };\n delete pathOnly.geojson;\n if (jsonByteLength(pathOnly) <= MAX_LAYER_LIBRARY_ENTRY_BYTES) {\n return { ok: true, entry: pathOnly };\n }\n }\n // Blame the right thing: an entry can also exceed the cap with no embedded\n // features at all (a rule-based style with hundreds of rules, a large\n // attribute form), and telling that user their \"features are the only copy\"\n // would send them looking in the wrong place.\n return { ok: false, reason: embed ? \"features-too-large\" : \"config-too-large\" };\n}\n\n/**\n * The presentation state a re-added layer takes from its entry. For the\n * `local-file` plan the host imports the file first (producing a default-styled\n * layer) and then applies this patch, so a degraded entry still comes back\n * \"fully configured\" rather than losing the styling the entry preserved.\n */\nexport interface LayerLibraryConfigPatch {\n name: string;\n style: LayerStyle;\n opacity: number;\n joins?: LayerJoin[];\n virtualFields?: LayerVirtualField[];\n attributeForm?: AttributeFormConfig;\n popup?: LayerPopupConfig;\n descriptiveMetadata?: LayerDescriptiveMetadata;\n}\n\n/** How an entry should be re-added to the current project. */\nexport type LayerLibraryAddPlan =\n /** Add this layer record to the store; the map sync renders it. */\n | { kind: \"layer\"; layer: GeoLibreLayer }\n /**\n * The entry's features live only in a local file the host must re-read (its\n * data was too large to embed), so the caller runs its local-file add path for\n * `path` and then applies `config` to the layer that import produced. Only\n * reachable on a host with filesystem access.\n */\n | { kind: \"local-file\"; path: string; config: LayerLibraryConfigPatch };\n\n/**\n * Plan how to re-add a library entry to the current project.\n *\n * @param entry - The library entry to add.\n * @param options - The fresh layer `id` to create it under.\n * @returns Either the layer record to add, or the local file to re-read.\n */\nexport function planLayerLibraryAdd(\n entry: LayerLibraryEntry,\n options: { id: string },\n): LayerLibraryAddPlan {\n if (layerLibraryEntryNeedsLocalFile(entry)) {\n return {\n kind: \"local-file\",\n // Non-null by the guard above, which also re-validates the path.\n path: entry.sourcePath as string,\n config: layerLibraryConfigPatch(entry),\n };\n }\n return {\n kind: \"layer\",\n layer: {\n id: options.id,\n name: entry.name,\n type: entry.layerType,\n source: structuredClone(entry.source),\n // Always added visible: the user just asked for this layer.\n visible: true,\n opacity: entry.opacity,\n style: structuredClone(entry.style),\n metadata: structuredClone(entry.metadata),\n ...(entry.sourcePath ? { sourcePath: entry.sourcePath } : {}),\n ...(entry.geojson ? { geojson: structuredClone(entry.geojson) } : {}),\n ...(entry.joins ? { joins: structuredClone(entry.joins) } : {}),\n ...(entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {}),\n ...(entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {}),\n ...(entry.popup ? { popup: structuredClone(entry.popup) } : {}),\n ...(entry.descriptiveMetadata\n ? { descriptiveMetadata: structuredClone(entry.descriptiveMetadata) }\n : {}),\n },\n };\n}\n\n/**\n * The presentation state to apply to a layer the host imported from an entry's\n * file, so the `local-file` plan preserves everything the entry captured.\n *\n * @param entry - The entry being re-added.\n * @returns The patch to merge onto the imported layer.\n */\nexport function layerLibraryConfigPatch(entry: LayerLibraryEntry): LayerLibraryConfigPatch {\n return {\n name: entry.name,\n style: structuredClone(entry.style),\n opacity: entry.opacity,\n ...(entry.joins ? { joins: structuredClone(entry.joins) } : {}),\n ...(entry.virtualFields ? { virtualFields: structuredClone(entry.virtualFields) } : {}),\n ...(entry.attributeForm ? { attributeForm: structuredClone(entry.attributeForm) } : {}),\n ...(entry.popup ? { popup: structuredClone(entry.popup) } : {}),\n ...(entry.descriptiveMetadata\n ? { descriptiveMetadata: structuredClone(entry.descriptiveMetadata) }\n : {}),\n };\n}\n\n/**\n * The saved joins whose source layer is not present in the target project.\n *\n * A {@link LayerJoin} points at another layer by its **project-local id**, so a\n * join travels only as far as the project it was captured in: re-adding an entry\n * elsewhere leaves those joins referring to layers that do not exist. The join\n * engine degrades quietly (`applyLayerJoins` just drops an unresolved join from\n * its active set), which is the right runtime behavior but a silent one — so the\n * caller uses this to tell the user which joins did not come back, instead of\n * letting them find a column missing later.\n *\n * The joins are still captured and reapplied rather than dropped: they resolve\n * whenever the source layer *is* there, which covers re-adding into the project\n * the entry came from, or into one restored from the same saved project.\n *\n * @param entry - The entry being re-added.\n * @param presentLayerIds - Ids of the layers in the target project.\n * @returns Ids of the join-target layers that cannot resolve ({@link LayerJoin}\n * has no display name).\n */\nexport function unresolvedLayerLibraryJoins(\n entry: Pick<LayerLibraryEntry, \"joins\">,\n presentLayerIds: Iterable<string>,\n): string[] {\n if (!entry.joins?.length) return [];\n const present = new Set(presentLayerIds);\n return entry.joins\n .filter((join) => join.enabled !== false && !present.has(join.joinLayerId))\n .map((join) => join.joinLayerId);\n}\n\n/**\n * Whether re-adding an entry needs a host that can read local files — used to\n * badge the entry in the Browser panel and to explain the failure in the\n * browser build, where there is no filesystem to re-read from.\n *\n * @param entry - The entry to test.\n * @returns True when only a filesystem-capable host can add it.\n */\nexport function layerLibraryEntryNeedsLocalFile(entry: LayerLibraryEntry): boolean {\n return entry.needsLocalFile === true && isReReadablePath(entry.sourcePath);\n}\n\n/** A plain JSON object (not an array, not null), or undefined. */\nfunction plainObject(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\n/** Whether every listed key on an object holds a non-empty string. */\nfunction hasStringKeys(object: Record<string, unknown>, keys: readonly string[]): boolean {\n return keys.every((key) => nonEmptyString(object[key]));\n}\n\n/**\n * Keep only the {@link LayerJoin} members a hand-edited or imported bundle got\n * right. A join missing any of its required fields resolves to nothing and would\n * sit in the Joins UI as a permanently-broken definition, so drop it rather than\n * the whole entry.\n */\nfunction validJoins(value: unknown): LayerJoin[] | undefined {\n const items = objectArray(value)?.filter((join) =>\n hasStringKeys(join, [\"id\", \"joinLayerId\", \"targetField\", \"joinField\"]),\n );\n return items && items.length > 0 ? (structuredClone(items) as unknown as LayerJoin[]) : undefined;\n}\n\n/** Keep only {@link LayerVirtualField} members that carry an id, name, and expression. */\nfunction validVirtualFields(value: unknown): LayerVirtualField[] | undefined {\n const items = objectArray(value)?.filter((field) =>\n hasStringKeys(field, [\"id\", \"name\", \"expression\"]),\n );\n return items && items.length > 0\n ? (structuredClone(items) as unknown as LayerVirtualField[])\n : undefined;\n}\n\n/**\n * Keep an {@link AttributeFormConfig} only when every field entry names the\n * feature property it configures; a field with no `field` key would apply to\n * nothing.\n */\nfunction validAttributeForm(value: unknown): AttributeFormConfig | undefined {\n const config = plainObject(value);\n if (!config || !Array.isArray(config.fields)) return undefined;\n const fields = config.fields.filter(\n (field) =>\n plainObject(field) !== undefined && nonEmptyString((field as { field?: unknown }).field),\n );\n if (fields.length === 0) return undefined;\n return structuredClone({ ...config, fields }) as unknown as AttributeFormConfig;\n}\n\n/**\n * A structurally plausible popup design, or undefined. `fields` is optional (a\n * config may carry only a title or the hover flag), so unlike the attribute\n * form an empty field list does not disqualify the block — but a config with\n * nothing set at all is dropped rather than stored as an empty object.\n */\nfunction validPopup(value: unknown): LayerPopupConfig | undefined {\n const config = plainObject(value);\n if (!config) return undefined;\n const fields = Array.isArray(config.fields)\n ? config.fields.filter(\n (field) =>\n plainObject(field) !== undefined && nonEmptyString((field as { field?: unknown }).field),\n )\n : undefined;\n const rest = { ...config };\n delete rest.fields;\n if (!fields?.length && Object.keys(rest).length === 0) return undefined;\n return structuredClone({\n ...rest,\n ...(fields?.length ? { fields } : {}),\n }) as unknown as LayerPopupConfig;\n}\n\n/** An array of plain JSON objects, or undefined when nothing usable is present. */\nfunction objectArray(value: unknown): Record<string, unknown>[] | undefined {\n if (!Array.isArray(value)) return undefined;\n const items = value.filter((item): item is Record<string, unknown> => Boolean(plainObject(item)));\n return items.length > 0 ? items : undefined;\n}\n\n/** A GeoJSON FeatureCollection shape, or undefined. */\nfunction featureCollection(value: unknown): FeatureCollection | undefined {\n const object = plainObject(value);\n if (!object || object.type !== \"FeatureCollection\" || !Array.isArray(object.features)) {\n return undefined;\n }\n return object as unknown as FeatureCollection;\n}\n\n/**\n * Coerce an untrusted entries array (an imported bundle, or a record written by\n * an older version) into valid {@link LayerLibraryEntry} records. Entries\n * without a usable id, name, or layer type are dropped, ids are de-duplicated,\n * the style is sanitized through the Style Manager's\n * {@link sanitizeLayerStylePatch} and completed against the defaults, and each\n * optional block is kept only when structurally plausible.\n *\n * Entries that cannot be re-added at all (no source, no path, no features) are\n * dropped rather than shown as permanently-failing rows.\n *\n * @param value - The raw entries value.\n * @returns Normalized, de-duplicated entries (empty when none survive).\n */\nexport function normalizeLayerLibraryEntries(value: unknown): LayerLibraryEntry[] {\n if (!Array.isArray(value)) return [];\n const entries: LayerLibraryEntry[] = [];\n const seen = new Set<string>();\n for (const raw of value) {\n const candidate = plainObject(raw);\n if (!candidate) continue;\n const id = typeof candidate.id === \"string\" ? candidate.id.trim() : \"\";\n const name = typeof candidate.name === \"string\" ? candidate.name.trim() : \"\";\n // An unknown layer type is dropped rather than coerced: every downstream\n // renderer branches on it, so a made-up value would render nothing.\n const layerType = LAYER_TYPES.includes(candidate.layerType as LayerType)\n ? (candidate.layerType as LayerType)\n : undefined;\n if (!id || !name || !layerType || seen.has(id)) continue;\n const source = plainObject(candidate.source) ?? {};\n const metadata = { ...(plainObject(candidate.metadata) ?? {}) };\n // Strip the same per-session keys the capture path drops, so a stale one on\n // an older record or an imported bundle is cleaned up on load instead of\n // replaying a dev-server proxy rewrite indefinitely.\n for (const key of TRANSIENT_METADATA_KEYS) delete metadata[key];\n // `metadata.localFilePath` is the *second* path in an entry, and the one\n // `restoreRasterLayers` re-reads — so it needs the same validation as\n // `sourcePath` below, not a free pass because it happens to live in\n // metadata. Without this a shared bundle could keep a valid-looking\n // `sourcePath` while pointing the raster restore at any file on the\n // importing user's disk.\n if (\"localFilePath\" in metadata && !isReReadablePath(metadata.localFilePath)) {\n delete metadata.localFilePath;\n }\n // A bundle is shareable, so its `sourcePath` is untrusted: keep it only when\n // it is an absolute, traversal-free path a host could legitimately re-read.\n // Without this a crafted entry could point `onAddFilePath` at any file on the\n // importing user's disk.\n const sourcePath = isReReadablePath(candidate.sourcePath) ? candidate.sourcePath : undefined;\n const geojson = featureCollection(candidate.geojson);\n // Same \"is there anything to re-add from\" gate as the capture path, so a\n // hand-edited bundle cannot introduce an entry that always fails. Both path\n // conventions count (a local raster's only path is `metadata.localFilePath`,\n // already validated above), and so does a control-painted entry's\n // `metadata.embeddedGeoJSON`, which holds its features instead of `geojson`.\n if (\n !hasRestorableLayerSource({ source, metadata }) &&\n !sourcePath &&\n !nonEmptyString(metadata.localFilePath) &&\n featureCount(geojson) === 0 &&\n featureCount(featureCollection(metadata.embeddedGeoJSON)) === 0\n ) {\n continue;\n }\n const opacity =\n typeof candidate.opacity === \"number\" &&\n Number.isFinite(candidate.opacity) &&\n candidate.opacity >= 0 &&\n candidate.opacity <= 1\n ? candidate.opacity\n : 1;\n const normalized: LayerLibraryEntry = {\n id,\n name,\n addedAt: typeof candidate.addedAt === \"string\" ? candidate.addedAt : \"\",\n layerType,\n source: structuredClone(source),\n style: { ...DEFAULT_LAYER_STYLE, ...sanitizeLayerStylePatch(candidate.style) },\n opacity,\n metadata: structuredClone(metadata),\n ...(sourcePath ? { sourcePath } : {}),\n ...(() => {\n // Each block is kept only when its members carry the fields their engine\n // needs; a malformed member is dropped rather than the whole entry.\n const joins = validJoins(candidate.joins);\n const virtualFields = validVirtualFields(candidate.virtualFields);\n const attributeForm = validAttributeForm(candidate.attributeForm);\n const popup = validPopup(candidate.popup);\n const descriptiveMetadata = normalizeLayerDescriptiveMetadata(\n candidate.descriptiveMetadata,\n );\n return {\n ...(joins ? { joins } : {}),\n ...(virtualFields ? { virtualFields } : {}),\n ...(attributeForm ? { attributeForm } : {}),\n ...(popup ? { popup } : {}),\n ...(descriptiveMetadata ? { descriptiveMetadata } : {}),\n };\n })(),\n ...(geojson ? { geojson: structuredClone(geojson) } : {}),\n // `needsLocalFile` only means anything with a path to read.\n ...(candidate.needsLocalFile === true && sourcePath ? { needsLocalFile: true } : {}),\n };\n // The per-entry size ceiling is enforced here too, not only on capture: an\n // imported bundle is produced elsewhere, so without this a crafted (or just\n // hand-assembled) file could push tens of MB of embedded features per entry\n // into IndexedDB, exactly the \"library as a data store\" outcome the cap\n // exists to prevent.\n if (jsonByteLength(normalized) > MAX_LAYER_LIBRARY_ENTRY_BYTES) continue;\n seen.add(id);\n entries.push(normalized);\n if (entries.length >= MAX_LAYER_LIBRARY_ENTRIES) break;\n }\n return entries;\n}\n\n/**\n * Copy of an entry with per-user credentials removed from its source.\n *\n * `source` fields that can hold a per-user credential are removed on export.\n * `requestHeaders`, for example, carries the bearer token / API key an\n * authenticated 3D Tiles layer was added with (see\n * `persistedThreeDTilesRequestHeaders`, which keeps non-Google auth headers when\n * persisting into a project file).\n *\n * Kept in the local IndexedDB entry so re-adding on this machine still works,\n * and dropped only from the bundle: Export is pitched as a one-click \"share with\n * your team\" action, which makes it a far more direct exposure path than handing\n * someone a whole project file. A recipient re-enters their own credentials,\n * which is the correct outcome.\n *\n * The names come from `isCredentialFieldName`, so this path and project export\n * share one registry instead of drifting apart.\n */\nfunction redactEntryForExport(entry: LayerLibraryEntry): LayerLibraryEntry {\n const source = { ...entry.source };\n let changed = false;\n for (const key of Object.keys(source)) {\n if (isCredentialFieldName(key)) {\n delete source[key];\n changed = true;\n }\n }\n // Sweep every URL-shaped value in `source` rather than naming the fields that\n // hold one. Different layer types put their URL in different places — `url`,\n // `data` (a GeoJSON source pointing at a URL, which\n // `hasRestorableLayerSource` already treats as re-fetchable), `tiles[]` — and\n // a per-field list has to be extended for each new one, with a silent\n // credential leak as the cost of forgetting.\n for (const [key, value] of Object.entries(source)) {\n const redacted = redactSourceValue(value);\n if (redacted !== value) {\n source[key] = redacted;\n changed = true;\n }\n }\n // `metadata.originalUrl` is the template a tile layer restores from, so it\n // carries the same credential as `source.tiles`.\n let metadata = entry.metadata;\n if (typeof metadata.originalUrl === \"string\") {\n const redacted = redactUrlCredentials(metadata.originalUrl);\n if (redacted !== metadata.originalUrl) {\n metadata = { ...metadata, originalUrl: redacted };\n changed = true;\n }\n }\n return changed ? { ...entry, source, metadata } : entry;\n}\n\n/** Depth limit for the export sweep, so a pathological source cannot recurse far. */\nconst MAX_REDACT_DEPTH = 6;\n\n/**\n * Whether a value is GeoJSON data rather than source configuration. The sweep\n * stops at these: `source.data` can hold an inline FeatureCollection, and\n * descending into it would both walk every feature and *rewrite* any feature\n * property that happens to look like a URL with a `key=` parameter — corrupting\n * the user's data in the name of protecting it. Credentials live in\n * configuration, not in features.\n */\nfunction isGeoJsonPayload(value: Record<string, unknown>): boolean {\n return (\n value.type === \"FeatureCollection\" ||\n value.type === \"Feature\" ||\n value.type === \"GeometryCollection\"\n );\n}\n\n/**\n * Redact a `source` value of any shape: strings are treated as possible URLs,\n * arrays as lists of them (tile templates), and nested objects are swept\n * recursively — a layer type that groups its endpoint and credentials as\n * `{ store: { url, headers } }` must not slip a token past the export just\n * because it is one level deeper. Nested `requestHeaders`-style keys are dropped\n * the same way the top level drops them.\n *\n * GeoJSON payloads are left alone (see {@link isGeoJsonPayload}) and recursion is\n * depth-capped. Returns the original reference when nothing changed, so callers\n * can detect that by identity.\n */\nfunction redactSourceValue(value: unknown, depth = 0): unknown {\n if (typeof value === \"string\") return redactUrlCredentials(value);\n if (depth >= MAX_REDACT_DEPTH) {\n // Fail closed, matching `redactConfigurationValue` in credentials.ts: no\n // built-in layer needs configuration nested this deep to be re-addable, and\n // handing the value back unswept would let a credential ride out of the\n // bundle merely by sitting past the traversal cap. Only the exported bundle\n // loses it; the local IndexedDB entry is untouched.\n return undefined;\n }\n if (Array.isArray(value)) {\n const redacted = value.map((item) => redactSourceValue(item, depth + 1));\n return redacted.some((item, index) => item !== value[index]) ? redacted : value;\n }\n const object = plainObject(value);\n if (!object || isGeoJsonPayload(object)) return value;\n let changed = false;\n const out: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(object)) {\n if (isCredentialFieldName(key)) {\n changed = true;\n continue;\n }\n const redacted = redactSourceValue(nested, depth + 1);\n if (redacted !== nested) changed = true;\n out[key] = redacted;\n }\n return changed ? out : value;\n}\n\n/**\n * Serialize Layer Library entries into the shareable bundle JSON written by the\n * Export action and read back by {@link parseLayerLibrary}. Per-user credentials\n * are redacted first (see {@link EXPORT_REDACTED_SOURCE_KEYS}).\n *\n * @param entries - The entries to export.\n * @returns Pretty-printed bundle JSON.\n */\nexport function serializeLayerLibrary(entries: LayerLibraryEntry[]): string {\n return JSON.stringify(\n {\n type: LAYER_LIBRARY_BUNDLE_TYPE,\n version: LAYER_LIBRARY_BUNDLE_VERSION,\n entries: entries.map(redactEntryForExport),\n },\n null,\n 2,\n );\n}\n\n/**\n * Parse a Layer Library bundle produced by {@link serializeLayerLibrary} (a\n * bare entries array is also accepted, so hand-authored files work). Entries\n * are normalized through {@link normalizeLayerLibraryEntries}.\n *\n * @param json - The bundle file content.\n * @returns The normalized entries.\n * @throws Error when the JSON is not a layer-library bundle or holds no usable\n * entries.\n */\nexport function parseLayerLibrary(json: string): LayerLibraryEntry[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"Not a valid layer library file (invalid JSON).\");\n }\n let rawEntries: unknown = null;\n if (Array.isArray(parsed)) {\n rawEntries = parsed;\n } else if (plainObject(parsed)?.type === LAYER_LIBRARY_BUNDLE_TYPE) {\n // Refuse a bundle from a newer format rather than misreading it with v1\n // semantics. Bare arrays stay accepted for hand-authored files.\n if (plainObject(parsed)?.version !== LAYER_LIBRARY_BUNDLE_VERSION) {\n throw new Error(\"Unsupported layer library version.\");\n }\n rawEntries = plainObject(parsed)?.entries;\n }\n if (rawEntries === null) {\n throw new Error(\"Not a valid layer library file.\");\n }\n const entries = normalizeLayerLibraryEntries(rawEntries);\n if (entries.length === 0) {\n throw new Error(\"The layer library file holds no usable entries.\");\n }\n return entries;\n}\n\n/**\n * Create a fresh unique id for a new library entry.\n *\n * @returns A random id (UUID when available).\n */\nexport function createLayerLibraryEntryId(): string {\n if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n return crypto.randomUUID();\n }\n return `layer-lib-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n","/**\n * The Style Manager, Layer Library and Template Library. The app-level lists\n * live outside the project lifecycle (persisted by the desktop app); only the\n * project-scoped style list is saved with the project.\n */\nimport { MAX_LAYER_LIBRARY_ENTRIES } from \"../layer-library\";\nimport type { LayerLibraryEntry, ProjectTemplateEntry, StyleLibraryEntry } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface LibrariesSlice {\n /**\n * App-level Style Manager library (issue #1294). Lives outside the project\n * lifecycle: never serialized into the project file, untouched by\n * newProject/loadProject, and persisted by the desktop app (IndexedDB).\n */\n styleLibrary: StyleLibraryEntry[];\n /**\n * App-level Layer Library (issue #1520) — the Browser panel's My Data\n * section. Like {@link styleLibrary} it lives outside the project lifecycle:\n * never serialized into the project file, untouched by newProject/loadProject,\n * and persisted by the desktop app (IndexedDB). Most recently saved first.\n */\n layerLibrary: LayerLibraryEntry[];\n /**\n * App-level Template Library. Persisted by the desktop app (IndexedDB).\n */\n templateLibrary: ProjectTemplateEntry[];\n /**\n * Project-scoped Style Manager entries (issue #1294), serialized into the\n * `.geolibre.json` `styleLibrary` array and replaced on project load.\n */\n projectStyleLibrary: StyleLibraryEntry[];\n\n /**\n * Replace the app-level style library wholesale. Used by the persistence\n * layer on startup and by bundle imports.\n */\n setStyleLibrary: (entries: StyleLibraryEntry[]) => void;\n /**\n * Insert or replace (matching by `id`) a Style Manager entry in the given\n * scope. Scope-local, like {@link deleteStyleLibraryEntry}: the other\n * scope's list is never touched, since a same id there can belong to an\n * unrelated entry after loading a project authored elsewhere. Project-scope\n * saves mark the project dirty.\n */\n saveStyleLibraryEntry: (entry: StyleLibraryEntry, scope?: \"app\" | \"project\") => void;\n /**\n * Remove a Style Manager entry by id. When `scope` is given only that list\n * is touched — the two scopes can legitimately hold the same id after\n * loading a project authored elsewhere, and deleting a project entry must\n * not erase a local app-library style (or vice versa). Omitting `scope`\n * removes the id from both lists.\n */\n deleteStyleLibraryEntry: (id: string, scope?: \"app\" | \"project\") => void;\n\n /**\n * Replace the app-level Layer Library wholesale. Used by the persistence\n * layer on startup and by bundle imports.\n */\n setLayerLibrary: (entries: LayerLibraryEntry[]) => void;\n /**\n * Insert or replace (matching by `id`) a Layer Library entry. New entries go\n * to the front so the most recently saved layer leads the My Data section.\n */\n saveLayerLibraryEntry: (entry: LayerLibraryEntry) => void;\n /** Rename a Layer Library entry; a blank name is ignored. */\n renameLayerLibraryEntry: (id: string, name: string) => void;\n /** Remove a Layer Library entry by id. */\n deleteLayerLibraryEntry: (id: string) => void;\n\n /** Replace the app-level template library wholesale. */\n setTemplateLibrary: (templates: ProjectTemplateEntry[]) => void;\n /** Insert or replace a template in the Template Library. */\n saveTemplateEntry: (entry: ProjectTemplateEntry) => void;\n /** Remove a template entry by id from the Template Library. */\n deleteTemplateEntry: (id: string) => void;\n}\n\nexport const createLibrariesSlice: SliceCreator<LibrariesSlice> = (set) => ({\n styleLibrary: [],\n layerLibrary: [],\n templateLibrary: [],\n projectStyleLibrary: [],\n\n setStyleLibrary: (entries) => set({ styleLibrary: entries }),\n saveStyleLibraryEntry: (entry, scope = \"app\") =>\n set((s) => {\n const upsert = (list: StyleLibraryEntry[]) =>\n list.some((e) => e.id === entry.id)\n ? list.map((e) => (e.id === entry.id ? entry : e))\n : [...list, entry];\n if (scope === \"project\") {\n return {\n projectStyleLibrary: upsert(s.projectStyleLibrary),\n isDirty: true,\n };\n }\n // App-level saves don't touch the project file, so no dirty flag.\n return { styleLibrary: upsert(s.styleLibrary) };\n }),\n deleteStyleLibraryEntry: (id, scope) =>\n set((s) => {\n const inProject = scope !== \"app\" && s.projectStyleLibrary.some((e) => e.id === id);\n const inLibrary = scope !== \"project\" && s.styleLibrary.some((e) => e.id === id);\n // Keep untouched scopes reference-stable so the IndexedDB\n // persistence (which watches the styleLibrary reference) does not\n // rewrite an unchanged library.\n return {\n styleLibrary: inLibrary ? s.styleLibrary.filter((e) => e.id !== id) : s.styleLibrary,\n projectStyleLibrary: inProject\n ? s.projectStyleLibrary.filter((e) => e.id !== id)\n : s.projectStyleLibrary,\n isDirty: s.isDirty || inProject,\n };\n }),\n\n // The entry cap is applied on every write, not just when reading\n // untrusted input, so ordinary use (repeated saves, importing several\n // bundles over time) cannot grow the library past it. Mirrors\n // `writeBrowserFavorites`, which slices to MAX_FAVORITES on each write.\n setLayerLibrary: (entries) => set({ layerLibrary: entries.slice(0, MAX_LAYER_LIBRARY_ENTRIES) }),\n saveLayerLibraryEntry: (entry) =>\n set((s) => ({\n // App-level saves don't touch the project file, so no dirty flag\n // (mirrors saveStyleLibraryEntry's \"app\" scope). A new entry goes to\n // the front, so at the cap the oldest falls off the end.\n layerLibrary: s.layerLibrary.some((e) => e.id === entry.id)\n ? s.layerLibrary.map((e) => (e.id === entry.id ? entry : e))\n : [entry, ...s.layerLibrary].slice(0, MAX_LAYER_LIBRARY_ENTRIES),\n })),\n renameLayerLibraryEntry: (id, name) =>\n set((s) => {\n const trimmed = name.trim();\n // Keep the array reference stable for a no-op rename so the\n // IndexedDB persistence (which watches the reference) skips a write.\n if (!trimmed || !s.layerLibrary.some((e) => e.id === id && e.name !== trimmed)) {\n return {};\n }\n return {\n layerLibrary: s.layerLibrary.map((e) => (e.id === id ? { ...e, name: trimmed } : e)),\n };\n }),\n deleteLayerLibraryEntry: (id) =>\n set((s) =>\n s.layerLibrary.some((e) => e.id === id)\n ? { layerLibrary: s.layerLibrary.filter((e) => e.id !== id) }\n : {},\n ),\n\n setTemplateLibrary: (templates) => set({ templateLibrary: templates }),\n saveTemplateEntry: (entry) =>\n set((s) => ({\n templateLibrary: s.templateLibrary.some((t) => t.id === entry.id)\n ? s.templateLibrary.map((t) => (t.id === entry.id ? entry : t))\n : [...s.templateLibrary, entry],\n })),\n deleteTemplateEntry: (id) =>\n set((s) => ({\n templateLibrary: s.templateLibrary.filter((t) => t.id !== id),\n })),\n});\n","/**\n * The map workspace: the primary camera, the basemap, project preferences, the\n * multi-map grid and its secondary panes, and which engine draws the primary\n * map. The basemap fields are tracked by undo history; the camera is not.\n */\nimport { v4 as uuidv4 } from \"uuid\";\nimport { createDefaultMapView, normalizeBlankBackgroundColor } from \"../project\";\nimport { DEFAULT_ELLIPSOID_ID } from \"../ellipsoids\";\nimport type { PlanetaryBasemap } from \"../ellipsoids\";\nimport {\n DEFAULT_BASEMAP,\n DEFAULT_MAP_GRID_LAYOUT,\n DEFAULT_PRIMARY_RENDERER,\n DEFAULT_PROJECT_PREFERENCES,\n MAX_MAP_GRID_DIM,\n type MapGridLayout,\n type MapRendererKind,\n type MapViewState,\n type ProjectPreferences,\n type SecondaryMapView,\n} from \"../types\";\nimport type { AppState, SliceCreator } from \"./types\";\n\n/** An explicit background choice replaces the active renderer's override. */\nfunction preferencesForBasemap(state: AppState, ellipsoidId = state.preferences.map.ellipsoidId) {\n // Each engine keeps its own persisted basemap override, so this names them.\n /* eslint-disable local/no-renderer-kind-checks -- per-engine persisted basemap settings */\n const clearMapbox =\n state.primaryRenderer === \"mapbox\" && state.preferences.map.mapboxStyleUrl !== undefined;\n // Any Cesium or ArcGIS pane, not only a primary one: split panes pick the\n // renderer independently, and a pinned globe imagery or Esri style would\n // otherwise ignore the picker.\n const clearCesium =\n state.preferences.map.cesiumBasemap !== \"project\" &&\n (state.primaryRenderer === \"cesium\" ||\n state.secondaryMapViews.some((pane) => pane.viewKind === \"cesium\"));\n const clearArcgis =\n state.preferences.map.arcgisBasemap !== undefined &&\n (state.primaryRenderer === \"arcgis\" ||\n state.secondaryMapViews.some((pane) => pane.viewKind === \"arcgis\"));\n /* eslint-enable local/no-renderer-kind-checks */\n if (\n !clearMapbox &&\n !clearCesium &&\n !clearArcgis &&\n ellipsoidId === state.preferences.map.ellipsoidId\n )\n return state.preferences;\n return {\n ...state.preferences,\n map: {\n ...state.preferences.map,\n ...(clearMapbox ? { mapboxStyleUrl: undefined } : {}),\n ...(clearCesium ? { cesiumBasemap: \"project\" as const } : {}),\n ...(clearArcgis ? { arcgisBasemap: undefined } : {}),\n ellipsoidId,\n },\n };\n}\n\n/** True when two camera states have identical center/zoom/bearing/pitch. */\nfunction sameCamera(a: MapViewState, b: MapViewState): boolean {\n return (\n a.center[0] === b.center[0] &&\n a.center[1] === b.center[1] &&\n a.zoom === b.zoom &&\n a.bearing === b.bearing &&\n a.pitch === b.pitch\n );\n}\n\n/** Clamp a requested grid row/column count into the supported [1, MAX] range. */\nfunction clampGridDim(value: number): number {\n if (!Number.isFinite(value)) return 1;\n return Math.max(1, Math.min(MAX_MAP_GRID_DIM, Math.floor(value)));\n}\n\n/**\n * Pick a grid that holds at least `total` panes within the supported\n * `MAX_MAP_GRID_DIM x MAX_MAP_GRID_DIM` bound, minimizing empty cells first and\n * then preferring a column count close to (and, on ties, no smaller than)\n * `preferredCols` so removing a pane keeps the layout's orientation. Used when\n * collapsing the grid after a pane is removed.\n *\n * Bounding both dimensions matters: a prime `total` (e.g. 5 panes left after\n * removing one from a 2x3 grid) has no gap-free factor pair inside the bound, so\n * the only gap-free options (1x5 / 5x1) would exceed `MAX_MAP_GRID_DIM`. In that\n * case we accept the smallest bounded grid with one empty trailing cell (2x3)\n * rather than returning an out-of-range dimension.\n */\nfunction fitGrid(total: number, preferredCols: number): { rows: number; cols: number } {\n if (total <= 1) return { rows: 1, cols: 1 };\n let best: {\n rows: number;\n cols: number;\n empty: number;\n score: number;\n } | null = null;\n for (let rows = 1; rows <= MAX_MAP_GRID_DIM; rows++) {\n for (let cols = 1; cols <= MAX_MAP_GRID_DIM; cols++) {\n const capacity = rows * cols;\n if (capacity < total) continue;\n const empty = capacity - total;\n const score = Math.abs(cols - preferredCols);\n // Fewest empty cells wins; then the column count closest to\n // preferredCols; then, on a tie, the larger column count (favoring wider,\n // side-by-side layouts over tall ones).\n const better =\n best === null ||\n empty < best.empty ||\n (empty === best.empty &&\n (score < best.score || (score === best.score && cols > best.cols)));\n if (better) best = { rows, cols, empty, score };\n }\n }\n return best ? { rows: best.rows, cols: best.cols } : { rows: 1, cols: 1 };\n}\n\nexport interface MapViewSlice {\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n blankBackgroundColor: string | null;\n preferences: ProjectPreferences;\n /**\n * Multi-map grid layout (issue: split/grid view). A 1x1 grid is the normal\n * single-map workspace. `rows * cols` panes are shown; pane 0 is the primary\n * map driven by `mapView` / `basemap*`, panes 1.. are `secondaryMapViews`.\n */\n mapLayout: MapGridLayout;\n /**\n * Secondary map panes (everything past the primary pane). The store keeps\n * exactly `rows * cols - 1` entries in sync with `mapLayout`.\n */\n secondaryMapViews: SecondaryMapView[];\n /** User-entered label for the primary pane (shown only in multi-map mode). */\n primaryMapLabel: string;\n /**\n * Which engine draws the primary map area (issue #2217): the 2D MapLibre map\n * or the 3D Cesium globe. Both render the same store state, so switching\n * keeps the camera, basemap, layers, groups, visibility, and opacity — it\n * only changes what draws them. Independent of `mapLayout`: switching never\n * adds or removes panes.\n */\n primaryRenderer: MapRendererKind;\n\n setMapView: (view: Partial<MapViewState>, markDirty?: boolean) => void;\n /**\n * Resize the map grid. Clamps `rows`/`cols` into range and grows/shrinks\n * `secondaryMapViews` so it always holds `rows * cols - 1` panes; new panes\n * clone the primary map's current camera and basemap.\n */\n setMapGrid: (rows: number, cols: number) => void;\n /** Toggle synchronized camera across all panes. */\n setSyncView: (syncView: boolean) => void;\n /** Patch one secondary pane's camera by id (no-op if the id is unknown). */\n setSecondaryMapView: (id: string, view: Partial<MapViewState>, markDirty?: boolean) => void;\n /**\n * Override a layer's visibility in one secondary pane (no-op if the pane id is\n * unknown). The override forces the layer visible/hidden in that pane only,\n * independent of the primary map's visibility.\n */\n setSecondaryLayerVisibility: (id: string, layerId: string, visible: boolean) => void;\n /** Set the primary pane's custom label. */\n setPrimaryMapLabel: (label: string) => void;\n /**\n * Switch the primary map area between the 2D map and the 3D globe (no-op if\n * unchanged). Touches nothing else in the store, so the shared camera, layer,\n * and basemap state carries straight across the swap.\n */\n setPrimaryRenderer: (renderer: MapRendererKind) => void;\n /** Set one secondary pane's custom label (no-op if the id is unknown). */\n setSecondaryMapLabel: (id: string, label: string) => void;\n /**\n * Switch one secondary pane between the 2D map and the 3D globe (no-op if the\n * id is unknown or the kind is unchanged).\n */\n setSecondaryViewKind: (id: string, viewKind: NonNullable<SecondaryMapView[\"viewKind\"]>) => void;\n /** Remove one secondary pane and collapse the grid back toward 1x1. */\n removeSecondaryMapView: (id: string) => void;\n setBasemapStyleUrl: (url: string) => void;\n /**\n * Apply a planetary basemap and sync the project's ellipsoid to the body it\n * depicts, so measurements and the globe control use that body's radius. Used\n * by both the basemap picker and the Layers-panel planet switcher.\n */\n applyPlanetaryBasemap: (basemap: PlanetaryBasemap) => void;\n /**\n * Return to Earth: apply `styleUrl` (typically the Earth basemap that was\n * active before a planet was selected, e.g. Liberty) and reset the ellipsoid\n * to Earth. Used when a planet is deselected in the switcher.\n */\n restoreEarthBasemap: (styleUrl: string) => void;\n setBasemapVisible: (visible: boolean) => void;\n setBasemapOpacity: (opacity: number) => void;\n setBlankBackgroundColor: (color: string | null) => void;\n setPreferences: (preferences: ProjectPreferences) => void;\n}\n\nexport const createMapViewSlice: SliceCreator<MapViewSlice> = (set) => ({\n mapView: createDefaultMapView(),\n basemapStyleUrl: DEFAULT_BASEMAP,\n basemapVisible: true,\n basemapOpacity: 1,\n blankBackgroundColor: null,\n preferences: DEFAULT_PROJECT_PREFERENCES,\n mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT },\n secondaryMapViews: [],\n primaryMapLabel: \"\",\n primaryRenderer: DEFAULT_PRIMARY_RENDERER,\n\n setMapView: (view, markDirty = false) =>\n set((s) => ({\n mapView: { ...s.mapView, ...view },\n isDirty: markDirty || s.isDirty,\n })),\n setMapGrid: (rows, cols) =>\n set((s) => {\n const clampedRows = clampGridDim(rows);\n const clampedCols = clampGridDim(cols);\n const desiredSecondary = clampedRows * clampedCols - 1;\n let secondaryMapViews = s.secondaryMapViews;\n if (desiredSecondary < secondaryMapViews.length) {\n secondaryMapViews = secondaryMapViews.slice(0, desiredSecondary);\n } else if (desiredSecondary > secondaryMapViews.length) {\n const additions: SecondaryMapView[] = [];\n for (let i = secondaryMapViews.length; i < desiredSecondary; i++) {\n // New panes start as a clone of the primary map's camera and (by\n // having no overrides) inherit its layer visibility, so the\n // comparison begins from the same view the user is looking at.\n additions.push({\n id: uuidv4(),\n view: { ...s.mapView },\n layerVisibility: {},\n });\n }\n secondaryMapViews = [...secondaryMapViews, ...additions];\n }\n return {\n mapLayout: {\n ...s.mapLayout,\n rows: clampedRows,\n cols: clampedCols,\n },\n secondaryMapViews,\n isDirty: true,\n };\n }),\n setSyncView: (syncView) =>\n set((s) => ({\n mapLayout: { ...s.mapLayout, syncView },\n isDirty: true,\n })),\n setSecondaryMapView: (id, view, markDirty = false) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n const merged = { ...pane.view, ...view };\n // Skip value-identical writes: a programmatic `applyView` (camera\n // sync, initial load) fires \"moveend\" too, so without this guard\n // each pane re-stores the same camera it was just given, churning a\n // new `secondaryMapViews` array and re-rendering every subscriber.\n if (sameCamera(pane.view, merged)) return pane;\n changed = true;\n return { ...pane, view: merged };\n });\n if (!changed) return s;\n return {\n secondaryMapViews,\n isDirty: markDirty || s.isDirty,\n };\n }),\n setSecondaryLayerVisibility: (id, layerId, visible) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n changed = true;\n return {\n ...pane,\n layerVisibility: { ...pane.layerVisibility, [layerId]: visible },\n };\n });\n if (!changed) return s;\n return { secondaryMapViews, isDirty: true };\n }),\n setPrimaryMapLabel: (label) => set({ primaryMapLabel: label, isDirty: true }),\n setPrimaryRenderer: (renderer) =>\n set((s) => (s.primaryRenderer === renderer ? s : { primaryRenderer: renderer, isDirty: true })),\n setSecondaryMapLabel: (id, label) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n changed = true;\n return { ...pane, label };\n });\n if (!changed) return s;\n return { secondaryMapViews, isDirty: true };\n }),\n setSecondaryViewKind: (id, viewKind) =>\n set((s) => {\n let changed = false;\n const secondaryMapViews = s.secondaryMapViews.map((pane) => {\n if (pane.id !== id) return pane;\n // Treat an absent viewKind as \"maplibre\" so switching a legacy pane\n // to maplibre is a no-op rather than a churned array.\n if ((pane.viewKind ?? \"maplibre\") === viewKind) return pane;\n changed = true;\n return { ...pane, viewKind };\n });\n if (!changed) return s;\n return { secondaryMapViews, isDirty: true };\n }),\n removeSecondaryMapView: (id) =>\n set((s) => {\n const secondaryMapViews = s.secondaryMapViews.filter((pane) => pane.id !== id);\n if (secondaryMapViews.length === s.secondaryMapViews.length) {\n return s;\n }\n // Collapse to a gap-free grid that fits the remaining panes, keeping\n // the layout's orientation as close as possible to the current one.\n const total = secondaryMapViews.length + 1;\n const { rows, cols } = fitGrid(total, s.mapLayout.cols);\n return {\n secondaryMapViews,\n mapLayout: { ...s.mapLayout, rows, cols },\n isDirty: true,\n };\n }),\n setBasemapStyleUrl: (url) =>\n set((state) => ({\n basemapStyleUrl: url,\n preferences: preferencesForBasemap(state),\n isDirty: true,\n })),\n applyPlanetaryBasemap: (basemap) =>\n set((state) => ({\n basemapStyleUrl: basemap.styleUrl,\n preferences: preferencesForBasemap(state, basemap.ellipsoidId),\n isDirty: true,\n })),\n restoreEarthBasemap: (styleUrl) =>\n set((state) => ({\n basemapStyleUrl: styleUrl,\n preferences: preferencesForBasemap(state, DEFAULT_ELLIPSOID_ID),\n isDirty: true,\n })),\n setBasemapVisible: (visible) => set({ basemapVisible: visible, isDirty: true }),\n setBasemapOpacity: (opacity) => set({ basemapOpacity: opacity, isDirty: true }),\n setBlankBackgroundColor: (color) =>\n set({ blankBackgroundColor: normalizeBlankBackgroundColor(color), isDirty: true }),\n setPreferences: (preferences) => set({ preferences, isDirty: true }),\n});\n","/**\n * Saved processing models (issue #344) and the Processing History log\n * (issue #1292). Both are saved with the project.\n */\nimport { MAX_PROCESSING_HISTORY, type ProcessingModel, type ProcessingRun } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface ProcessingSlice {\n /** Saved processing pipelines (batch/model chaining; issue #344). */\n models: ProcessingModel[];\n /** Recorded processing tool runs, oldest first (Processing History; #1292). */\n processingHistory: ProcessingRun[];\n /**\n * Object-Based Analysis workbench state saved with the project (#3053), an\n * app-owned JSON object; null until the workbench is used.\n */\n obiaWorkbench: Record<string, unknown> | null;\n\n /** Insert a new model or replace an existing one matching by `id`. */\n saveModel: (model: ProcessingModel) => void;\n /** Remove a saved model by id. */\n deleteModel: (id: string) => void;\n\n /** Append a processing run to the history (bounded, de-duped by id; #1292). */\n addProcessingRun: (run: ProcessingRun) => void;\n /** Patch a recorded run by id (no-op if absent), e.g. to add output layers. */\n updateProcessingRun: (id: string, patch: Partial<Omit<ProcessingRun, \"id\">>) => void;\n /** Drop all recorded processing runs. */\n clearProcessingHistory: () => void;\n /**\n * Replace the saved workbench state. `markDirty` is false when the app is\n * only mirroring state it just restored, so opening a project stays clean.\n */\n setObiaWorkbench: (value: Record<string, unknown> | null, markDirty?: boolean) => void;\n}\n\nexport const createProcessingSlice: SliceCreator<ProcessingSlice> = (set) => ({\n models: [],\n processingHistory: [],\n obiaWorkbench: null,\n\n saveModel: (model) =>\n set((s) => {\n const exists = s.models.some((m) => m.id === model.id);\n const models = exists\n ? s.models.map((m) => (m.id === model.id ? model : m))\n : [...s.models, model];\n return { models, isDirty: true };\n }),\n deleteModel: (id) =>\n set((s) => ({\n models: s.models.filter((m) => m.id !== id),\n isDirty: true,\n })),\n\n addProcessingRun: (run) =>\n set((s) => {\n // Ignore a duplicate id so updateProcessingRun stays unambiguous.\n if (s.processingHistory.some((r) => r.id === run.id)) return s;\n const processingHistory = [...s.processingHistory, run].slice(-MAX_PROCESSING_HISTORY);\n return { processingHistory, isDirty: true };\n }),\n updateProcessingRun: (id, patch) =>\n set((s) => {\n if (!s.processingHistory.some((r) => r.id === id)) return s;\n return {\n processingHistory: s.processingHistory.map((r) =>\n r.id === id ? { ...r, ...patch, id: r.id } : r,\n ),\n isDirty: true,\n };\n }),\n clearProcessingHistory: () =>\n set((s) => (s.processingHistory.length === 0 ? s : { processingHistory: [], isDirty: true })),\n setObiaWorkbench: (obiaWorkbench, markDirty = true) =>\n set(markDirty ? { obiaWorkbench, isDirty: true } : { obiaWorkbench }),\n});\n","/**\n * Project content that sits beside the layers: review comments (issue #1518),\n * Dashboard widgets (issue #401) and the story map. All of it is saved with the\n * project; `comments` and `storymap` are also tracked by undo history.\n */\nimport {\n DEFAULT_DASHBOARD_COLUMNS,\n DEFAULT_STORY_MAP,\n MAX_DASHBOARD_COLUMNS,\n MIN_DASHBOARD_COLUMNS,\n type CommentReply,\n type DashboardWidget,\n type ProjectBookmark,\n type ProjectBookmarkGroup,\n type ProjectComment,\n type ProjectInteraction,\n type StoryChapter,\n type StoryMap,\n} from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport interface ProjectContentSlice {\n storymap: StoryMap | null;\n /** Saved Dashboard panel chart widgets (issue #401). */\n widgets: DashboardWidget[];\n /** Number of columns in the Dashboard widget grid. */\n dashboardColumns: number;\n /** Saved map views from the Bookmarks panel, in panel order (#2869). */\n bookmarks: ProjectBookmark[];\n /** Folders the bookmarks are organized into. */\n bookmarkGroups: ProjectBookmarkGroup[];\n /** Anchored review comments on map points or features (issue #1518). */\n comments: ProjectComment[];\n /**\n * The loaded project's startup `interaction` block (issue #2688). Applied\n * once on load and written back unchanged on save; live Identify and control\n * changes do not edit it.\n */\n projectInteraction: ProjectInteraction | null;\n\n /** Append a new dashboard widget. */\n addWidget: (widget: DashboardWidget) => void;\n /** Patch an existing dashboard widget by id (no-op if absent). Merges, so an\n * omitted key keeps its current value; use replaceWidget to clear one. */\n updateWidget: (id: string, patch: Partial<Omit<DashboardWidget, \"id\">>) => void;\n /** Swap an existing dashboard widget for a complete new record, keeping its\n * id and position (no-op if absent). Unlike updateWidget this does not merge,\n * so fields the caller omits are cleared — what the widget editor needs to\n * persist an emptied title, color, prefix, or suffix. */\n replaceWidget: (id: string, widget: Omit<DashboardWidget, \"id\">) => void;\n /** Remove a dashboard widget by id. */\n removeWidget: (id: string) => void;\n /** Move a widget to a new index, clamped into range, preserving the rest. */\n moveWidget: (id: string, toIndex: number) => void;\n /** Set the Dashboard widget-grid column count (clamped into range). */\n setDashboardColumns: (columns: number) => void;\n\n setStorymap: (storymap: StoryMap | null) => void;\n updateStorymapSettings: (patch: Partial<Omit<StoryMap, \"chapters\">>) => void;\n addStoryChapter: (chapter: StoryChapter, atIndex?: number) => void;\n updateStoryChapter: (id: string, patch: Partial<StoryChapter>) => void;\n removeStoryChapter: (id: string) => void;\n moveStoryChapter: (id: string, targetIndex: number) => void;\n addComment: (comment: ProjectComment) => void;\n replyToComment: (commentId: string, reply: CommentReply) => void;\n toggleResolveComment: (commentId: string, resolved?: boolean) => void;\n deleteComment: (commentId: string) => void;\n setComments: (comments: ProjectComment[]) => void;\n /**\n * Replace every bookmark and folder (the Bookmarks panel writes its whole\n * state back after each edit). Marks the project dirty.\n */\n setBookmarks: (bookmarks: ProjectBookmark[], bookmarkGroups: ProjectBookmarkGroup[]) => void;\n}\n\nexport const createProjectContentSlice: SliceCreator<ProjectContentSlice> = (set) => ({\n storymap: null,\n widgets: [],\n dashboardColumns: DEFAULT_DASHBOARD_COLUMNS,\n bookmarks: [],\n bookmarkGroups: [],\n comments: [],\n projectInteraction: null,\n\n setBookmarks: (bookmarks, bookmarkGroups) => set({ bookmarks, bookmarkGroups, isDirty: true }),\n\n addComment: (comment) =>\n set((s) => {\n // Ignore a duplicate id: the WebSocket relay echoes our own\n // comment-mutation back to the sender, and a reconnect can replay\n // recent history, so de-dupe defensively (mirrors addCollaborationChat).\n if (s.comments.some((c) => c.id === comment.id)) return s;\n return { comments: [...s.comments, comment], isDirty: true };\n }),\n replyToComment: (commentId, reply) =>\n set((s) => {\n let appended = false;\n const nextComments = s.comments.map((c) => {\n if (c.id !== commentId) return c;\n if (c.replies.some((r) => r.id === reply.id)) return c;\n appended = true;\n return { ...c, replies: [...c.replies, reply] };\n });\n if (!appended) return s;\n return { comments: nextComments, isDirty: true };\n }),\n toggleResolveComment: (commentId, resolved) =>\n set((s) => ({\n comments: s.comments.map((c) =>\n c.id === commentId\n ? { ...c, resolved: resolved !== undefined ? resolved : !c.resolved }\n : c,\n ),\n isDirty: true,\n })),\n deleteComment: (commentId) =>\n set((s) => ({\n comments: s.comments.filter((c) => c.id !== commentId),\n isDirty: true,\n })),\n setComments: (comments) => set({ comments, isDirty: true }),\n\n addWidget: (widget) =>\n set((s) => {\n // Ignore a duplicate id so updateWidget/removeWidget stay unambiguous\n // and a later entry isn't silently dropped when normalized on save.\n if (s.widgets.some((w) => w.id === widget.id)) return s;\n return { widgets: [...s.widgets, widget], isDirty: true };\n }),\n updateWidget: (id, patch) =>\n set((s) => {\n const exists = s.widgets.some((w) => w.id === id);\n if (!exists) return s;\n return {\n widgets: s.widgets.map((w) => (w.id === id ? { ...w, ...patch, id: w.id } : w)),\n isDirty: true,\n };\n }),\n replaceWidget: (id, widget) =>\n set((s) => {\n if (!s.widgets.some((w) => w.id === id)) return s;\n return {\n widgets: s.widgets.map((w) => (w.id === id ? { ...widget, id } : w)),\n isDirty: true,\n };\n }),\n removeWidget: (id) =>\n set((s) => ({\n widgets: s.widgets.filter((w) => w.id !== id),\n isDirty: true,\n })),\n moveWidget: (id, toIndex) =>\n set((s) => {\n const from = s.widgets.findIndex((w) => w.id === id);\n if (from < 0) return s;\n const target = Math.max(0, Math.min(s.widgets.length - 1, toIndex));\n if (target === from) return s;\n const widgets = [...s.widgets];\n const [moved] = widgets.splice(from, 1);\n widgets.splice(target, 0, moved);\n return { widgets, isDirty: true };\n }),\n setDashboardColumns: (columns) =>\n set((s) => {\n // Guard non-finite input so the clamp can't yield NaN columns.\n if (!Number.isFinite(columns)) return s;\n return {\n dashboardColumns: Math.max(\n MIN_DASHBOARD_COLUMNS,\n Math.min(MAX_DASHBOARD_COLUMNS, Math.trunc(columns)),\n ),\n isDirty: true,\n };\n }),\n\n setStorymap: (storymap) => set({ storymap, isDirty: true }),\n updateStorymapSettings: (patch) =>\n set((s) => ({\n storymap: { ...(s.storymap ?? DEFAULT_STORY_MAP), ...patch },\n isDirty: true,\n })),\n addStoryChapter: (chapter, atIndex) =>\n set((s) => {\n const base = s.storymap ?? DEFAULT_STORY_MAP;\n const chapters = [...base.chapters];\n const index =\n atIndex === undefined ? chapters.length : Math.min(Math.max(atIndex, 0), chapters.length);\n chapters.splice(index, 0, chapter);\n return { storymap: { ...base, chapters }, isDirty: true };\n }),\n updateStoryChapter: (id, patch) =>\n set((s) => {\n if (!s.storymap) return s;\n return {\n storymap: {\n ...s.storymap,\n chapters: s.storymap.chapters.map((chapter) =>\n chapter.id === id ? { ...chapter, ...patch } : chapter,\n ),\n },\n isDirty: true,\n };\n }),\n removeStoryChapter: (id) =>\n set((s) => {\n if (!s.storymap) return s;\n return {\n storymap: {\n ...s.storymap,\n chapters: s.storymap.chapters.filter((chapter) => chapter.id !== id),\n },\n isDirty: true,\n };\n }),\n moveStoryChapter: (id, targetIndex) =>\n set((s) => {\n if (!s.storymap) return s;\n const current = s.storymap.chapters.findIndex((chapter) => chapter.id === id);\n if (current < 0) return s;\n const chapters = [...s.storymap.chapters];\n const [chapter] = chapters.splice(current, 1);\n if (!chapter) return s;\n const next = Math.min(Math.max(targetIndex, 0), chapters.length);\n chapters.splice(next, 0, chapter);\n if (chapters.every((item, i) => item.id === s.storymap?.chapters[i]?.id)) {\n return s;\n }\n return { storymap: { ...s.storymap, chapters }, isDirty: true };\n }),\n});\n","/**\n * Runtime-settable window (ms) used to coalesce rapid history-producing changes\n * (e.g. a continuous opacity/style slider drag) into a single undo entry. Tests\n * set this to 0 for deterministic one-entry-per-action behavior.\n */\nlet historyCoalesceMs = 400;\n\nexport function setHistoryCoalesceMs(ms: number): void {\n historyCoalesceMs = ms;\n}\n\nexport function getHistoryCoalesceMs(): number {\n return historyCoalesceMs;\n}\n\n/**\n * Soft budget (in total feature count) for the geometry/attribute payload\n * retained across undo snapshots. Each in-app edit of a vector layer pushes a\n * new snapshot that holds the layer's full `geojson`; without a bound, editing\n * a large layer many times pins several copies of its feature set in memory\n * (issue #341). When the distinct features across snapshots exceed this budget,\n * the oldest snapshots are dropped (see {@link trimHistoryBySize}).\n *\n * Feature count is a cheap proxy for payload size; it avoids serializing\n * geometry on every edit. The default is generous enough that ordinary\n * multi-step editing of small/medium layers keeps the full history depth, while\n * very large layers trade history depth for a bounded footprint.\n */\nlet maxHistoryFeatureCount = 500_000;\n\nexport function setMaxHistoryFeatureCount(count: number): void {\n if (!Number.isFinite(count) || count < 0) {\n throw new RangeError(\n `maxHistoryFeatureCount must be a non-negative finite number, got ${count}`,\n );\n }\n maxHistoryFeatureCount = count;\n}\n\nexport function getMaxHistoryFeatureCount(): number {\n return maxHistoryFeatureCount;\n}\n\n/** Structural shape of a partialized undo snapshot, for size accounting. */\ninterface HistorySnapshot {\n layers?: {\n geojson?: { features?: unknown[] } | null;\n source?: { url?: unknown } | null;\n }[];\n}\n\n/**\n * Bytes of inline payload charged as one feature-equivalent, so a `data:` URL\n * competes for the same budget as geometry. A rough vector feature costs a few\n * hundred bytes of live JS objects, which is the order this matches.\n */\nconst BYTES_PER_FEATURE_EQUIVALENT = 200;\n\n/**\n * The size a snapshot adds to the budget, skipping any payload already in\n * `seen` so payloads shared across snapshots (unchanged layers keep the same\n * reference) are counted once. Mutates `seen` with each newly counted payload.\n *\n * Two payload kinds count. Vector `geojson` contributes its feature count. A\n * layer whose `source.url` is an inline `data:` URL contributes its length as\n * feature-equivalents: a NetCDF grid baked to pixels holds a whole PNG there,\n * and re-symbolizing it writes a fresh multi-megabyte string on every change, so\n * without this the pruner sees a \"free\" snapshot and history grows unbounded.\n */\nfunction distinctFeatureCount(snapshot: HistorySnapshot, seen: Set<unknown>): number {\n let count = 0;\n for (const layer of snapshot.layers ?? []) {\n const geojson = layer?.geojson;\n // Dedup the reference first so a payload shared across snapshots is visited\n // once even when its `features` is missing/malformed (it then contributes 0).\n if (geojson && !seen.has(geojson)) {\n seen.add(geojson);\n if (Array.isArray(geojson.features)) count += geojson.features.length;\n }\n const url = layer?.source?.url;\n // Strings have no stable identity to dedup by reference, so `seen` holds the\n // value itself; an unchanged layer repeats the same URL across snapshots and\n // must be charged once, exactly as a shared feature payload is.\n if (typeof url === \"string\" && url.startsWith(\"data:\") && !seen.has(url)) {\n seen.add(url);\n count += Math.ceil(url.length / BYTES_PER_FEATURE_EQUIVALENT);\n }\n }\n return count;\n}\n\n/**\n * Bound the memory held by undo history by dropping the oldest snapshots whose\n * combined feature payload exceeds `maxFeatures`.\n *\n * Walks newest-to-oldest and keeps the most recent snapshots whose cumulative\n * distinct feature count fits the budget. The single newest snapshot is always\n * kept regardless of size, so one-step undo of even a huge edit still works.\n * Feature sets shared by reference across snapshots (unchanged layers) are\n * counted once, so retaining many snapshots of small layers stays cheap.\n *\n * Returns the original array when nothing needs trimming, or a trimmed slice\n * (oldest entries removed) otherwise. Pure: never mutates its input.\n */\nexport function trimHistoryBySize<T extends HistorySnapshot>(\n pastStates: T[],\n maxFeatures: number,\n): T[] {\n if (!Number.isFinite(maxFeatures) || maxFeatures < 0) {\n throw new RangeError(`maxFeatures must be a non-negative finite number, got ${maxFeatures}`);\n }\n if (pastStates.length <= 1) return pastStates;\n const seen = new Set<unknown>();\n const lastIndex = pastStates.length - 1;\n // The newest snapshot is always retained, even if it alone exceeds the budget.\n let total = distinctFeatureCount(pastStates[lastIndex], seen);\n let keepFrom = lastIndex;\n for (let i = lastIndex - 1; i >= 0; i--) {\n const added = distinctFeatureCount(pastStates[i], seen);\n if (total + added > maxFeatures) break;\n total += added;\n keepFrom = i;\n }\n return keepFrom === 0 ? pastStates : pastStates.slice(keepFrom);\n}\n\n/** A debounced function with a `cancel` to clear its in-flight burst window. */\nexport type DebouncedFn<A extends unknown[]> = ((...args: A) => void) & {\n /** Clear the active burst timer so the next call fires on its leading edge. */\n cancel: () => void;\n};\n\n/**\n * Leading-edge debounce. Fires `fn` immediately on the first call of a burst,\n * then suppresses further calls until `getWait()` ms of quiet have elapsed.\n * When the wait is <= 0, every call is passed straight through (used in tests).\n *\n * Used as zundo's `handleSet`: `fn` is zundo's \"save previous state to history\"\n * function, so firing only on the leading edge records the pre-burst state once.\n * `cancel()` resets the window so that clearing history mid-burst doesn't\n * suppress the next edit's save.\n */\nexport function leadingDebounce<A extends unknown[]>(\n fn: (...args: A) => void,\n getWait: () => number,\n): DebouncedFn<A> {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const wrapped = (...args: A) => {\n const wait = getWait();\n if (wait <= 0) {\n fn(...args);\n return;\n }\n const atBurstStart = timer === null;\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n }, wait);\n if (atBurstStart) fn(...args);\n };\n wrapped.cancel = () => {\n if (timer) {\n clearTimeout(timer);\n timer = null;\n }\n };\n return wrapped;\n}\n","/**\n * Undo/redo for the app store. zundo's `temporal` middleware tracks only the\n * project-data fields that {@link partializeHistory} lists (layers, groups,\n * basemap, story map, comments, dashboard widgets, legend); every other field,\n * including the whole `ui`\n * dialog sub-state, selection and the camera, is outside the history, so\n * changing it never records an undo step and undo/redo never reverts it.\n *\n * The functions here act on the store bound by {@link bindHistoryStore}\n * rather than importing it, so the slices (whose `newProject`/`loadProject`\n * call {@link clearHistory}) can depend on this module without a cycle.\n */\nimport { shallow } from \"zustand/shallow\";\nimport type { StoreApi } from \"zustand\";\nimport type { TemporalState, ZundoOptions } from \"zundo\";\nimport { DEFAULT_ELLIPSOID_ID, getPlanetaryBasemapByStyleUrl } from \"../ellipsoids\";\nimport {\n getHistoryCoalesceMs,\n getMaxHistoryFeatureCount,\n leadingDebounce,\n trimHistoryBySize,\n} from \"../history\";\nimport { scrubPrintLayoutForLayers } from \"../print-layout-config\";\nimport type { GeoLibreProject, LayerGroup } from \"../types\";\nimport { scrubSecondaryPaneLayerVisibility } from \"./layer-removal\";\nimport type { AppState } from \"./types\";\n\n/** The fields undo/redo snapshots, restores and compares. */\nexport type HistoryState = Pick<\n AppState,\n | \"layers\"\n | \"layerGroups\"\n | \"basemapStyleUrl\"\n | \"basemapVisible\"\n | \"basemapOpacity\"\n | \"blankBackgroundColor\"\n | \"storymap\"\n | \"comments\"\n | \"widgets\"\n | \"legend\"\n>;\n\n/** The parts of the bound store the history functions use. */\ninterface HistoryStore {\n getState: () => AppState;\n setState: (partial: Partial<AppState>) => void;\n temporal: StoreApi<TemporalState<HistoryState>>;\n}\n\nlet historyStore: HistoryStore | null = null;\n\n/** Point the history functions at the app store; called once by `store.ts`. */\nexport function bindHistoryStore(store: HistoryStore): void {\n historyStore = store;\n}\n\nfunction boundStore(): HistoryStore {\n if (!historyStore) throw new Error(\"The app store has not been created yet.\");\n return historyStore;\n}\n\n/**\n * Compare two `layerGroups` arrays for undo-history purposes, ignoring the\n * `collapsed` flag so expand/collapse (a UI-panel preference) never records a\n * history entry. Every other field — order, name, visibility, opacity — is\n * still compared, so real edits are tracked.\n */\nfunction layerGroupsEqualForHistory(a: LayerGroup[], b: LayerGroup[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n const x = a[i];\n const y = b[i];\n if (x === y) continue;\n if (x.id !== y.id || x.name !== y.name || x.visible !== y.visible || x.opacity !== y.opacity) {\n return false;\n }\n }\n return true;\n}\n\n/** Cancels the active history coalesce window (assigned by zundo's handleSet). */\nlet cancelHistoryCoalesce: () => void = () => {};\n\ninterface ProjectRestoreHistoryEntry {\n before: GeoLibreProject;\n beforePath: string | null;\n after: GeoLibreProject;\n afterPath: string | null;\n}\n\nlet projectRestoreUndo: ProjectRestoreHistoryEntry | null = null;\nlet projectRestoreRedo: ProjectRestoreHistoryEntry | null = null;\nlet applyingProjectRestoreHistory = false;\nconst projectRestoreHistoryListeners = new Set<() => void>();\n\nfunction notifyProjectRestoreHistory(): void {\n projectRestoreHistoryListeners.forEach((listener) => listener());\n}\n\nexport function subscribeProjectRestoreHistory(listener: () => void): () => void {\n projectRestoreHistoryListeners.add(listener);\n return () => projectRestoreHistoryListeners.delete(listener);\n}\n\nexport function canUndoProjectRestore(): boolean {\n return projectRestoreUndo !== null;\n}\n\nexport function canRedoProjectRestore(): boolean {\n return projectRestoreRedo !== null;\n}\n\n/**\n * Register a whole-project restore as one undoable operation. The regular\n * temporal history intentionally tracks only editing fields, while restoring a\n * history snapshot also changes project metadata, camera, plugins, and other\n * serialized state, so it needs a full canonical project pair.\n */\nexport function registerProjectRestoreHistory(\n before: GeoLibreProject,\n beforePath: string | null,\n after: GeoLibreProject,\n afterPath: string | null = null,\n): void {\n projectRestoreUndo = { before, beforePath, after, afterPath };\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\n}\n\n/**\n * Drop the oldest undo snapshots once their combined feature payload exceeds the\n * configured budget, bounding the memory held by history when a large vector\n * layer is edited repeatedly (issue #341). Called after a snapshot is appended.\n * Operates on the live temporal store directly; this never touches the main\n * store, so it does not itself record a history entry.\n */\nfunction pruneHistoryBySize(): void {\n const temporalStore = boundStore().temporal;\n const { pastStates } = temporalStore.getState();\n const trimmed = trimHistoryBySize(pastStates, getMaxHistoryFeatureCount());\n if (trimmed.length !== pastStates.length) {\n temporalStore.setState({ pastStates: trimmed });\n }\n}\n\n/**\n * Only these fields participate in undo/redo; everything else (selection,\n * ui flags, mapView/camera, pointerCoords, project metadata, isDirty, ...)\n * is excluded, so changing them never creates a history entry.\n */\nexport function partializeHistory(s: AppState): HistoryState {\n return {\n layers: s.layers,\n layerGroups: s.layerGroups,\n basemapStyleUrl: s.basemapStyleUrl,\n basemapVisible: s.basemapVisible,\n basemapOpacity: s.basemapOpacity,\n blankBackgroundColor: s.blankBackgroundColor,\n storymap: s.storymap,\n comments: s.comments,\n // Widgets and legend entries reference layers, and deleting a layer\n // scrubs them; tracking them makes undoing a delete (or a snapshot layer\n // restore) bring them back with the layer instead of leaving them lost or\n // dangling. Secondary panes and the Print Layout are deliberately not\n // tracked (see finishHistoryStep).\n widgets: s.widgets,\n legend: s.legend,\n };\n}\n\n/** zundo options for the app store's undo history. */\nexport function createHistoryOptions(): ZundoOptions<AppState, HistoryState> {\n return {\n partialize: partializeHistory,\n // Records a history entry only when the tracked slice really changed.\n // Basemap fields compare with ===; `layers` is compared element-by-element\n // (Object.is per element) via shallow. Every mutating action creates new\n // layer/group objects, so real changes differ; two distinct empty arrays\n // compare equal, so resetting them (e.g. newProject) records nothing.\n // `storymap` is compared by reference: every authoring action creates a\n // new object, so real edits differ while an unchanged null stays equal.\n // `layerGroups` is compared ignoring `collapsed`, which is a UI preference\n // excluded from undo (see toggleLayerGroupCollapsed).\n // `comments` and `widgets` are compared shallowly by reference, and\n // `legend` by reference: setLegend always stores a new object.\n equality: (a, b) =>\n a.basemapStyleUrl === b.basemapStyleUrl &&\n a.basemapVisible === b.basemapVisible &&\n a.basemapOpacity === b.basemapOpacity &&\n a.blankBackgroundColor === b.blankBackgroundColor &&\n a.storymap === b.storymap &&\n shallow(a.layers, b.layers) &&\n shallow(a.comments, b.comments) &&\n shallow(a.widgets, b.widgets) &&\n a.legend === b.legend &&\n layerGroupsEqualForHistory(a.layerGroups, b.layerGroups),\n limit: 100,\n // Group rapid bursts (slider drags) into one entry; window is 0 in tests.\n // Keep the debounced wrapper so clearHistory can reset an in-flight burst.\n handleSet: (baseHandleSet) => {\n const debounced = leadingDebounce(baseHandleSet, getHistoryCoalesceMs);\n cancelHistoryCoalesce = debounced.cancel;\n // Trim history back under the feature-payload budget so editing large\n // layers can't pin unbounded copies of their feature sets in memory\n // (issue #341). Only the leading-edge call actually pushes a snapshot;\n // burst-suppressed calls leave `pastStates` untouched, so skip the scan\n // unless a new snapshot was appended.\n return (...args: Parameters<typeof debounced>) => {\n const before = boundStore().temporal.getState().pastStates;\n debounced(...args);\n if (boundStore().temporal.getState().pastStates !== before) {\n if (!applyingProjectRestoreHistory && projectRestoreRedo) {\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\n }\n pruneHistoryBySize();\n }\n };\n },\n };\n}\n\n/**\n * After an undo/redo restores the tracked slice, mark the project dirty and\n * drop references the step left pointing at a layer that no longer exists:\n * the selection, per-pane layer visibility, and Print Layout data/atlas\n * blocks. None of those is tracked in history (selection is UI state; panes\n * carry their own camera and must stay in step with the untracked grid\n * layout; the Print Layout composer keeps a local copy it writes back), so\n * they are scrubbed the way deleting the layer would have scrubbed them.\n */\nfunction finishHistoryStep(previousBasemapStyleUrl: string): void {\n const s = boundStore().getState();\n const layerIds = new Set(s.layers.map((layer) => layer.id));\n const danglingPaneIds = new Set<string>();\n for (const pane of s.secondaryMapViews) {\n for (const id of Object.keys(pane.layerVisibility)) {\n if (!layerIds.has(id)) danglingPaneIds.add(id);\n }\n }\n const secondaryMapViews = scrubSecondaryPaneLayerVisibility(s.secondaryMapViews, danglingPaneIds);\n const printLayout = scrubPrintLayoutForLayers(s.printLayout, layerIds);\n const referencePatch = {\n ...(secondaryMapViews !== s.secondaryMapViews ? { secondaryMapViews } : {}),\n ...(printLayout !== s.printLayout ? { printLayout } : {}),\n };\n const selectionDangling =\n s.selectedLayerId !== null && !s.layers.some((layer) => layer.id === s.selectedLayerId);\n // The basemap is in the undo history but the ellipsoid preference is not, so a\n // step that restores a *different* basemap can leave the two out of sync (e.g.\n // undoing a switch to Mars would keep the Mars radius under an Earth basemap).\n // Re-derive the ellipsoid from the restored basemap's body — Earth for a\n // non-planetary basemap — but only when this step actually changed the\n // basemap. Steps that leave the basemap untouched must not touch the ellipsoid,\n // which the user can set independently of the basemap in Settings.\n const restoredEllipsoidId =\n getPlanetaryBasemapByStyleUrl(s.basemapStyleUrl)?.ellipsoidId ?? DEFAULT_ELLIPSOID_ID;\n const ellipsoidPatch =\n s.basemapStyleUrl !== previousBasemapStyleUrl &&\n s.preferences.map.ellipsoidId !== restoredEllipsoidId\n ? {\n preferences: {\n ...s.preferences,\n map: { ...s.preferences.map, ellipsoidId: restoredEllipsoidId },\n },\n }\n : {};\n boundStore().setState(\n selectionDangling\n ? {\n isDirty: true,\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n ...ellipsoidPatch,\n ...referencePatch,\n }\n : { isDirty: true, ...ellipsoidPatch, ...referencePatch },\n );\n // The setState above must not leave a coalesce window open for the next edit.\n cancelHistoryCoalesce();\n}\n\n/**\n * Step the layer/basemap history back one entry and mark the project dirty.\n * zundo restores the partialized slice via the store's set; the resulting new\n * `layers`/basemap refs drive MapCanvas's existing effects, so the map\n * reconciles through MapController.syncLayers (never mutated directly here).\n */\nexport function undo(): void {\n const temporal = boundStore().temporal.getState();\n if (temporal.pastStates.length === 0 && projectRestoreUndo) {\n const entry = projectRestoreUndo;\n applyingProjectRestoreHistory = true;\n try {\n boundStore().getState().loadProject(entry.before, entry.beforePath, {\n rememberRecent: false,\n presenting: false,\n });\n projectRestoreUndo = null;\n boundStore().setState({ isDirty: true });\n projectRestoreRedo = entry;\n notifyProjectRestoreHistory();\n } catch (error) {\n console.error(\"Could not undo the project snapshot restore.\", error);\n } finally {\n applyingProjectRestoreHistory = false;\n }\n return;\n }\n if (temporal.pastStates.length === 0) return; // nothing to undo; stay clean\n cancelHistoryCoalesce(); // break any in-flight burst so the next edit records\n const previousBasemapStyleUrl = boundStore().getState().basemapStyleUrl;\n temporal.undo();\n finishHistoryStep(previousBasemapStyleUrl);\n}\n\n/** Step the history forward one entry and mark the project dirty. */\nexport function redo(): void {\n const temporal = boundStore().temporal.getState();\n if (temporal.futureStates.length === 0 && projectRestoreRedo) {\n const entry = projectRestoreRedo;\n applyingProjectRestoreHistory = true;\n try {\n boundStore().getState().loadProject(entry.after, entry.afterPath, {\n rememberRecent: false,\n presenting: false,\n });\n projectRestoreRedo = null;\n boundStore().setState({ isDirty: true });\n projectRestoreUndo = entry;\n notifyProjectRestoreHistory();\n } catch (error) {\n console.error(\"Could not redo the project snapshot restore.\", error);\n } finally {\n applyingProjectRestoreHistory = false;\n }\n return;\n }\n if (temporal.futureStates.length === 0) return; // nothing to redo; stay clean\n cancelHistoryCoalesce(); // break any in-flight burst so the next edit records\n const previousBasemapStyleUrl = boundStore().getState().basemapStyleUrl;\n temporal.redo();\n finishHistoryStep(previousBasemapStyleUrl);\n}\n\n/**\n * Apply a store change that is not the user's (data loading in behind a\n * project) without recording an undo step for it. The history stays as it\n * was, so undo neither takes the change back out nor loses earlier steps.\n *\n * @param change - The store updates to apply.\n */\nexport function withoutHistory(change: () => void): void {\n cancelHistoryCoalesce(); // a burst left open would record this change\n const temporal = boundStore().temporal.getState();\n temporal.pause();\n try {\n change();\n } finally {\n temporal.resume();\n }\n}\n\n/** Empty both the undo and redo stacks (e.g. on new/loaded project). */\nexport function clearHistory(): void {\n cancelHistoryCoalesce(); // reset any in-flight burst so the next edit records\n boundStore().temporal.getState().clear();\n if (!applyingProjectRestoreHistory) {\n projectRestoreUndo = null;\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\n }\n}\n","/**\n * Project identity and lifecycle: name, path, dirty flag, recent projects, and\n * the project-level settings that have no richer home (plugins, legend, print\n * layout, metadata). `newProject` and `loadProject` replace the whole document\n * (every slice's project fields) and clear undo history.\n */\nimport {\n applyProjectToStore,\n type CreateProjectOptions,\n createEmptyProject,\n DEFAULT_PROJECT_NAME,\n} from \"../project\";\nimport {\n createDefaultPrintLayout,\n printLayoutConfigsEqual,\n type PrintLayoutConfig,\n} from \"../print-layout-config\";\nimport { reapplyLayerJoins } from \"../joins\";\nimport {\n DEFAULT_LEGEND_CONFIG,\n type GeoLibreLayer,\n type GeoLibreProject,\n type LayerGroup,\n type LegendConfig,\n type ProjectPluginState,\n type RecentProjectEntry,\n} from \"../types\";\nimport { resolveIdentifyTarget } from \"./session-slice\";\nimport type { SliceCreator } from \"./types\";\nimport { clearHistory } from \"./undo-history\";\n\nconst MAX_RECENT_PROJECTS = 10;\n\n/** Derive a human-friendly display name from a file path or URL. */\nexport function projectPathLabel(path: string): string {\n return path.split(/[/\\\\]/).pop() || path;\n}\n\nfunction normalizeRecentProjects(projects: RecentProjectEntry[]): RecentProjectEntry[] {\n const seen = new Set<string>();\n const normalized: RecentProjectEntry[] = [];\n\n for (const project of projects) {\n const path = project.path.trim();\n if (!path || seen.has(path)) continue;\n\n const name = project.name.trim() || projectPathLabel(path);\n normalized.push({\n path,\n name,\n openedAt: project.openedAt || new Date().toISOString(),\n });\n seen.add(path);\n }\n\n return normalized.slice(0, MAX_RECENT_PROJECTS);\n}\n\nexport interface ProjectSlice {\n projectName: string;\n projectPath: string | null;\n projectGeneration: number;\n isDirty: boolean;\n projectPlugins: ProjectPluginState | null;\n legend: LegendConfig;\n /** Print Layout composer settings for the open project (discussion #1992). */\n printLayout: PrintLayoutConfig;\n metadata: Record<string, unknown>;\n recentProjects: RecentProjectEntry[];\n\n setLegend: (legend: LegendConfig) => void;\n /**\n * Replace the Print Layout composer settings. A config equal to the current\n * one is ignored, so re-opening the composer (or a project load seeding the\n * dialog) never marks the project dirty.\n */\n setPrintLayout: (printLayout: PrintLayoutConfig) => void;\n setProjectPlugins: (projectPlugins: ProjectPluginState | null, shouldMarkDirty?: boolean) => void;\n /**\n * Start a new, untitled project. `layers` and `layerGroups` seed it with\n * layers (the Startup setting's default layers).\n */\n newProject: (\n options?: CreateProjectOptions & {\n name?: string;\n layers?: GeoLibreLayer[];\n layerGroups?: LayerGroup[];\n },\n ) => void;\n loadProject: (\n project: GeoLibreProject,\n path?: string | null,\n options?: { rememberRecent?: boolean; presenting?: boolean },\n ) => void;\n setProjectPath: (path: string | null) => void;\n setProjectName: (name: string) => void;\n setRecentProjects: (projects: RecentProjectEntry[]) => void;\n rememberRecentProject: (entry: RecentProjectEntry) => void;\n forgetRecentProject: (path: string) => void;\n clearRecentProjects: () => void;\n markSaved: () => void;\n}\n\nexport const createProjectSlice: SliceCreator<ProjectSlice> = (set, get) => ({\n projectName: DEFAULT_PROJECT_NAME,\n projectPath: null,\n projectGeneration: 0,\n isDirty: false,\n projectPlugins: null,\n legend: { ...DEFAULT_LEGEND_CONFIG },\n printLayout: createDefaultPrintLayout(),\n metadata: {},\n recentProjects: [],\n\n setLegend: (legend) => set({ legend, isDirty: true }),\n\n setPrintLayout: (printLayout) =>\n set((s) =>\n printLayoutConfigsEqual(s.printLayout, printLayout) ? s : { printLayout, isDirty: true },\n ),\n // When shouldMarkDirty is false the existing dirty flag is preserved rather\n // than set; it cannot clear the flag (only markSaved() does that).\n setProjectPlugins: (projectPlugins, shouldMarkDirty = true) =>\n set((s) => ({\n projectPlugins,\n isDirty: shouldMarkDirty || s.isDirty,\n })),\n\n setProjectPath: (path) => set({ projectPath: path }),\n setProjectName: (name) => set({ projectName: name, isDirty: true }),\n setRecentProjects: (projects) => set({ recentProjects: normalizeRecentProjects(projects) }),\n rememberRecentProject: (entry) =>\n set((s) => ({\n recentProjects: normalizeRecentProjects([entry, ...s.recentProjects]),\n })),\n forgetRecentProject: (path) => {\n // Compare with separators normalized so a backslash/forward-slash mismatch\n // on Windows does not leave a stale entry behind.\n const normalized = path.replace(/\\\\/g, \"/\");\n set((s) => ({\n recentProjects: s.recentProjects.filter(\n (project) => project.path.replace(/\\\\/g, \"/\") !== normalized,\n ),\n }));\n },\n clearRecentProjects: () => set({ recentProjects: [] }),\n markSaved: () => set({ isDirty: false }),\n\n newProject: (options = {}) => {\n const project = {\n ...createEmptyProject(options.name, options),\n ...(options.layers ? { layers: options.layers } : {}),\n ...(options.layerGroups ? { layerGroups: options.layerGroups } : {}),\n };\n const applied = applyProjectToStore(project);\n set((s) => ({\n ...applied,\n hoverTooltipsEnabled: true,\n projectPath: null,\n projectGeneration: s.projectGeneration + 1,\n isDirty: false,\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n identifyLayerId: null,\n identifyLayerIds: null,\n featureSelectionActive: false,\n // The copied style names a layer from the previous project, so a\n // paste in the new one would apply an orphaned entry.\n copiedLayerStyle: null,\n pointerCoords: null,\n pointerElevation: null,\n cameraAltitude: null,\n attributeFilter: \"\",\n // Don't carry an active story presentation into a different project.\n ui: {\n ...s.ui,\n storymapPresenting: false,\n storymapReturnToEditor: false,\n storymapLayerOpacity: {},\n storymapPanelOpen: false,\n storymapComposingId: null,\n // An open selection dialog (and its preselected layer id) belongs\n // to the previous project's layers.\n selectByExpressionOpen: false,\n selectByExpressionLayerId: null,\n selectByLocationOpen: false,\n selectByLocationLayerId: null,\n loadEditorFeaturesOpen: false,\n loadEditorFeaturesLayerId: null,\n // A pending assistant-requested Model Builder load names a model in\n // the previous project's `savedModels`.\n modelBuilderRequestedModelId: null,\n },\n }));\n clearHistory();\n },\n\n loadProject: (project, path = null, options = {}) => {\n const applied = applyProjectToStore(project);\n // Re-resolve persistent attribute joins against the loaded layer set,\n // so joined columns reflect the join tables as saved (and a stale\n // saved copy of the joined output self-heals).\n applied.layers = reapplyLayerJoins(applied.layers);\n // A project that ships a story map opens straight into the presentation\n // so the reader sees the story, not the editor. Projects without a story\n // (or with an empty one) open normally. Callers that open a project for\n // authoring rather than viewing can pass `presenting: false` to override.\n const presentStory = options.presenting ?? (applied.storymap?.chapters.length ?? 0) > 0;\n const selectedLayerId =\n project.selectedLayerId === null\n ? null\n : typeof project.selectedLayerId === \"string\" &&\n applied.layers.some((layer) => layer.id === project.selectedLayerId)\n ? project.selectedLayerId\n : (applied.layers[0]?.id ?? null);\n set((s) => ({\n ...applied,\n hoverTooltipsEnabled: true,\n projectPath: path,\n projectGeneration: s.projectGeneration + 1,\n isDirty: false,\n selectedLayerId,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n featureSelectionActive: false,\n // A project that saved an Identify target (issue #2688) opens with it\n // armed; any other load disarms Identify, since the previous target\n // named a layer of the project being replaced.\n ...resolveIdentifyTarget(\n applied.projectInteraction?.identify,\n applied.layers.map((layer) => layer.id),\n ),\n // The copied style names a layer from the previous project, so a\n // paste in the loaded one would apply an orphaned entry.\n copiedLayerStyle: null,\n // Ephemeral readouts describe the previous project's map. The\n // elevation and altitude especially: a project that switches to a\n // planetary body would otherwise keep showing Earth-scaled values\n // until the next hover or camera move.\n pointerCoords: null,\n pointerElevation: null,\n cameraAltitude: null,\n // Present a bundled story on load; otherwise drop any presentation\n // carried over from the previous project.\n ui: {\n ...s.ui,\n storymapPresenting: presentStory,\n // A bundled story auto-presents for viewing, so exiting it should\n // not pop open the editor (#918).\n storymapReturnToEditor: false,\n storymapLayerOpacity: {},\n storymapPanelOpen: false,\n storymapComposingId: null,\n // An open selection dialog (and its preselected layer id) belongs\n // to the previous project's layers.\n selectByExpressionOpen: false,\n selectByExpressionLayerId: null,\n selectByLocationOpen: false,\n selectByLocationLayerId: null,\n loadEditorFeaturesOpen: false,\n loadEditorFeaturesLayerId: null,\n // A pending assistant-requested Model Builder load names a model in\n // the previous project's `savedModels`.\n modelBuilderRequestedModelId: null,\n },\n }));\n clearHistory();\n if (path && options.rememberRecent !== false) {\n get().rememberRecentProject({\n path,\n name: project.name,\n openedAt: new Date().toISOString(),\n });\n }\n },\n});\n","/**\n * UI-only state: which dialogs and panels are open, and the one-shot requests\n * they consume. Lives in the `ui` sub-state, which the undo history never\n * tracks (see `undo-history.ts`), so opening or closing a dialog never records\n * an undo step and undo/redo never toggles one.\n */\nimport type { ProcessingRerunRequest } from \"../types\";\nimport type { SliceCreator } from \"./types\";\n\nexport type ConversionToolKind =\n | \"vector-to-vector\"\n | \"vector-to-geoparquet\"\n | \"vector-to-flatgeobuf\"\n | \"vector-to-shapefile\"\n | \"vector-to-geopackage\"\n | \"csv-to-geoparquet\"\n | \"vector-to-pmtiles\"\n | \"raster-to-pmtiles\"\n | \"raster-to-cog\";\n\n/**\n * Identifiers of the vector processing tools. Kept in sync by hand with the\n * `id` fields of `VECTOR_TOOLS` in `@geolibre/processing` (`vector-tools.ts`);\n * deriving the type there would create a core -> processing circular import.\n */\nexport type VectorToolKind =\n | \"buffer\"\n | \"centroids\"\n | \"convex-hull\"\n | \"dissolve\"\n | \"bounding-box\"\n | \"simplify\"\n | \"clip\"\n | \"intersection\"\n | \"difference\"\n | \"union\"\n | \"spatial-join\"\n | \"attribute-join\"\n | \"select-by-value\"\n | \"select-by-location\"\n | \"random-extract\"\n | \"reproject\"\n | \"explode\"\n | \"aggregate\"\n | \"smooth\"\n | \"extract-vertices\"\n | \"points-along-geometry\"\n | \"grid\"\n | \"voronoi\"\n | \"cell-sectors\"\n | \"dggs-grid\"\n | \"dggs-bin\"\n | \"dggs-compact\"\n | \"trajectory-speed\"\n | \"detect-stops\"\n | \"space-time-proximity\"\n | \"decode-polyline\"\n | \"encode-polyline\"\n | \"merge-layers\"\n | \"check-validity\"\n | \"fix-geometries\"\n | \"check-topology-rules\"\n | \"fix-topology\";\n\n/** Identifiers of the network-analysis tools (`NETWORK_TOOLS` ids). */\nexport type NetworkToolKind = \"isochrone\" | \"od-matrix\" | \"sequential-route\";\n\n/** Identifiers of the spatial-statistics tools (`STATISTICS_TOOLS` ids). */\nexport type StatisticsToolKind =\n | \"global-morans-i\"\n | \"local-morans-i\"\n | \"getis-ord-gi\"\n | \"average-nearest-neighbor\"\n | \"kernel-density\"\n | \"emerging-hot-spot\"\n | \"composite-score\";\n\n/**\n * Identifiers of the raster processing tools. Kept in sync by hand with the\n * `id` fields of `RASTER_TOOLS` in `@geolibre/processing` (`raster-tools.ts`);\n * deriving the type there would create a core -> processing circular import.\n */\nexport type RasterToolKind =\n | \"hillshade\"\n | \"slope\"\n | \"aspect\"\n | \"reproject\"\n | \"resample\"\n | \"clip-extent\"\n | \"clip-mask\"\n | \"polygonize\"\n | \"contour\"\n | \"interpolate\"\n | \"zonal\"\n | \"raster-calc\"\n | \"spectral-index\"\n | \"reclassify\"\n | \"mosaic\"\n | \"focal\";\n\nexport interface UiSlice {\n ui: {\n processingOpen: boolean;\n /**\n * Tool id to preselect when the Whitebox toolbox dialog opens, set when the\n * user picks a specific tool from the Processing menu's category submenus.\n * Consumed and cleared by ProcessingDialog. Null means \"no preselection\".\n */\n processingInitialTool: string | null;\n conversionOpen: ConversionToolKind | null;\n vectorToolOpen: VectorToolKind | null;\n networkToolOpen: NetworkToolKind | null;\n statisticsToolOpen: StatisticsToolKind | null;\n rasterToolOpen: RasterToolKind | null;\n segmentationOpen: boolean;\n objectDetectionOpen: boolean;\n segmentEverythingOpen: boolean;\n geocodeOpen: boolean;\n sqlWorkspaceOpen: boolean;\n loadEditorFeaturesOpen: boolean;\n // Store layer preselected in the \"Load Features into Editor\" dialog when it\n // is opened from a layer's context menu, or null when opened without a target.\n loadEditorFeaturesLayerId: string | null;\n pythonConsoleOpen: boolean;\n notebookOpen: boolean;\n assistantOpen: boolean;\n attributeTableOpen: boolean;\n /**\n * Pending \"open the attribute table in edit mode\" request for this layer,\n * written by Identify's Edit attributes action (#2932). The table consumes\n * and clears it. Null when idle.\n */\n attributeTableEditLayerId: string | null;\n /** Whether the Raster Attribute Table bottom panel is open (issue #1307). */\n rasterAttributeTableOpen: boolean;\n dashboardOpen: boolean;\n storymapPanelOpen: boolean;\n storymapPresenting: boolean;\n // True when the active presentation was launched from the editor, so exiting\n // it reopens the Story Map editor instead of dropping to the bare map\n // (#918). Auto-presented projects (opened for viewing) leave this false.\n storymapReturnToEditor: boolean;\n /**\n * Layer opacities the active story presentation has applied so far, keyed\n * by store layer id. Playback fades layers by writing MapLibre paint\n * properties directly (never the persisted `layers[].opacity`), so this is\n * how renderers outside MapLibre's paint model (deck.gl diagrams, 3D\n * Z-value geometry) and the on-map Legend follow a chapter's fades. Empty\n * while not presenting; never saved with the project.\n */\n storymapLayerOpacity: Record<string, number>;\n // Id of the chapter currently being composed on the live map. When set, the\n // Story Map dialog is hidden so the user can pan/zoom/tilt the real map and\n // save the resulting camera back into this chapter (issue #775).\n storymapComposingId: string | null;\n /** The Batch tools dialog (run one tool across many layers). */\n batchToolsOpen: boolean;\n /** The Model Builder canvas panel (author a processing graph). */\n modelBuilderOpen: boolean;\n /** One-shot request for Model Builder to load a saved model. */\n modelBuilderRequestedModelId: string | null;\n /** Style Manager dialog visibility (issue #1294). */\n styleManagerOpen: boolean;\n /** Processing History panel visibility (#1292). */\n processingHistoryOpen: boolean;\n /** Select by Expression dialog visibility (#1314). */\n selectByExpressionOpen: boolean;\n // Layer preselected in the Select by Expression dialog when it is opened\n // from a layer's context menu, or null when opened without a target.\n // Deliberately not selectLayer(): that would clear the live selection the\n // dialog's add/remove/intersect modes need to combine with.\n selectByExpressionLayerId: string | null;\n /** Select by Location dialog visibility (#1314). */\n selectByLocationOpen: boolean;\n /** Same contract as `selectByExpressionLayerId`, for Select by Location. */\n selectByLocationLayerId: string | null;\n /**\n * Pending \"re-run from History\" request. Written by the History panel just\n * before it opens the target processing dialog; consumed and cleared by\n * that dialog once it has pre-filled its parameter form. Null when idle.\n */\n processingRerun: ProcessingRerunRequest | null;\n zoomToSelectedFeature: boolean;\n // Live-collaboration dialog visibility. Lifted into the store (rather than\n // local toolbar state) so the on-canvas session-status badge can reopen the\n // Collaborate dialog from outside the toolbar's component tree (#754).\n collaborateDialogOpen: boolean;\n };\n\n setProcessingOpen: (open: boolean) => void;\n setProcessingInitialTool: (toolId: string | null) => void;\n setConversionOpen: (kind: ConversionToolKind | null) => void;\n setVectorToolOpen: (kind: VectorToolKind | null) => void;\n setNetworkToolOpen: (kind: NetworkToolKind | null) => void;\n setStatisticsToolOpen: (kind: StatisticsToolKind | null) => void;\n setRasterToolOpen: (kind: RasterToolKind | null) => void;\n setSegmentationOpen: (open: boolean) => void;\n setObjectDetectionOpen: (open: boolean) => void;\n setSegmentEverythingOpen: (open: boolean) => void;\n setGeocodeOpen: (open: boolean) => void;\n setSqlWorkspaceOpen: (open: boolean) => void;\n setLoadEditorFeaturesOpen: (open: boolean, layerId?: string | null) => void;\n setPythonConsoleOpen: (open: boolean) => void;\n setNotebookOpen: (open: boolean) => void;\n setAssistantOpen: (open: boolean) => void;\n setAttributeTableOpen: (open: boolean) => void;\n /**\n * Ask the attribute table to open in edit mode on `layerId` (opening the\n * table too), or clear a pending request with `null`.\n */\n requestAttributeTableEdit: (layerId: string | null) => void;\n setRasterAttributeTableOpen: (open: boolean) => void;\n setDashboardOpen: (open: boolean) => void;\n setStorymapPanelOpen: (open: boolean) => void;\n setStorymapPresenting: (presenting: boolean, returnToEditor?: boolean) => void;\n /**\n * Record the layer opacities a story chapter applied, merging into the\n * presentation's running map (see `ui.storymapLayerOpacity`).\n */\n setStorymapLayerOpacity: (changes: Record<string, number>) => void;\n setStorymapComposing: (chapterId: string | null) => void;\n setBatchToolsOpen: (open: boolean) => void;\n setModelBuilderOpen: (open: boolean) => void;\n setModelBuilderRequestedModelId: (id: string | null) => void;\n setProcessingHistoryOpen: (open: boolean) => void;\n /** Open/close Select by Expression, optionally preselecting a target layer. */\n setSelectByExpressionOpen: (open: boolean, layerId?: string | null) => void;\n /** Open/close Select by Location, optionally preselecting a target layer. */\n setSelectByLocationOpen: (open: boolean, layerId?: string | null) => void;\n setProcessingRerun: (request: ProcessingRerunRequest | null) => void;\n setCollaborateDialogOpen: (open: boolean) => void;\n setZoomToSelectedFeature: (enabled: boolean) => void;\n setStyleManagerOpen: (open: boolean) => void;\n}\n\nexport const createUiSlice: SliceCreator<UiSlice> = (set) => ({\n ui: {\n processingOpen: false,\n processingInitialTool: null,\n conversionOpen: null,\n vectorToolOpen: null,\n networkToolOpen: null,\n statisticsToolOpen: null,\n rasterToolOpen: null,\n segmentationOpen: false,\n objectDetectionOpen: false,\n segmentEverythingOpen: false,\n geocodeOpen: false,\n sqlWorkspaceOpen: false,\n loadEditorFeaturesOpen: false,\n loadEditorFeaturesLayerId: null,\n pythonConsoleOpen: false,\n notebookOpen: false,\n assistantOpen: false,\n attributeTableOpen: false,\n attributeTableEditLayerId: null,\n rasterAttributeTableOpen: false,\n dashboardOpen: false,\n storymapPanelOpen: false,\n storymapPresenting: false,\n storymapReturnToEditor: false,\n storymapLayerOpacity: {},\n storymapComposingId: null,\n batchToolsOpen: false,\n modelBuilderOpen: false,\n modelBuilderRequestedModelId: null,\n styleManagerOpen: false,\n processingHistoryOpen: false,\n selectByExpressionOpen: false,\n selectByExpressionLayerId: null,\n selectByLocationOpen: false,\n selectByLocationLayerId: null,\n processingRerun: null,\n zoomToSelectedFeature: false,\n collaborateDialogOpen: false,\n },\n\n setProcessingOpen: (open) => set((s) => ({ ui: { ...s.ui, processingOpen: open } })),\n setProcessingInitialTool: (toolId) =>\n set((s) => ({ ui: { ...s.ui, processingInitialTool: toolId } })),\n setConversionOpen: (kind) => set((s) => ({ ui: { ...s.ui, conversionOpen: kind } })),\n setVectorToolOpen: (kind) =>\n set((s) => ({\n ui: {\n ...s.ui,\n // Pre-DGGS projects / callers may still pass h3-grid / h3-bin-points.\n vectorToolOpen:\n (kind as string | null) === \"h3-grid\"\n ? \"dggs-grid\"\n : (kind as string | null) === \"h3-bin-points\"\n ? \"dggs-bin\"\n : kind,\n },\n })),\n setNetworkToolOpen: (kind) => set((s) => ({ ui: { ...s.ui, networkToolOpen: kind } })),\n setStatisticsToolOpen: (kind) => set((s) => ({ ui: { ...s.ui, statisticsToolOpen: kind } })),\n setRasterToolOpen: (kind) => set((s) => ({ ui: { ...s.ui, rasterToolOpen: kind } })),\n setSegmentationOpen: (open) => set((s) => ({ ui: { ...s.ui, segmentationOpen: open } })),\n setObjectDetectionOpen: (open) => set((s) => ({ ui: { ...s.ui, objectDetectionOpen: open } })),\n setSegmentEverythingOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, segmentEverythingOpen: open } })),\n setGeocodeOpen: (open) => set((s) => ({ ui: { ...s.ui, geocodeOpen: open } })),\n setSqlWorkspaceOpen: (open) => set((s) => ({ ui: { ...s.ui, sqlWorkspaceOpen: open } })),\n setLoadEditorFeaturesOpen: (open, layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n loadEditorFeaturesOpen: open,\n loadEditorFeaturesLayerId: open ? (layerId ?? null) : null,\n },\n })),\n setPythonConsoleOpen: (open) => set((s) => ({ ui: { ...s.ui, pythonConsoleOpen: open } })),\n setNotebookOpen: (open) => set((s) => ({ ui: { ...s.ui, notebookOpen: open } })),\n setAssistantOpen: (open) => set((s) => ({ ui: { ...s.ui, assistantOpen: open } })),\n setAttributeTableOpen: (open) => set((s) => ({ ui: { ...s.ui, attributeTableOpen: open } })),\n requestAttributeTableEdit: (layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n attributeTableEditLayerId: layerId,\n attributeTableOpen: layerId === null ? s.ui.attributeTableOpen : true,\n },\n })),\n setRasterAttributeTableOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, rasterAttributeTableOpen: open } })),\n setDashboardOpen: (open) => set((s) => ({ ui: { ...s.ui, dashboardOpen: open } })),\n setStorymapPanelOpen: (open) =>\n set((s) => ({\n ui: {\n ...s.ui,\n storymapPanelOpen: open,\n // Opening the editor must leave compose mode, or the menu item could\n // re-open the dialog while the compose bar is still active over a\n // now-hidden map (#775). Closing (entering compose) leaves it as-is.\n ...(open ? { storymapComposingId: null } : {}),\n },\n })),\n setStorymapPresenting: (presenting, returnToEditor = false) =>\n set((s) => ({\n ui: {\n ...s.ui,\n storymapPresenting: presenting,\n // Track whether exiting should reopen the editor; only meaningful\n // while presenting, so it clears once the presentation ends (#918).\n storymapReturnToEditor: presenting ? returnToEditor : false,\n // A presentation starts from (and leaves behind) a clean slate; the\n // fades it applies are replayed from chapter 0 on the next run.\n storymapLayerOpacity: {},\n },\n })),\n setStorymapLayerOpacity: (changes) =>\n set((s) => {\n const next = { ...s.ui.storymapLayerOpacity };\n let changed = false;\n for (const [layerId, opacity] of Object.entries(changes)) {\n const clamped = Math.min(1, Math.max(0, opacity));\n if (next[layerId] === clamped) continue;\n next[layerId] = clamped;\n changed = true;\n }\n // Return the current state untouched when nothing moved: Zustand only\n // skips the listener broadcast for the same state reference, and a\n // chapter re-entering the same opacities would otherwise rebuild\n // every store subscriber's view.\n return changed ? { ui: { ...s.ui, storymapLayerOpacity: next } } : s;\n }),\n setStorymapComposing: (chapterId) =>\n set((s) => ({ ui: { ...s.ui, storymapComposingId: chapterId } })),\n setBatchToolsOpen: (open) => set((s) => ({ ui: { ...s.ui, batchToolsOpen: open } })),\n setModelBuilderOpen: (open) => set((s) => ({ ui: { ...s.ui, modelBuilderOpen: open } })),\n setModelBuilderRequestedModelId: (id) =>\n set((s) => ({ ui: { ...s.ui, modelBuilderRequestedModelId: id } })),\n setProcessingHistoryOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, processingHistoryOpen: open } })),\n setProcessingRerun: (request) => set((s) => ({ ui: { ...s.ui, processingRerun: request } })),\n setSelectByExpressionOpen: (open, layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n selectByExpressionOpen: open,\n selectByExpressionLayerId: open ? (layerId ?? null) : null,\n },\n })),\n setSelectByLocationOpen: (open, layerId) =>\n set((s) => ({\n ui: {\n ...s.ui,\n selectByLocationOpen: open,\n selectByLocationLayerId: open ? (layerId ?? null) : null,\n },\n })),\n setCollaborateDialogOpen: (open) =>\n set((s) => ({ ui: { ...s.ui, collaborateDialogOpen: open } })),\n setZoomToSelectedFeature: (enabled) =>\n set((s) => ({ ui: { ...s.ui, zoomToSelectedFeature: enabled } })),\n\n setStyleManagerOpen: (open) => set((s) => ({ ui: { ...s.ui, styleManagerOpen: open } })),\n});\n","/**\n * The app store: one Zustand store wrapped in zundo's undo history, composed\n * from the slices under `./store/` (zustand's slices pattern). The split is\n * internal — the hook, the state shape and every export are unchanged, and\n * consumers keep importing from here (or `@geolibre/core`).\n *\n * Slices: project (identity and lifecycle), map-view (camera, basemap, grid),\n * layers, layer-groups, project-content (comments, dashboard, story map),\n * processing (models, run history), libraries (style / layer / template),\n * session (selection and live readouts), collaboration, capabilities, and ui\n * (dialog and panel flags). Undo history lives in `./store/undo-history.ts`.\n */\nimport { create } from \"zustand\";\nimport { temporal } from \"zundo\";\nimport { appPrivilegeReason } from \"./capabilities\";\nimport { setActiveEllipsoidId } from \"./ellipsoids\";\nimport type { AppPrivilege } from \"./types\";\nimport { createCapabilitiesSlice } from \"./store/capabilities-slice\";\nimport { createCollaborationSlice } from \"./store/collaboration-slice\";\nimport { createLayerGroupsSlice } from \"./store/layer-groups-slice\";\nimport { createLayersSlice } from \"./store/layers-slice\";\nimport { createLibrariesSlice } from \"./store/libraries-slice\";\nimport { createMapViewSlice } from \"./store/map-view-slice\";\nimport { createProcessingSlice } from \"./store/processing-slice\";\nimport { createProjectContentSlice } from \"./store/project-content-slice\";\nimport { createProjectSlice } from \"./store/project-slice\";\nimport { createSessionSlice } from \"./store/session-slice\";\nimport { createUiSlice } from \"./store/ui-slice\";\nimport type { AppState } from \"./store/types\";\nimport { bindHistoryStore, createHistoryOptions } from \"./store/undo-history\";\n\nexport type { AppState } from \"./store/types\";\nexport type {\n ConversionToolKind,\n NetworkToolKind,\n RasterToolKind,\n StatisticsToolKind,\n VectorToolKind,\n} from \"./store/ui-slice\";\nexport {\n type GpsStatusFix,\n IDENTIFY_ALL_LAYERS_ID,\n type IdentifyState,\n identifyAllIncludes,\n identifyStateWithoutLayers,\n resolveIdentifyTarget,\n} from \"./store/session-slice\";\nexport { DEFAULT_COLLABORATION_STATE } from \"./store/collaboration-slice\";\nexport { projectPathLabel } from \"./store/project-slice\";\nexport {\n canRedoProjectRestore,\n canUndoProjectRestore,\n clearHistory,\n withoutHistory,\n redo,\n registerProjectRestoreHistory,\n subscribeProjectRestoreHistory,\n undo,\n} from \"./store/undo-history\";\n\nexport const useAppStore = create<AppState>()(\n temporal(\n (set, get) => ({\n ...createProjectSlice(set, get),\n ...createMapViewSlice(set, get),\n ...createLayersSlice(set, get),\n ...createLayerGroupsSlice(set, get),\n ...createProjectContentSlice(set, get),\n ...createProcessingSlice(set, get),\n ...createLibrariesSlice(set, get),\n ...createSessionSlice(set, get),\n ...createCollaborationSlice(set, get),\n ...createCapabilitiesSlice(set, get),\n ...createUiSlice(set, get),\n }),\n createHistoryOptions(),\n ),\n);\n\nbindHistoryStore(useAppStore);\n\n// Mirror the project's ellipsoid into the module-level singleton the\n// measurement helpers read. One subscription covers every path that changes\n// preferences (setPreferences, load/new project, undo/redo) without threading\n// the value through each call site. The subscription runs on every store\n// mutation (e.g. setPointerCoords on each mousemove), so guard on the id to skip\n// the redundant work for a value that changes at most once per session.\nlet lastEllipsoidId = useAppStore.getState().preferences.map.ellipsoidId;\nsetActiveEllipsoidId(lastEllipsoidId);\nuseAppStore.subscribe((state) => {\n const id = state.preferences.map.ellipsoidId;\n if (id === lastEllipsoidId) return;\n lastEllipsoidId = id;\n setActiveEllipsoidId(id);\n});\n\n/**\n * Whether the map should fit to data the user just added, per the project's\n * Map Preferences. Add-data paths consult this before an automatic fit; an\n * explicit \"Zoom to layer\" request ignores it.\n *\n * @returns `true` unless the user turned off zooming to new layers.\n */\nexport function shouldZoomToNewLayers(): boolean {\n return useAppStore.getState().preferences.map.zoomToNewLayers !== false;\n}\n\n/**\n * React hook for consuming application capability state for a specific privilege.\n *\n * @param privilege - The privilege to check.\n * @returns `{ granted: boolean, reason?: string }`\n */\nexport function useAppCapability(privilege: AppPrivilege): { granted: boolean; reason?: string } {\n const capabilities = useAppStore((state) => state.capabilities);\n return {\n granted: capabilities.privileges.includes(privilege),\n reason: appPrivilegeReason(capabilities, privilege),\n };\n}\n","import { useAppStore } from \"./store\";\nimport { applySelectionMode, type SelectionMode } from \"./selection\";\n\n/**\n * Store-touching selection helpers. Kept out of `selection.ts` so that module\n * stays a pure, directly testable set of functions over id arrays.\n */\n\n/**\n * Applies a matched id set to the live selection under the given mode and\n * returns the resulting selection size. Combines with the current selection\n * only when the target layer already holds it (ids are per-layer, so a\n * cross-layer combine would mix unrelated features). Otherwise `current` is\n * empty: \"new\"/\"add\" start a fresh selection on the target layer, while\n * \"remove\"/\"intersect\" necessarily yield an empty one — the dialogs guard\n * this by forcing mode \"new\" when the target layer does not hold the\n * selection (SelectionModeField's disableCombineModes). `selectLayer` runs\n * before `selectFeatures` because it clears the selection as a side effect.\n *\n * Lives in `@geolibre/core` rather than the app so the layer-panel dialogs and\n * the map's drawing gestures (`@geolibre/map`) share one copy of this rule\n * instead of two that can drift apart.\n */\nexport function applyMatchedSelection(\n targetLayerId: string,\n matchedIds: string[],\n mode: SelectionMode,\n): number {\n const store = useAppStore.getState();\n const current = store.selectedLayerId === targetLayerId ? store.selectedFeatureIds : [];\n const next = applySelectionMode(current, matchedIds, mode);\n if (store.selectedLayerId !== targetLayerId) store.selectLayer(targetLayerId);\n store.selectFeatures(next);\n return next.length;\n}\n","/**\n * Shared scale-bar unit maths.\n *\n * The on-screen scale bar (`PlanetaryScaleControl` in `@geolibre/map`) and the\n * Print Layout scale bar (`apps/geolibre-desktop/src/lib/print-layout.ts`) both\n * need to pick a \"nice\" round distance in the user's chosen unit system\n * (metric / imperial / nautical). Keeping that logic in one place here means the\n * two bars round a given ground span to the same value and share the same unit\n * constants, so a change to one can never silently diverge from the other.\n */\n\nimport type { MapScaleUnit } from \"./types\";\n\n/** Conversion constants shared by every scale-bar denomination. */\nexport const FEET_PER_METER = 3.2808398950131235; // 1 / 0.3048\nexport const METERS_PER_FOOT = 0.3048;\nexport const FEET_PER_MILE = 5280;\nexport const METERS_PER_MILE = 1609.344; // 5280 ft\nexport const METERS_PER_NAUTICAL_MILE = 1852;\n\n/**\n * The largest `1 / 2 / 3 / 5 / 10 × 10ⁿ` number that does not exceed `num`, so\n * the bar lands on a readable round value. Uses log10 for the magnitude (unlike\n * MapLibre's digit-count trick, which returns 1 for any `0 < num < 1` and so\n * rounds *up* — producing a bar wider than the max width for sub-unit spans).\n * Returns 0 for a non-positive input so callers can clamp or fall back.\n */\nexport function getRoundNum(num: number): number {\n if (!(num > 0)) return 0;\n const pow10 = Math.pow(10, Math.floor(Math.log10(num)));\n const d = num / pow10;\n const nice = d >= 10 ? 10 : d >= 5 ? 5 : d >= 3 ? 3 : d >= 2 ? 2 : 1;\n return pow10 * nice;\n}\n\n/** Trim trailing-zero noise from a rounded value (e.g. 0.5, 2, 300). */\nexport function formatRoundNum(value: number): string {\n return Number.parseFloat(value.toPrecision(12)).toString();\n}\n\n/**\n * The denomination a scale bar rounds and labels distances with for a ground\n * span of `maxMeters` in the requested unit system: km/m for metric, mi/ft for\n * imperial, and nautical miles for nautical. The unit crosses to the larger\n * denomination once the span exceeds one of it. `metersPerUnit` is the size of\n * one unit of that denomination in metres, so a caller can round the span in\n * unit space and convert the result back to metres (and thus pixels).\n */\nexport function scaleDenomination(\n maxMeters: number,\n unit: MapScaleUnit,\n): { metersPerUnit: number; label: string } {\n if (unit === \"imperial\") {\n const feet = maxMeters * FEET_PER_METER;\n return feet >= FEET_PER_MILE\n ? { metersPerUnit: METERS_PER_MILE, label: \"mi\" }\n : { metersPerUnit: METERS_PER_FOOT, label: \"ft\" };\n }\n if (unit === \"nautical\") {\n return { metersPerUnit: METERS_PER_NAUTICAL_MILE, label: \"nmi\" };\n }\n return maxMeters >= 1000\n ? { metersPerUnit: 1000, label: \"km\" }\n : { metersPerUnit: 1, label: \"m\" };\n}\n\n/**\n * Convert a ground distance in metres into the span (already divided into the\n * bar's denomination) and its label — the shape the on-screen control consumes.\n * Derived from {@link scaleDenomination} so the two never diverge.\n */\nexport function scaleSpan(maxMeters: number, unit: MapScaleUnit): { span: number; label: string } {\n const { metersPerUnit, label } = scaleDenomination(maxMeters, unit);\n return { span: maxMeters / metersPerUnit, label };\n}\n","/**\n * Shared ground-elevation resolution.\n *\n * Several places in the app need \"how high is the ground at this coordinate\":\n * the Elevation Profile plugin (along a drawn line), the Measure tool's\n * terrain-aware readout (along a measured geometry), and the status bar (under\n * the cursor). They all resolve it the same way, from two sources tried in\n * order:\n *\n * 1. The map's own terrain (`map.queryTerrainElevation`) when 3D terrain is\n * enabled — instant and offline, but only where DEM tiles are loaded.\n * MapLibre returns elevations multiplied by the terrain exaggeration, so\n * values are divided back to true meters.\n * 2. The keyless Open-Meteo elevation API — works without terrain enabled and\n * anywhere on Earth, but costs a network round-trip.\n *\n * This module lives in `@geolibre/core` rather than beside the Elevation\n * Profile plugin because `@geolibre/map` (which owns the pointer readout)\n * cannot import from `@geolibre/plugins` without a dependency cycle — plugins\n * already depends on map. Core is the only package all three can share.\n *\n * Source 2 is Earth-only: Open-Meteo has no data for the planetary basemaps, so\n * callers must not reach for it when the active body is not Earth.\n */\n\n/** A coordinate as `[longitude, latitude]` in degrees. */\nexport type LngLat = [number, number];\n\n/** Open-Meteo accepts at most 100 coordinates per elevation request. */\nexport const MAX_POINTS_PER_REQUEST = 100;\n\n/** Abort an elevation request that has not responded within this window. */\nexport const ELEVATION_REQUEST_TIMEOUT_MS = 15000;\n\nconst ENDPOINT = \"https://api.open-meteo.com/v1/elevation\";\n\n/** Error thrown when an elevation request cannot be completed or parsed. */\nexport class ElevationFetchError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ElevationFetchError\";\n }\n}\n\n/** A `fetch`-compatible function, so tests can inject a stub. */\nexport type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;\n\ninterface ElevationResponse {\n elevation?: number[];\n}\n\n/**\n * Normalize longitude into [-180, 180] degrees (handles unwrapped coordinates\n * from antimeridian cross-overs and world-copy pannings).\n */\nexport function normalizeLongitude(lng: number): number {\n if (!Number.isFinite(lng)) return 0;\n return ((((lng + 180) % 360) + 360) % 360) - 180;\n}\n\n/**\n * Clamp latitude into [-90, 90] degrees.\n */\nexport function clampLatitude(lat: number): number {\n if (!Number.isFinite(lat)) return 0;\n return Math.min(90, Math.max(-90, lat));\n}\n\n/**\n * Fetch elevations (in meters) for an ordered list of coordinates.\n *\n * @param points - Coordinates as `[lng, lat]`, at most {@link MAX_POINTS_PER_REQUEST}\n * @param fetchImpl - Optional `fetch` implementation; defaults to the global `fetch`\n * @returns The elevation in meters for each input coordinate, in the same order\n * @throws {ElevationFetchError} On too many points, a non-finite coordinate, a\n * network error, a non-2xx response, a malformed body, or a length mismatch\n */\nexport async function fetchElevations(points: LngLat[], fetchImpl?: FetchLike): Promise<number[]> {\n if (points.length === 0) return [];\n if (points.length > MAX_POINTS_PER_REQUEST) {\n throw new ElevationFetchError(\n `Too many points: ${points.length} (max ${MAX_POINTS_PER_REQUEST}).`,\n );\n }\n // Normalizing would turn NaN/Infinity into 0 and query (0, 0) as if it were\n // the requested point, so reject them instead of returning a wrong elevation.\n if (points.some(([lng, lat]) => !Number.isFinite(lng) || !Number.isFinite(lat))) {\n throw new ElevationFetchError(\"Invalid coordinate: longitude and latitude must be finite.\");\n }\n\n const doFetch: FetchLike = fetchImpl ?? ((url, init) => fetch(url, init));\n const latitudes = points.map((p) => clampLatitude(p[1]).toFixed(6)).join(\",\");\n const longitudes = points.map((p) => normalizeLongitude(p[0]).toFixed(6)).join(\",\");\n const url = `${ENDPOINT}?latitude=${latitudes}&longitude=${longitudes}`;\n\n // A default fetch never times out, so a hung request would leave the control's\n // busy state stuck with no recovery. Abort after ELEVATION_REQUEST_TIMEOUT_MS\n // and surface it as a normal fetch error the caller already handles.\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), ELEVATION_REQUEST_TIMEOUT_MS);\n let response: Response;\n try {\n response = await doFetch(url, { signal: controller.signal });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw new ElevationFetchError(\"Elevation request timed out.\");\n }\n const detail = error instanceof Error ? error.message : \"unknown error\";\n throw new ElevationFetchError(`Could not reach the elevation service: ${detail}`);\n } finally {\n clearTimeout(timeoutId);\n }\n\n if (!response.ok) {\n throw new ElevationFetchError(`Elevation request failed (HTTP ${response.status}).`);\n }\n\n let data: ElevationResponse;\n try {\n data = (await response.json()) as ElevationResponse;\n } catch {\n throw new ElevationFetchError(\"Could not parse the elevation response.\");\n }\n\n if (!data || !Array.isArray(data.elevation)) {\n throw new ElevationFetchError(\"Malformed elevation response.\");\n }\n if (data.elevation.length !== points.length) {\n throw new ElevationFetchError(\n `Expected ${points.length} elevations but received ${data.elevation.length}.`,\n );\n }\n\n return data.elevation;\n}\n\n/** The slice of the MapLibre map the terrain sampler needs (stubbed in tests). */\nexport interface TerrainMapLike {\n getTerrain?: () => { exaggeration?: number } | null | undefined;\n queryTerrainElevation?: (lngLat: LngLat) => number | null;\n}\n\n/**\n * Sample elevations from the map's enabled 3D terrain, in true meters\n * (MapLibre's `queryTerrainElevation` bakes the exaggeration in, so it is\n * divided back out). Returns null when terrain is not enabled.\n */\nexport function sampleMapTerrain(\n map: TerrainMapLike | null | undefined,\n points: LngLat[],\n): (number | null)[] | null {\n if (!map?.getTerrain || !map.queryTerrainElevation) return null;\n const terrain = map.getTerrain();\n if (!terrain) return null;\n const exaggeration =\n typeof terrain.exaggeration === \"number\" && terrain.exaggeration > 0 ? terrain.exaggeration : 1;\n return points.map((point) => {\n const elevation = map.queryTerrainElevation!(point);\n return typeof elevation === \"number\" && Number.isFinite(elevation)\n ? elevation / exaggeration\n : null;\n });\n}\n\n/**\n * Sample a single point's elevation from enabled 3D terrain, in true meters.\n * Returns null when terrain is off or the DEM tile under the point has not\n * loaded — the caller decides whether that is worth a network round-trip.\n */\nexport function sampleMapTerrainPoint(\n map: TerrainMapLike | null | undefined,\n point: LngLat,\n): number | null {\n return sampleMapTerrain(map, [point])?.[0] ?? null;\n}\n\n/**\n * Sample elevations from the Open-Meteo API, chunked to its 100-point request\n * limit. A failed chunk yields nulls for its points rather than throwing, so a\n * flaky network degrades the readout instead of breaking the caller.\n */\nexport async function sampleRemoteElevations(\n points: LngLat[],\n fetchImpl?: FetchLike,\n): Promise<(number | null)[]> {\n const chunks: LngLat[][] = [];\n for (let i = 0; i < points.length; i += MAX_POINTS_PER_REQUEST) {\n chunks.push(points.slice(i, i + MAX_POINTS_PER_REQUEST));\n }\n // The chunks are independent, so fire them concurrently; Promise.all\n // preserves their order for reassembly.\n const chunkResults = await Promise.all(\n chunks.map(async (chunk) => {\n try {\n const elevations = await fetchElevations(chunk, fetchImpl);\n return elevations.map((elevation) => (Number.isFinite(elevation) ? elevation : null));\n } catch {\n return chunk.map(() => null);\n }\n }),\n );\n return chunkResults.flat();\n}\n\n// --- Pointer elevation resolver --------------------------------------------\n\n/** Round a coordinate to ~11 m so nearby hovers share one cache entry. */\nfunction cacheKey(point: LngLat): string {\n return `${point[0].toFixed(4)},${point[1].toFixed(4)}`;\n}\n\n/** How long the pointer must sit still before a remote lookup is worth it. */\nexport const POINTER_ELEVATION_DEBOUNCE_MS = 500;\n\n/** Remote results kept per resolver; bounded so a long session cannot grow it without limit. */\nexport const POINTER_ELEVATION_CACHE_LIMIT = 500;\n\nexport interface PointerElevationResolverOptions {\n /** The live map, for the terrain sample. May return null before init. */\n getMap: () => TerrainMapLike | null | undefined;\n /**\n * Whether the readout is switched on (Controls -> Elevation). Read per call\n * rather than captured, and rechecked before publishing a remote result, so\n * turning it off mid-request neither shows a late value nor leaves the last\n * one on screen. Defaults to always-on for callers that do not gate it.\n */\n isEnabled?: () => boolean;\n /**\n * Whether the *remote* Open-Meteo fallback may run. The terrain path sends\n * nothing anywhere, so it is deliberately not gated by this — a user who has\n * declined the network lookup still gets a live readout wherever 3D terrain\n * is on. Defaults to allowed for callers that do not gate it.\n */\n canUseRemote?: () => boolean;\n /** Whether the active body is Earth — Open-Meteo has no data for anything else. */\n isEarth: () => boolean;\n /** Called with the resolved elevation in true metres, or null when unknown. */\n emit: (elevation: number | null) => void;\n fetchImpl?: FetchLike;\n debounceMs?: number;\n}\n\nexport interface PointerElevationResolver {\n /** Report a new pointer position, or null when the pointer leaves the map. */\n update: (point: LngLat | null) => void;\n /**\n * Drop any pending *and* in-flight lookup without tearing the resolver down.\n * Used when the map's context changes under it — loading another project\n * resets the readout, and a response for the previous project must not\n * repaint it.\n */\n invalidate: () => void;\n /** Cancel any pending lookup. */\n dispose: () => void;\n}\n\n/**\n * Resolves the ground elevation under the pointer for the status bar.\n *\n * Two paths, deliberately asymmetric:\n *\n * - **Terrain enabled** — `queryTerrainElevation` is synchronous and free, so\n * it runs on every move and the readout tracks the cursor live.\n * - **Terrain disabled** — falls back to Open-Meteo, but only after the\n * pointer has been still for {@link POINTER_ELEVATION_DEBOUNCE_MS}, and only\n * on Earth. Results are cached per ~11 m cell, so sweeping back over ground\n * already looked up costs nothing. Without both guards a hover readout would\n * fire a request per mousemove at a free, keyless public service.\n *\n * While a remote lookup is outstanding the readout reads null rather than the\n * previous point's value: showing a stale height next to fresh coordinates\n * would be worse than showing nothing.\n */\nexport function createPointerElevationResolver(\n options: PointerElevationResolverOptions,\n): PointerElevationResolver {\n const { getMap, isEarth, emit, fetchImpl } = options;\n const isEnabled = options.isEnabled ?? (() => true);\n const canUseRemote = options.canUseRemote ?? (() => true);\n const debounceMs = options.debounceMs ?? POINTER_ELEVATION_DEBOUNCE_MS;\n const cache = new Map<string, number | null>();\n let timer: ReturnType<typeof setTimeout> | null = null;\n // Bumped on every update so a slow response for an abandoned point is dropped\n // rather than overwriting the readout for wherever the pointer is now.\n let generation = 0;\n\n const cancelPending = () => {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n };\n\n const update = (point: LngLat | null) => {\n generation += 1;\n cancelPending();\n\n if (!point || !isEnabled()) {\n emit(null);\n return;\n }\n\n const fromTerrain = sampleMapTerrainPoint(getMap(), point);\n if (fromTerrain !== null) {\n emit(fromTerrain);\n return;\n }\n\n // Open-Meteo has no data off Earth, and the remote path needs consent.\n if (!isEarth() || !canUseRemote()) {\n emit(null);\n return;\n }\n\n const key = cacheKey(point);\n if (cache.has(key)) {\n const cached = cache.get(key) ?? null;\n // Re-insert to move this entry to the end: Map iterates in insertion\n // order, so the first key is always the least recently used.\n cache.delete(key);\n cache.set(key, cached);\n emit(cached);\n return;\n }\n\n emit(null);\n const requested = generation;\n timer = setTimeout(() => {\n timer = null;\n void (async () => {\n const [elevation] = await sampleRemoteElevations([point], fetchImpl);\n const value = elevation ?? null;\n // `sampleRemoteElevations` turns a fetch failure into a null, which is\n // indistinguishable here from \"no data for this cell\". Caching that\n // would blackhole the readout for this ~11 m cell for the rest of the\n // session, so only successes are remembered and a transient network\n // blip gets another chance on the next hover.\n if (value !== null) {\n // Evict just the oldest entry. Clearing the whole cache at the cap\n // would make every 500th distinct cell trigger a burst of re-fetches\n // for cells that may still be under active hovering.\n if (cache.size >= POINTER_ELEVATION_CACHE_LIMIT) {\n const oldest = cache.keys().next();\n if (!oldest.done) cache.delete(oldest.value);\n }\n cache.set(key, value);\n }\n // The pointer moved, left, or the resolver was disposed while the\n // request was in flight. Also recheck the body: a switch to a planetary\n // basemap mid-request must not publish an Earth elevation over it.\n if (requested !== generation || !isEarth() || !isEnabled() || !canUseRemote()) return;\n emit(value);\n })();\n }, debounceMs);\n };\n\n // Bumping the generation is what actually invalidates an *in-flight*\n // request; cancelPending alone only clears a timer that has not fired yet, so\n // a fetch already in progress would still emit into a torn-down map.\n const invalidate = (): void => {\n generation += 1;\n cancelPending();\n };\n\n return { update, invalidate, dispose: invalidate };\n}\n","/**\n * Camera altitude (\"Eye alt\") for the status bar — issue #1816.\n *\n * MapLibre's `transform.getCameraAltitude()` returns the camera height above\n * sea level in metres, which is exactly what Google Earth Pro's `Eye alt`\n * reports. But it derives that height from the Web-Mercator scale, which is\n * built on Earth's radius — so on a Moon / Mars / Mercury basemap it is wrong by\n * the ratio of that body's circumference to Earth's, the same way MapLibre's\n * built-in ScaleControl is (see `PlanetaryScaleControl`, which fixes the\n * horizontal case).\n *\n * {@link scaleAltitudeToActiveBody} applies the matching correction so the\n * readout is right on every body, using the same active-ellipsoid singleton the\n * scale bar and the measurement tools read.\n */\n\nimport { getActiveBodyRadiusRatio } from \"./ellipsoids\";\nimport { scaleDenomination } from \"./scale-units\";\nimport type { MapScaleUnit } from \"./types\";\n\n/**\n * Convert a MapLibre camera altitude (computed against Earth's radius) into the\n * active body's true altitude. A no-op on Earth.\n *\n * @param earthAltitudeMeters Altitude in metres as MapLibre reports it\n * @returns Altitude in metres above the active body's datum, or null when the\n * input is not a usable number (no map, degenerate transform, globe edge case)\n */\nexport function scaleAltitudeToActiveBody(earthAltitudeMeters: number | null): number | null {\n if (earthAltitudeMeters === null || !Number.isFinite(earthAltitudeMeters)) return null;\n return earthAltitudeMeters * getActiveBodyRadiusRatio();\n}\n\n/**\n * Format a camera altitude for the status bar.\n *\n * Altitude spans metres (standing on a ridge) to tens of thousands of\n * kilometres (whole-globe view), so it borrows the scale bar's denomination\n * switching rather than printing \"12,742,000 m\". Metric crosses to km past\n * 1 km, imperial to miles past a mile, and nautical stays in nautical miles —\n * the same thresholds {@link scaleDenomination} gives the scale bar, so the two\n * always agree about which unit this view is being described in.\n */\nexport function formatCameraAltitude(meters: number, unit: MapScaleUnit): string {\n const { metersPerUnit, label } = scaleDenomination(Math.abs(meters), unit);\n const value = meters / metersPerUnit;\n // Sub-unit and single-digit values need a decimal to say anything; past that\n // the extra digit is noise on a number that moves with every zoom step.\n const digits = Math.abs(value) < 10 ? 1 : 0;\n return `${value.toLocaleString(undefined, {\n minimumFractionDigits: digits,\n maximumFractionDigits: digits,\n })} ${label}`;\n}\n","// A layers file holds a set of layer records that point at their data rather\n// than carry it: tile and service URLs, remote files, and absolute local paths\n// the desktop app can re-read. Project → Export → Export Layers writes one, and\n// the Startup setting adds its layers to every untitled workspace (at launch\n// and on File → New). Layers whose features exist only in memory are left out,\n// so the file stays small enough to keep a copy of in the desktop settings.\n\nimport { hasRestorableLayerSource, layerLocalPath } from \"./layer-library\";\nimport { createDefaultMapView, isSessionOnlyLayer, parseProject } from \"./project\";\nimport {\n LAYER_TYPES,\n PROJECT_VERSION,\n type GeoLibreLayer,\n type GeoLibreProject,\n type LayerGroup,\n} from \"./types\";\n\n/** `type` tag identifying a layers file. */\nexport const LAYERS_FILE_TYPE = \"geolibre-layers\";\n\n/** Format version written by {@link serializeLayersFile}. */\nexport const LAYERS_FILE_VERSION = 1;\n\n/**\n * Size ceiling for a layers file, in UTF-8 bytes. A file of references is a few\n * kilobytes per layer, and the Startup setting keeps a parsed copy in the\n * desktop settings (localStorage), so a file past this is refused rather than\n * crowding out the rest of the settings.\n */\nexport const MAX_LAYERS_FILE_BYTES = 1024 * 1024;\n\n/** The layers and the folders they sit in. */\nexport interface LayersFileContent {\n /** Layer records in store (bottom-to-top) order. */\n layers: GeoLibreLayer[];\n /** The folders the layers belong to, with their ancestors. */\n layerGroups: LayerGroup[];\n}\n\n/** The on-disk layers document. */\nexport interface LayersFile extends LayersFileContent {\n type: typeof LAYERS_FILE_TYPE;\n version: typeof LAYERS_FILE_VERSION;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isHttpUrl(value: unknown): value is string {\n if (typeof value !== \"string\") return false;\n try {\n const url = new URL(value.trim());\n return url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n return false;\n }\n}\n\n/** A `blob:` or `data:` URL is the data itself, not a reference to it. */\nfunction isInlineUrl(value: unknown): boolean {\n return typeof value === \"string\" && /^\\s*(blob|data):/i.test(value);\n}\n\nfunction hasInlineUrl(layer: GeoLibreLayer): boolean {\n const source = layer.source ?? {};\n const tiles = Array.isArray(source.tiles) ? source.tiles : [];\n return (\n isInlineUrl(source.url) ||\n isInlineUrl(source.data) ||\n tiles.some(isInlineUrl) ||\n isInlineUrl(layer.sourcePath) ||\n isInlineUrl((layer.metadata ?? {}).originalUrl)\n );\n}\n\n/**\n * Whether a layer (in its saved form) points at its data rather than carrying\n * it: it has a re-fetchable source, an http(s) source path, or an absolute\n * local path the desktop app re-reads, and holds no inline features.\n *\n * @param layer - A layer as `projectFromStore` writes it.\n * @returns True when the layer can go in a layers file.\n */\nexport function isReferencedLayer(layer: GeoLibreLayer): boolean {\n if (isSessionOnlyLayer(layer)) return false;\n if (layer.geojson) return false;\n const source = layer.source ?? {};\n const metadata = layer.metadata ?? {};\n if (source.data !== undefined && typeof source.data !== \"string\") return false;\n if (source.czmlData !== undefined) return false;\n if (metadata.embeddedGeoJSON !== undefined) return false;\n if (hasInlineUrl(layer)) return false;\n return (\n hasRestorableLayerSource(layer) ||\n layerLocalPath(layer) !== undefined ||\n isHttpUrl(layer.sourcePath)\n );\n}\n\n/** Folders the given layers sit in, plus every ancestor, in their original order. */\nfunction groupsForLayers(\n layers: readonly GeoLibreLayer[],\n groups: readonly LayerGroup[],\n): LayerGroup[] {\n const byId = new Map(groups.map((group) => [group.id, group]));\n const keep = new Set<string>();\n for (const layer of layers) {\n let id = layer.groupId;\n // The `keep` check also stops a hand-edited parent cycle.\n while (id && !keep.has(id) && byId.has(id)) {\n keep.add(id);\n id = byId.get(id)?.parentId;\n }\n }\n return groups.filter((group) => keep.has(group.id));\n}\n\n/**\n * Pick the layers of a saved project that can go in a layers file.\n *\n * @param project - A project as `projectFromStore` writes it. A host that can\n * re-fetch a layer's features (a GeoJSON layer loaded from a URL) strips its\n * `geojson` first, so the layer counts as a reference here.\n * @returns The file content, and the names of the layers left out because they\n * carry their data inline.\n */\nexport function extractLayersFileContent(\n project: Pick<GeoLibreProject, \"layers\" | \"layerGroups\">,\n): { content: LayersFileContent; skipped: string[] } {\n const layers: GeoLibreLayer[] = [];\n const skipped: string[] = [];\n for (const layer of project.layers) {\n if (isReferencedLayer(layer)) layers.push(layer);\n else if (!isSessionOnlyLayer(layer)) skipped.push(layer.name);\n }\n return {\n content: { layers, layerGroups: groupsForLayers(layers, project.layerGroups ?? []) },\n skipped,\n };\n}\n\n/**\n * Serialize layers into the JSON written by Export Layers.\n *\n * @param content - The layers and their folders.\n * @returns Pretty-printed file content.\n */\nexport function serializeLayersFile(content: LayersFileContent): string {\n const file: LayersFile = {\n type: LAYERS_FILE_TYPE,\n version: LAYERS_FILE_VERSION,\n layers: [...content.layers],\n layerGroups: [...content.layerGroups],\n };\n return JSON.stringify(file, null, 2);\n}\n\n/**\n * Coerce untrusted layers (a hand-edited file, or the copy kept in settings)\n * into clean ones. Each layer goes through the same normalization a project\n * file's layers do; one without an id, a name, a known type, or a reference to\n * its data is dropped, as is a repeated id.\n *\n * @param value - An object with `layers` and optional `layerGroups` arrays.\n * @returns The usable layers and the folders they sit in.\n */\nexport function normalizeLayersFileContent(value: unknown): LayersFileContent {\n const empty: LayersFileContent = { layers: [], layerGroups: [] };\n if (!isPlainObject(value) || !Array.isArray(value.layers)) return empty;\n try {\n return normalizeLayers(value.layers, value.layerGroups);\n } catch {\n // A corrupt copy in settings must not break loading the rest of them.\n return empty;\n }\n}\n\n/**\n * The body of {@link normalizeLayersFileContent}, which catches what it throws.\n *\n * @param rawLayers - The raw `layers` array.\n * @param rawGroups - The raw `layerGroups` value.\n * @returns The usable layers and the folders they sit in, or nothing when the\n * layers are past {@link MAX_LAYERS_FILE_BYTES}.\n */\nfunction normalizeLayers(rawLayers: unknown[], rawGroups: unknown): LayersFileContent {\n const seen = new Set<string>();\n const candidates = rawLayers.filter((layer): layer is Record<string, unknown> => {\n if (!isPlainObject(layer)) return false;\n if (typeof layer.id !== \"string\" || !layer.id.trim() || seen.has(layer.id)) return false;\n if (typeof layer.name !== \"string\") return false;\n if (!(LAYER_TYPES as readonly unknown[]).includes(layer.type)) return false;\n if (!isPlainObject(layer.source)) return false;\n seen.add(layer.id);\n return true;\n });\n // Borrow the project parser rather than duplicate its per-layer rules: the\n // layers below are exactly what a project file with only these layers holds.\n // The round trip through JSON also bounds the size of what is kept.\n const json = JSON.stringify({\n version: PROJECT_VERSION,\n name: \"layers\",\n mapView: createDefaultMapView(),\n layers: candidates.map((layer) => ({\n ...layer,\n metadata: isPlainObject(layer.metadata) ? layer.metadata : {},\n style: isPlainObject(layer.style) ? layer.style : {},\n })),\n layerGroups: Array.isArray(rawGroups) ? rawGroups : [],\n });\n if (new TextEncoder().encode(json).length > MAX_LAYERS_FILE_BYTES) {\n return { layers: [], layerGroups: [] };\n }\n const parsed = parseProject(json);\n const layers = parsed.layers.filter(isReferencedLayer);\n return { layers, layerGroups: groupsForLayers(layers, parsed.layerGroups ?? []) };\n}\n\n/**\n * Parse a file written by {@link serializeLayersFile}.\n *\n * @param json - The file content.\n * @returns The normalized layers and folders.\n * @throws Error when the content is too large, is not a layers file, comes\n * from a newer format version, or holds no usable layers.\n */\nexport function parseLayersFile(json: string): LayersFileContent {\n if (new TextEncoder().encode(json).length > MAX_LAYERS_FILE_BYTES) {\n throw new Error(\"The layers file is too large. Keep it under 1 MB.\");\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new Error(\"Not a valid layers file (invalid JSON).\");\n }\n if (!isPlainObject(parsed) || parsed.type !== LAYERS_FILE_TYPE) {\n throw new Error(\"Not a valid layers file.\");\n }\n // Refuse a newer format rather than misread it with this version's rules.\n if (parsed.version !== LAYERS_FILE_VERSION) {\n throw new Error(\"Unsupported layers file version.\");\n }\n const content = normalizeLayersFileContent(parsed);\n if (content.layers.length === 0) {\n throw new Error(\"The layers file holds no usable layers.\");\n }\n return content;\n}\n\n/**\n * Add a layers file's layers on top of a project's own, keeping the project's\n * layers where an id repeats.\n *\n * @param project - The project to add to (usually a new, empty one).\n * @param content - The layers and folders to add.\n * @returns A new project with the layers added.\n */\nexport function addLayersToProject(\n project: GeoLibreProject,\n content: LayersFileContent,\n): GeoLibreProject {\n const layerIds = new Set(project.layers.map((layer) => layer.id));\n const groups = project.layerGroups ?? [];\n const groupIds = new Set(groups.map((group) => group.id));\n const layers = content.layers.filter((layer) => !layerIds.has(layer.id));\n const layerGroups = content.layerGroups.filter((group) => !groupIds.has(group.id));\n return {\n ...project,\n layers: [...project.layers, ...structuredClone(layers)],\n layerGroups: [...groups, ...structuredClone(layerGroups)],\n };\n}\n","/**\n * Trim a float sample to something readable. A 32-bit raster value decoded to a\n * JS double prints all 17 digits of its binary representation\n * (`48.11851119995117`), which is noise past the sensor's precision — six\n * significant digits is more than any COG carries. Integers are left alone so a\n * classification code is never shown in exponential form.\n *\n * @param value A decoded raster sample.\n * @returns The sample rendered for display.\n */\nexport function formatPixelValue(value: number): string {\n if (!Number.isFinite(value)) return String(value);\n if (Number.isInteger(value)) return String(value);\n return String(Number(value.toPrecision(6)));\n}\n","/**\n * Amazon S3 (and S3-compatible) object URLs: parsing, public HTTPS mapping,\n * SigV4 query-string presigning, and the registry the app uses to plug\n * credentialed signing into every reader.\n *\n * Readers in GeoLibre take a plain HTTPS URL (geotiff.js, cog-tiler-wasm,\n * pmtiles, DuckDB-WASM's HTTP reader, the vector control). A presigned URL is\n * the one form of authenticated S3 access all of them accept without a custom\n * fetch hook, so private objects are read by signing the URL first and handing\n * the reader the result. Layers keep the unsigned source URL (`s3://…` or the\n * plain object URL); the signed one is minted again on every load, so a saved\n * project never carries a signature or a session token.\n */\n\n/** A bucket and object key, with the endpoint the URL named, if any. */\nexport interface S3ObjectLocation {\n bucket: string;\n /** The raw (decoded) object key, without a leading slash. */\n key: string;\n /** The region an AWS HTTPS URL named in its host, when it named one. */\n region?: string;\n}\n\n/** Where and how a bucket is addressed. */\nexport interface S3EndpointConfig {\n /** Signing region, e.g. `us-east-1`. S3-compatible stores often accept `auto`. */\n region: string;\n /**\n * Custom endpoint origin for S3-compatible stores (MinIO, Cloudflare R2,\n * Wasabi, Ceph), e.g. `https://minio.example.com:9000`. Empty for AWS.\n */\n endpoint?: string;\n /** Address the bucket in the path instead of the host name. */\n pathStyle?: boolean;\n}\n\n/** AWS credentials used to sign a request. */\nexport interface S3Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string;\n /** Epoch milliseconds at which temporary credentials stop working. */\n expiresAt?: number;\n}\n\n/** A presigned URL and the moment it stops working. */\nexport interface S3SignedUrl {\n href: string;\n /** Epoch milliseconds. */\n expiresAt: number;\n}\n\n/** SigV4 allows a presigned URL to live at most seven days. */\nexport const S3_MAX_PRESIGN_SECONDS = 7 * 24 * 60 * 60;\n\nconst S3_SCHEME = /^s3a?:\\/\\//i;\n// bucket.s3.amazonaws.com, bucket.s3.us-west-2.amazonaws.com,\n// bucket.s3-us-west-2.amazonaws.com, bucket.s3.dualstack.us-west-2.amazonaws.com\nconst VIRTUAL_HOST = /^(.+)\\.s3(?:[.-](?:dualstack\\.)?([a-z0-9-]+))?\\.amazonaws\\.com(?:\\.cn)?$/i;\n// s3.amazonaws.com, s3.us-west-2.amazonaws.com, s3-us-west-2.amazonaws.com\nconst PATH_HOST = /^s3(?:[.-](?:dualstack\\.)?([a-z0-9-]+))?\\.amazonaws\\.com(?:\\.cn)?$/i;\n\nfunction regionFromHost(value: string | undefined): string | undefined {\n // `s3-external-1` and `s3.dualstack` carry no region of their own.\n if (!value || value === \"external-1\" || value === \"dualstack\") return undefined;\n return value.toLowerCase();\n}\n\n/** Static-website endpoints serve HTML, not the REST API a signature targets. */\nfunction isWebsiteRegion(value: string | undefined): boolean {\n return Boolean(value && /^website/i.test(value));\n}\n\nfunction decodeKey(path: string): string {\n try {\n return decodeURIComponent(path);\n } catch {\n return path;\n }\n}\n\n/**\n * Parses an `s3://bucket/key` URI or an AWS S3 HTTPS object URL\n * (virtual-hosted or path-style, any region) into its bucket and key.\n * Query strings and fragments are ignored, so a presigned URL parses to the\n * object it signs.\n *\n * @param url The URL to parse.\n * @returns The bucket and key, or null when the URL is not an S3 object URL.\n */\nexport function parseS3Url(url: string): S3ObjectLocation | null {\n const trimmed = url.trim();\n if (S3_SCHEME.test(trimmed)) {\n const rest = trimmed.replace(S3_SCHEME, \"\").split(/[?#]/)[0];\n const slash = rest.indexOf(\"/\");\n const bucket = slash === -1 ? rest : rest.slice(0, slash);\n const key = slash === -1 ? \"\" : rest.slice(slash + 1);\n return isSafeBucketName(bucket) ? { bucket, key } : null;\n }\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n return null;\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") return null;\n const host = parsed.hostname.toLowerCase();\n const path = parsed.pathname.replace(/^\\//, \"\");\n const virtual = host.match(VIRTUAL_HOST);\n if (virtual && !PATH_HOST.test(host)) {\n if (isWebsiteRegion(virtual[2])) return null;\n const region = regionFromHost(virtual[2]);\n return { bucket: virtual[1], key: decodeKey(path), ...(region ? { region } : {}) };\n }\n const pathStyle = host.match(PATH_HOST);\n if (pathStyle) {\n if (isWebsiteRegion(pathStyle[1])) return null;\n const slash = path.indexOf(\"/\");\n const bucket = slash === -1 ? path : path.slice(0, slash);\n if (!bucket) return null;\n const region = regionFromHost(pathStyle[1]);\n return {\n bucket: decodeKey(bucket),\n key: slash === -1 ? \"\" : decodeKey(path.slice(slash + 1)),\n ...(region ? { region } : {}),\n };\n }\n return null;\n}\n\n/**\n * Bucket names are letters, digits, dots, dashes, and underscores (the last\n * two outside AWS's own rules, for S3-compatible stores). Anything else, a\n * backslash or `@` say, could move a virtual-hosted URL's authority to\n * another host before a signature is attached, so it is refused outright.\n */\nconst SAFE_BUCKET_NAME = /^[A-Za-z0-9._-]{1,255}$/;\n\nfunction isSafeBucketName(bucket: string): boolean {\n return SAFE_BUCKET_NAME.test(bucket);\n}\n\n/** Whether `url` is an `s3://` URI (as opposed to an HTTPS object URL). */\nexport function isS3Uri(url: string): boolean {\n return S3_SCHEME.test(url.trim());\n}\n\n/** Formats a location as an `s3://bucket/key` URI. */\nexport function formatS3Uri(location: S3ObjectLocation): string {\n return `s3://${location.bucket}/${location.key}`;\n}\n\n/**\n * URI-encodes a key the way SigV4 canonicalizes an S3 path: every byte except\n * the unreserved set and `/` is percent-encoded, once.\n */\nexport function encodeS3Key(key: string): string {\n return key\n .split(\"/\")\n .map((segment) =>\n encodeURIComponent(segment).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n ),\n )\n .join(\"/\");\n}\n\n/** A custom endpoint's path, without a trailing slash (\"\" for none). */\nfunction endpointPathPrefix(base: URL): string {\n return base.pathname.replace(/\\/+$/, \"\");\n}\n\n/**\n * Builds the HTTPS URL of an object.\n *\n * AWS buckets whose name contains a dot are addressed path-style, since the\n * `*.s3.amazonaws.com` wildcard certificate covers one label only.\n *\n * @param location The bucket and key.\n * @param config The endpoint; without one the global AWS endpoint is used.\n * @returns The unsigned object URL.\n */\nexport function s3ObjectHttpsUrl(\n location: S3ObjectLocation,\n config: Partial<S3EndpointConfig> = {},\n): string {\n if (location.bucket && !isSafeBucketName(location.bucket)) {\n throw new Error(`Invalid S3 bucket name: ${location.bucket}`);\n }\n const key = encodeS3Key(location.key);\n const endpoint = config.endpoint?.trim().replace(/\\/+$/, \"\");\n // No bucket addresses the service itself (ListBuckets).\n if (!location.bucket) {\n if (endpoint) {\n const base = new URL(endpoint.includes(\"://\") ? endpoint : `https://${endpoint}`);\n return `${base.protocol}//${base.host}${endpointPathPrefix(base)}/`;\n }\n const serviceRegion = config.region || location.region;\n return serviceRegion && serviceRegion !== \"us-east-1\"\n ? `https://s3.${serviceRegion}.amazonaws.com/`\n : \"https://s3.amazonaws.com/\";\n }\n if (endpoint) {\n const base = new URL(endpoint.includes(\"://\") ? endpoint : `https://${endpoint}`);\n // An S3-compatible gateway may sit under a path (`https://host/s3`).\n const prefix = endpointPathPrefix(base);\n return config.pathStyle\n ? `${base.protocol}//${base.host}${prefix}/${encodeS3Key(location.bucket)}/${key}`\n : `${base.protocol}//${location.bucket}.${base.host}${prefix}/${key}`;\n }\n const region = config.region || location.region;\n const host = region && region !== \"us-east-1\" ? `s3.${region}.amazonaws.com` : \"s3.amazonaws.com\";\n if (config.pathStyle || location.bucket.includes(\".\")) {\n return `https://${host}/${encodeS3Key(location.bucket)}/${key}`;\n }\n return `https://${location.bucket}.${host}/${key}`;\n}\n\n// ---------------------------------------------------------------------------\n// SigV4 query-string signing\n// ---------------------------------------------------------------------------\n\nconst encoder = new TextEncoder();\n\nfunction toHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer), (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nasync function sha256Hex(value: string): Promise<string> {\n return toHex(await crypto.subtle.digest(\"SHA-256\", encoder.encode(value)));\n}\n\nasync function hmac(key: ArrayBuffer | Uint8Array, value: string): Promise<ArrayBuffer> {\n const cryptoKey = await crypto.subtle.importKey(\n \"raw\",\n key as BufferSource,\n { name: \"HMAC\", hash: \"SHA-256\" },\n false,\n [\"sign\"],\n );\n return crypto.subtle.sign(\"HMAC\", cryptoKey, encoder.encode(value));\n}\n\n/** RFC 3986 encoding, as SigV4 requires for query names and values. */\nfunction encodeRfc3986(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,\n );\n}\n\nfunction amzDate(date: Date): { stamp: string; day: string } {\n const stamp = date\n .toISOString()\n .replace(/[-:]/g, \"\")\n .replace(/\\.\\d{3}/, \"\");\n return { stamp, day: stamp.slice(0, 8) };\n}\n\nexport interface PresignS3Options {\n /** The unsigned HTTPS object URL (see {@link s3ObjectHttpsUrl}). */\n url: string;\n region: string;\n credentials: S3Credentials;\n /** Lifetime in seconds, clamped to 1…{@link S3_MAX_PRESIGN_SECONDS}. */\n expiresIn: number;\n /** HTTP method the URL authorizes. Defaults to GET. */\n method?: string;\n /** Signing time; defaults to now. Exposed for tests. */\n now?: Date;\n}\n\n/**\n * Presigns an S3 request with AWS Signature Version 4 in the query string.\n * Runs on WebCrypto, so it works in the browser, the Tauri webview, and Node.\n *\n * The URL authorizes exactly one method: a GET URL cannot answer a HEAD, so\n * readers that probe with HEAD need a GET range request instead.\n *\n * @param options The object URL, region, credentials, and lifetime.\n * @returns The presigned URL.\n */\nexport async function presignS3Url(options: PresignS3Options): Promise<string> {\n const { credentials, region } = options;\n const method = (options.method ?? \"GET\").toUpperCase();\n const url = new URL(options.url);\n const { stamp, day } = amzDate(options.now ?? new Date());\n const scope = `${day}/${region}/s3/aws4_request`;\n const expiresIn = Math.max(1, Math.min(S3_MAX_PRESIGN_SECONDS, Math.floor(options.expiresIn)));\n\n const params: Array<[string, string]> = [];\n url.searchParams.forEach((value, name) => {\n if (!name.toLowerCase().startsWith(\"x-amz-\")) params.push([name, value]);\n });\n params.push(\n [\"X-Amz-Algorithm\", \"AWS4-HMAC-SHA256\"],\n [\"X-Amz-Credential\", `${credentials.accessKeyId}/${scope}`],\n [\"X-Amz-Date\", stamp],\n [\"X-Amz-Expires\", String(expiresIn)],\n [\"X-Amz-SignedHeaders\", \"host\"],\n );\n if (credentials.sessionToken) params.push([\"X-Amz-Security-Token\", credentials.sessionToken]);\n const canonicalQuery = params\n .map(([name, value]) => [encodeRfc3986(name), encodeRfc3986(value)] as const)\n .sort(([a, av], [b, bv]) => (a < b ? -1 : a > b ? 1 : av < bv ? -1 : av > bv ? 1 : 0))\n .map(([name, value]) => `${name}=${value}`)\n .join(\"&\");\n\n // The URL constructor already percent-encoded the path; S3 signs it as sent.\n const canonicalPath = url.pathname || \"/\";\n const canonicalRequest = [\n method,\n canonicalPath,\n canonicalQuery,\n `host:${url.host}\\n`,\n \"host\",\n \"UNSIGNED-PAYLOAD\",\n ].join(\"\\n\");\n const stringToSign = [\"AWS4-HMAC-SHA256\", stamp, scope, await sha256Hex(canonicalRequest)].join(\n \"\\n\",\n );\n\n const dateKey = await hmac(encoder.encode(`AWS4${credentials.secretAccessKey}`), day);\n const regionKey = await hmac(dateKey, region);\n const serviceKey = await hmac(regionKey, \"s3\");\n const signingKey = await hmac(serviceKey, \"aws4_request\");\n const signature = toHex(await hmac(signingKey, stringToSign));\n\n return `${url.origin}${canonicalPath}?${canonicalQuery}&X-Amz-Signature=${signature}`;\n}\n\n// ---------------------------------------------------------------------------\n// Signer registry\n// ---------------------------------------------------------------------------\n\n/** One signed S3 GET: an object read, or a bucket-level call such as ListObjectsV2. */\nexport interface S3PresignRequest {\n bucket: string;\n /** Object key; empty for a bucket-level request. */\n key: string;\n /** Extra query parameters to sign (e.g. `list-type=2`). */\n query?: Record<string, string>;\n /** Region an AWS URL named, used ahead of the connection's default. */\n region?: string;\n /**\n * Sign with this connection instead of the one matched by bucket name (the\n * S3 browser listing a chosen connection's buckets, where `bucket` is empty).\n */\n connectionId?: string;\n}\n\n/** A configured connection, as the S3 browser lists it. No secrets. */\nexport interface S3ConnectionSummary {\n id: string;\n name: string;\n /** Bucket patterns the connection covers; empty means every bucket. */\n buckets: string[];\n}\n\n/**\n * Presigns S3 requests with the app's configured credentials. `presign`\n * resolves to null when no configured connection covers the bucket, in which\n * case the object is read anonymously.\n */\nexport interface S3UrlSigner {\n /** Whether a configured connection covers `bucket`. Must be synchronous. */\n covers(bucket: string): boolean;\n /** Presigns a GET; rejects when the connection's credentials cannot be resolved. */\n presign(request: S3PresignRequest, signal?: AbortSignal): Promise<S3SignedUrl | null>;\n /** The configured connections, for pickers. */\n connections(): S3ConnectionSummary[];\n /**\n * Fetches a presigned bucket-level URL (listing). The desktop app routes it\n * through its native HTTP client, which needs no bucket CORS rule.\n */\n fetchText?(url: string, signal?: AbortSignal): Promise<{ status: number; body: string }>;\n /**\n * Drops the cached credentials and signed URLs of the connection that\n * serves `target`, so the next `presign` resolves them afresh. Called when S3\n * refuses a request as expired.\n */\n invalidateCredentials?(target: { bucket?: string; connectionId?: string }): void;\n /** The persisted location (`s3://bucket/prefix/`) the S3 browser opens at, or \"\". */\n defaultLocation?(): string;\n /** Persists the S3 browser's default location; \"\" clears it. */\n setDefaultLocation?(location: string): void;\n}\n\nlet activeSigner: S3UrlSigner | null = null;\n/** Presigned URL → the unsigned URL it was minted for. Bounded. */\nconst sourceBySignedHref = new Map<string, string>();\nconst SIGNED_SOURCE_LIMIT = 500;\n\n/** Installs (or with null, removes) the app's S3 signer. */\nexport function registerS3UrlSigner(signer: S3UrlSigner | null): void {\n activeSigner = signer;\n}\n\n/**\n * Whether reading `url` needs the S3 signer: it names an S3 object in a bucket\n * a configured connection covers.\n */\nexport function isCredentialedS3Url(url: string): boolean {\n if (!activeSigner) return false;\n const location = parseS3Url(url);\n return Boolean(location && activeSigner.covers(location.bucket));\n}\n\n/** Records that `signedHref` reads `sourceUrl`, so a layer can keep the latter. */\nfunction rememberSignedSource(signedHref: string, sourceUrl: string): void {\n // Least recently used goes first: deleting before setting moves a URL that\n // is signed (handed out) again to the back of the eviction order.\n sourceBySignedHref.delete(signedHref);\n if (sourceBySignedHref.size >= SIGNED_SOURCE_LIMIT) {\n const oldest = sourceBySignedHref.keys().next().value;\n if (oldest !== undefined) sourceBySignedHref.delete(oldest);\n }\n sourceBySignedHref.set(signedHref, sourceUrl);\n}\n\n/** Removes the SigV4 query parameters (credential, token, signature) from a URL. */\nfunction stripPresignParameters(url: string): string {\n try {\n const parsed = new URL(url);\n for (const name of [...parsed.searchParams.keys()]) {\n if (name.toLowerCase().startsWith(\"x-amz-\")) parsed.searchParams.delete(name);\n }\n return parsed.href;\n } catch {\n return url;\n }\n}\n\n/**\n * The unsigned URL a presigned one was minted for by\n * {@link resolveReadableUrl}, or `url` itself. Layer sync runs every URL a\n * control reports through this, so the store (and a saved project) keeps the\n * `s3://` source rather than an expiring signature.\n */\nexport function unsignedSourceUrl(url: string): string;\nexport function unsignedSourceUrl(url: string | undefined): string | undefined;\nexport function unsignedSourceUrl(url: string | undefined): string | undefined {\n if (url === undefined) return undefined;\n const known = sourceBySignedHref.get(url);\n if (known !== undefined) return known;\n // A presigned URL whose mapping was evicted must still never reach a saved\n // project: fall back to the object it signs, without the signature.\n if (!/[?&]x-amz-signature=/i.test(url)) return url;\n const location = parseS3Url(url);\n // An `s3://` URI cannot carry a key with `?` or `#` (they would be read as a\n // query or fragment), so such keys keep the object URL form.\n return location && !/[?#]/.test(location.key)\n ? formatS3Uri(location)\n : stripPresignParameters(url);\n}\n\n/**\n * Resolves a data URL into one a plain HTTP reader can fetch:\n *\n * - an S3 object in a bucket a configured connection covers → a presigned URL;\n * - any other `s3://` URI → the public HTTPS object URL (anonymous read);\n * - anything else → unchanged.\n *\n * @param url The URL a layer or query names.\n * @param signal Aborts a pending credential lookup.\n * @returns The URL to hand the reader.\n * @throws When the bucket is covered but its credentials cannot be resolved.\n */\nexport async function resolveReadableUrl(url: string, signal?: AbortSignal): Promise<string> {\n const location = parseS3Url(url);\n if (!location) return url;\n if (activeSigner?.covers(location.bucket)) {\n const signed = await activeSigner.presign(\n { bucket: location.bucket, key: location.key, region: location.region },\n signal,\n );\n if (signed) {\n rememberSignedSource(signed.href, url);\n return signed.href;\n }\n }\n return isS3Uri(url) ? s3ObjectHttpsUrl(location) : url;\n}\n\n/** The installed signer, for the S3 browser. */\nexport function getS3UrlSigner(): S3UrlSigner | null {\n return activeSigner;\n}\n\n// ---------------------------------------------------------------------------\n// ListObjectsV2\n// ---------------------------------------------------------------------------\n\n/** One page of a ListObjectsV2 response. */\nexport interface S3ListPage {\n /** Common prefixes (\"folders\") under the requested prefix. */\n prefixes: string[];\n objects: Array<{ key: string; size: number; lastModified?: string }>;\n /** Present when more results follow. */\n nextContinuationToken?: string;\n}\n\nfunction decodeXmlText(value: string): string {\n return value\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&apos;/g, \"'\")\n .replace(/&#(\\d+);/g, (_, code: string) => String.fromCodePoint(Number(code)))\n .replace(/&#x([0-9a-f]+);/gi, (_, code: string) => String.fromCodePoint(parseInt(code, 16)))\n .replace(/&amp;/g, \"&\");\n}\n\nfunction xmlTag(block: string, tag: string): string | undefined {\n const match = block.match(new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`));\n return match ? decodeXmlText(match[1]) : undefined;\n}\n\nfunction xmlBlocks(xml: string, tag: string): string[] {\n return Array.from(xml.matchAll(new RegExp(`<${tag}>([\\\\s\\\\S]*?)</${tag}>`, \"g\")), (m) => m[1]);\n}\n\n/**\n * Parses a ListObjectsV2 XML body. S3's listing XML is flat and predictable,\n * so this reads it with patterns rather than an XML DOM, which keeps it usable\n * in workers and Node tests.\n */\nexport function parseS3ListObjects(xml: string): S3ListPage {\n const prefixes = xmlBlocks(xml, \"CommonPrefixes\")\n .map((block) => xmlTag(block, \"Prefix\"))\n .filter((value): value is string => Boolean(value));\n const objects = xmlBlocks(xml, \"Contents\").flatMap((block) => {\n const key = xmlTag(block, \"Key\");\n if (key === undefined) return [];\n const lastModified = xmlTag(block, \"LastModified\");\n return [\n { key, size: Number(xmlTag(block, \"Size\") ?? 0), ...(lastModified ? { lastModified } : {}) },\n ];\n });\n const truncated = xmlTag(xml, \"IsTruncated\") === \"true\";\n const token = xmlTag(xml, \"NextContinuationToken\");\n return { prefixes, objects, ...(truncated && token ? { nextContinuationToken: token } : {}) };\n}\n\n/** The error code and message of an S3 XML error body, when it is one. */\nexport function parseS3Error(\n xml: string,\n): { code: string; message: string; region?: string } | null {\n const block = xmlBlocks(xml, \"Error\")[0];\n if (block === undefined) return null;\n // A wrong-region signature names the region; an unsigned request to the\n // wrong endpoint (301 PermanentRedirect) names the right endpoint instead.\n const endpoint = xmlTag(block, \"Endpoint\");\n const region =\n xmlTag(block, \"Region\") ?? (endpoint ? parseS3Url(`https://${endpoint}/`)?.region : undefined);\n return {\n code: xmlTag(block, \"Code\") ?? \"Error\",\n message: xmlTag(block, \"Message\") ?? \"\",\n ...(region ? { region } : {}),\n };\n}\n\n/** Bucket names from a ListBuckets (`GET /` on the service) XML body. */\nexport function parseS3BucketList(xml: string): string[] {\n return xmlBlocks(xml, \"Bucket\")\n .map((block) => xmlTag(block, \"Name\"))\n .filter((name): name is string => Boolean(name));\n}\n\n// ---------------------------------------------------------------------------\n// CORS diagnosis\n// ---------------------------------------------------------------------------\n\n/**\n * How browsers word a request they blocked or could not complete, plus the\n * readers that already guess at CORS themselves (maplibre-gl-lidar's message\n * tells the user to use a file picker, which is wrong for a bucket).\n */\nconst NETWORK_FAILURE =\n /failed to fetch|networkerror|load failed|network request failed|err_failed|cross-origin|\\bcors\\b/i;\n\nfunction errorText(error: unknown): string {\n const parts: string[] = [];\n let current: unknown = error;\n for (let depth = 0; current && depth < 4; depth += 1) {\n parts.push(current instanceof Error ? `${current.name}: ${current.message}` : String(current));\n current = current instanceof Error ? (current as Error & { cause?: unknown }).cause : null;\n }\n return parts.join(\" | \");\n}\n\n/** A read the bucket's CORS configuration blocked. */\nexport class S3CorsError extends Error {\n readonly bucket: string;\n readonly origin: string;\n\n constructor(message: string, bucket: string, origin: string, cause: unknown) {\n super(message, { cause });\n this.name = \"S3CorsError\";\n this.bucket = bucket;\n this.origin = origin;\n }\n}\n\n/** Translates `key`, falling back to `defaultValue` (an app API's `translate`). */\nexport type S3ErrorTranslate = (\n key: string,\n defaultValue: string,\n params?: Record<string, string | number>,\n) => string;\n\n/**\n * Explains why reading an S3 object failed when the browser only said\n * \"Failed to fetch\". A browser reports a request the bucket's CORS rules\n * blocked exactly like a network outage, so this repeats the request twice:\n * normally, where success means the failure was transient; then in `no-cors`\n * mode, which CORS cannot block. Only when the first fails and the second gets\n * an answer is the bucket's CORS configuration the cause.\n *\n * @param url The `s3://` URI or object URL that was being read.\n * @param error What the reader threw.\n * @param translate Turns the message into the UI language.\n * @returns An {@link S3CorsError} naming the bucket and the origin to allow,\n * or `error` unchanged when it is not a CORS refusal of an S3 read.\n */\nexport async function explainS3ReadError(\n url: string,\n error: unknown,\n translate?: S3ErrorTranslate,\n): Promise<unknown> {\n const location = parseS3Url(url);\n if (!location || error instanceof S3CorsError) return error;\n if (!NETWORK_FAILURE.test(errorText(error))) return error;\n if (typeof fetch !== \"function\" || typeof window === \"undefined\") return error;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), 10_000);\n try {\n const href = await resolveReadableUrl(url);\n const probe = { signal: controller.signal, cache: \"no-store\" as const };\n // A normal (CORS) read first: if that works now, the failure was transient\n // and CORS is not the cause. The body is never needed, so each request is\n // cut off once its headers arrive.\n const corsAllowed = await fetch(href, probe).then(\n () => true,\n () => false,\n );\n if (corsAllowed) return error;\n // Blocked under CORS, yet S3 answers a no-cors request, which CORS cannot\n // block: the bucket's CORS configuration is what refuses this origin.\n await fetch(href, { ...probe, mode: \"no-cors\" });\n } catch {\n // Unreachable under no-cors too: a real network failure, not CORS.\n return error;\n } finally {\n controller.abort();\n clearTimeout(timer);\n }\n const origin = window.location.origin;\n const fallback =\n `The bucket \"${location.bucket}\" does not allow reads from ${origin}: its CORS configuration ` +\n \"is missing or does not list this origin. Add a CORS rule that allows GET and HEAD from this \" +\n \"origin and exposes Content-Length, Content-Range, and ETag (see Settings > Cloud Storage).\";\n const message = translate\n ? translate(\"s3Browser.corsError\", fallback, { bucket: location.bucket, origin })\n : fallback;\n return new S3CorsError(message, location.bucket, origin, error);\n}\n","import type { StoryMap } from \"./types\";\n\nconst SAMPLE_ASSET_BASE = \"https://opengeos.org/maplibre-gl-storymaps/assets\";\n\n/**\n * Build a ready-to-play sample story map.\n *\n * Mirrors the chapters of the `opengeos/maplibre-gl-storymaps` demo (five world\n * cities) so users can explore the feature instantly without authoring content.\n * A fresh object is returned on each call so callers can mutate it freely.\n *\n * @returns A populated {@link StoryMap} with five sample chapters.\n */\nexport function createSampleStoryMap(): StoryMap {\n return {\n title: \"A Tour of Five Cities\",\n subtitle: \"A scrollytelling map experience built with GeoLibre\",\n byline: \"By GeoLibre\",\n footer:\n 'Source: Wikipedia. Built with <a href=\"https://github.com/opengeos/GeoLibre\" target=\"_blank\">GeoLibre</a>, inspired by <a href=\"https://github.com/opengeos/maplibre-gl-storymaps\" target=\"_blank\">MapLibre Storytelling</a>.',\n theme: \"dark\",\n showMarkers: true,\n markerColor: \"#3fb1ce\",\n inset: true,\n insetPosition: \"bottom-left\",\n hideChapterNav: false,\n startSlide: \"none\",\n endSlide: \"none\",\n chapters: [\n {\n id: \"sample-san-francisco\",\n title: \"San Francisco, California\",\n image: `${SAMPLE_ASSET_BASE}/san-francisco.jpg`,\n description:\n \"San Francisco, a hilly city on the tip of a peninsula surrounded by the Pacific Ocean and San Francisco Bay, is known for its year-round fog, iconic Golden Gate Bridge, cable cars and colorful Victorian houses. <br><br>The city is also known for its vibrant tech industry, diverse neighborhoods, and rich cultural scene.\",\n alignment: \"left\",\n hidden: false,\n location: {\n center: [-122.4194, 37.7749],\n zoom: 11,\n pitch: 45,\n bearing: 0,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-new-york\",\n title: \"New York City, New York\",\n image: `${SAMPLE_ASSET_BASE}/new-york.jpg`,\n description:\n \"New York City comprises 5 boroughs sitting where the Hudson River meets the Atlantic Ocean. At its core is Manhattan, a densely populated borough that's among the world's major commercial, financial and cultural centers. <br><br>Its iconic sites include skyscrapers such as the Empire State Building and sprawling Central Park.\",\n alignment: \"right\",\n hidden: false,\n location: {\n center: [-74.006, 40.7128],\n zoom: 11,\n pitch: 60,\n bearing: -43.2,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: true,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-tokyo\",\n title: \"Tokyo, Japan\",\n image: `${SAMPLE_ASSET_BASE}/tokyo.jpg`,\n description:\n \"Tokyo, Japan's busy capital, mixes the ultramodern and the traditional, from neon-lit skyscrapers to historic temples. The opulent Meiji Shinto Shrine is known for its towering gate and surrounding woods. <br><br>The Imperial Palace sits amid large public gardens. The city's many museums offer exhibits ranging from classical art to a reconstructed kabuki theater.\",\n alignment: \"left\",\n hidden: false,\n location: {\n center: [139.6917, 35.6895],\n zoom: 10,\n pitch: 30,\n bearing: 20,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-sydney\",\n title: \"Sydney, Australia\",\n image: `${SAMPLE_ASSET_BASE}/sydney.jpg`,\n description:\n \"Sydney, capital of New South Wales and one of Australia's largest cities, is best known for its harbourfront Sydney Opera House, with a distinctive sail-like design. <br><br>Massive Darling Harbour and the smaller Circular Quay port are hubs of waterside life, with the arched Harbour Bridge and esteemed Royal Botanic Garden nearby.\",\n alignment: \"right\",\n hidden: false,\n location: {\n center: [151.2093, -33.8688],\n zoom: 11,\n pitch: 45,\n bearing: 0,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n {\n id: \"sample-cape-town\",\n title: \"Cape Town, South Africa\",\n image: `${SAMPLE_ASSET_BASE}/cape-town.jpg`,\n description:\n \"Cape Town is a port city on South Africa's southwest coast, on a peninsula beneath the imposing Table Mountain. Slowly rotating cable cars climb to the mountain's flat top, from which there are sweeping views of the city, the busy harbor and boats headed for Robben Island, the infamous prison that once held Nelson Mandela. <br><br>You can add as many chapters as you need to tell your story.\",\n alignment: \"left\",\n hidden: false,\n location: {\n center: [18.4241, -33.9249],\n zoom: 10,\n pitch: 50,\n bearing: 30,\n },\n mapAnimation: \"flyTo\",\n rotateAnimation: false,\n onChapterEnter: [],\n onChapterExit: [],\n },\n ],\n };\n}\n","/**\n * Helpers for following a story-map presentation's layer fades outside the\n * MapLibre paint model.\n *\n * Story playback fades layers by writing MapLibre paint properties directly\n * (see `MapController.setStoryLayerOpacity`) and records the applied values in\n * `ui.storymapLayerOpacity`. Anything that renders a layer some other way, such\n * as deck.gl feature diagrams and 3D Z-value geometry, or that lists layers\n * for the reader, such as the on-map Legend, reads that record through these\n * helpers so a faded-out layer disappears everywhere at once.\n */\nimport type { GeoLibreLayer, MapViewState, StoryChapterLocation } from \"./types\";\n\n/** The clamped opacity recorded for a layer, or undefined when untouched. */\nfunction recordedStoryOpacity(\n opacities: Record<string, number> | undefined,\n layerId: string,\n): number | undefined {\n const value = opacities?.[layerId];\n if (typeof value !== \"number\" || !Number.isFinite(value)) return undefined;\n return Math.min(1, Math.max(0, value));\n}\n\n/**\n * The opacity a story presentation has applied to a layer.\n *\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n * @param layerId Store layer id.\n * @returns The recorded opacity in `[0, 1]`, or `1` when the presentation has\n * not touched the layer (or nothing is presenting).\n */\nexport function storyLayerOpacityFactor(\n opacities: Record<string, number> | undefined,\n layerId: string,\n): number {\n return recordedStoryOpacity(opacities, layerId) ?? 1;\n}\n\n/**\n * A layer whose opacity reflects the story presentation's fade.\n *\n * A chapter's opacity *replaces* the layer's own opacity rather than scaling\n * it, matching `MapController.setStoryLayerOpacity`, which writes the chapter\n * value as the absolute paint opacity; otherwise a layer set to 0.7 in the\n * Layers panel and faded to 0.4 by a chapter would draw its diagrams at 0.28\n * while its MapLibre layers draw at 0.4.\n *\n * Returns the same object when the presentation has not touched the layer (or\n * recorded the opacity it already has), so identity-keyed caches downstream\n * keep hitting.\n *\n * @param layer Store layer.\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n */\nexport function applyStoryLayerOpacity(\n layer: GeoLibreLayer,\n opacities: Record<string, number> | undefined,\n): GeoLibreLayer {\n const opacity = recordedStoryOpacity(opacities, layer.id);\n if (opacity === undefined || opacity === layer.opacity) return layer;\n return { ...layer, opacity };\n}\n\n/**\n * Whether a story presentation has faded a layer fully out.\n *\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n * @param layerId Store layer id.\n */\nexport function isStoryHiddenLayer(\n opacities: Record<string, number> | undefined,\n layerId: string,\n): boolean {\n return storyLayerOpacityFactor(opacities, layerId) === 0;\n}\n\n/**\n * The layers a reader can currently see during a story presentation: the\n * input minus those the active chapter has faded fully out. Outside a\n * presentation the input is returned as-is (same array identity).\n *\n * @param layers Store layers in panel order.\n * @param presenting The store's `ui.storymapPresenting` flag.\n * @param opacities The store's `ui.storymapLayerOpacity` record.\n */\nexport function storyVisibleLayers(\n layers: GeoLibreLayer[],\n presenting: boolean,\n opacities: Record<string, number> | undefined,\n): GeoLibreLayer[] {\n if (!presenting) return layers;\n if (!layers.some((layer) => isStoryHiddenLayer(opacities, layer.id))) return layers;\n return layers.filter((layer) => !isStoryHiddenLayer(opacities, layer.id));\n}\n\n/**\n * Resolve a story chapter location to a full camera view.\n *\n * A location's `pitch` and `bearing` are optional in memory (a project file\n * fills them with 0 on load). An absent value keeps the camera's current one,\n * the same as MapLibre's camera methods do for an omitted option, so engines\n * whose camera API needs every field (Cesium, `MapEngine.applyView`) behave\n * like the MapLibre map.\n *\n * @param location The chapter's camera target.\n * @param current The camera's current view, used for an absent pitch/bearing.\n * @returns The location with `pitch` and `bearing` filled in.\n */\nexport function storyLocationView(\n location: StoryChapterLocation,\n current: Pick<MapViewState, \"pitch\" | \"bearing\">,\n): MapViewState {\n return {\n center: location.center,\n zoom: location.zoom,\n pitch: location.pitch ?? current.pitch,\n bearing: location.bearing ?? current.bearing,\n };\n}\n","import { normalizeStoryMap } from \"./project\";\nimport { DEFAULT_STORY_MAP, type StoryMap } from \"./types\";\n\n/**\n * Serialize a story map to pretty-printed JSON for export.\n *\n * @param storymap The story map to serialize.\n * @returns A JSON string (settings and chapters).\n */\nexport function serializeStoryMapJson(storymap: StoryMap): string {\n return JSON.stringify(storymap, null, 2);\n}\n\n/**\n * Parse a story map from JSON authored outside GeoLibre.\n *\n * Accepts either a bare story map object or a full `.geolibre.json` project with\n * a `storymap` field, then validates it through the same normalizer used when\n * loading projects.\n *\n * @param text JSON text to import.\n * @returns A normalized story map.\n * @throws If the JSON is invalid or contains no usable chapters.\n */\nexport function parseStoryMapJson(text: string): StoryMap {\n const data = JSON.parse(text) as unknown;\n const candidate =\n data && typeof data === \"object\" && \"storymap\" in (data as object)\n ? (data as { storymap: unknown }).storymap\n : data;\n const normalized = normalizeStoryMap(candidate);\n if (!normalized) {\n throw new Error(\"The imported story map has no chapters.\");\n }\n return normalized;\n}\n\n// Flat, spreadsheet-friendly chapter columns. Layer opacity effects are stored\n// as JSON in their own columns so the CSV round-trips, but hand-authors can\n// simply leave them blank.\nconst CSV_HEADERS = [\n \"id\",\n \"title\",\n \"description\",\n \"image\",\n \"alignment\",\n \"hidden\",\n \"lng\",\n \"lat\",\n \"zoom\",\n \"pitch\",\n \"bearing\",\n \"mapAnimation\",\n \"rotateAnimation\",\n \"onChapterEnter\",\n \"onChapterExit\",\n \"durationSeconds\",\n] as const;\n\n/**\n * Serialize a story map's chapters to CSV (one row per chapter).\n *\n * Story-level settings are not represented in CSV; they are kept in the project\n * and preserved on CSV import.\n *\n * @param storymap The story map whose chapters to serialize.\n * @returns A CSV string with a header row.\n */\nexport function serializeStoryMapCsv(storymap: StoryMap): string {\n const rows = [CSV_HEADERS.join(\",\")];\n for (const c of storymap.chapters) {\n rows.push(\n [\n c.id,\n c.title,\n c.description,\n c.image ?? \"\",\n c.alignment,\n String(c.hidden),\n String(c.location.center[0]),\n String(c.location.center[1]),\n String(c.location.zoom),\n // An absent pitch/bearing is an empty cell, not the string \"undefined\".\n String(c.location.pitch ?? \"\"),\n String(c.location.bearing ?? \"\"),\n c.mapAnimation,\n String(c.rotateAnimation),\n JSON.stringify(c.onChapterEnter),\n JSON.stringify(c.onChapterExit),\n String(c.durationSeconds ?? \"\"),\n ]\n .map(csvEscape)\n .join(\",\"),\n );\n }\n return rows.join(\"\\n\") + \"\\n\";\n}\n\n/**\n * Parse chapters from a CSV authored outside GeoLibre.\n *\n * Columns are matched by header name (case-insensitive) so column order is\n * flexible; `lng`/`lat` set the chapter center. Story-level settings come from\n * `base` (the current story) since CSV only carries chapters; missing ids are\n * generated.\n *\n * @param text CSV text to import.\n * @param base Existing story map whose settings should be preserved.\n * @returns A normalized story map.\n * @throws If the CSV has no data rows or no valid chapters.\n */\nexport function parseStoryMapCsv(text: string, base?: StoryMap | null): StoryMap {\n const rows = parseCsvRows(text);\n if (rows.length < 2) {\n throw new Error(\"The CSV has no chapter rows.\");\n }\n const header = rows[0].map((h) => h.trim().toLowerCase());\n const col = (name: string) => header.indexOf(name.toLowerCase());\n\n const chapters = rows\n .slice(1)\n .filter((row) => row.some((cell) => cell.trim() !== \"\"))\n .map((row, index) => {\n const get = (name: string) => {\n const j = col(name);\n return j >= 0 ? (row[j] ?? \"\") : \"\";\n };\n const num = (name: string, fallback: number) => {\n const value = Number(get(name));\n return Number.isFinite(value) ? value : fallback;\n };\n const bool = (name: string) => /^(true|1|yes)$/i.test(get(name).trim());\n const jsonArray = (name: string) => {\n try {\n const parsed = JSON.parse(get(name) || \"[]\");\n return Array.isArray(parsed) ? parsed : [];\n } catch {\n return [];\n }\n };\n return {\n id: get(\"id\").trim() || `chapter-${index + 1}`,\n title: get(\"title\"),\n description: get(\"description\"),\n image: get(\"image\").trim() || undefined,\n alignment: get(\"alignment\").trim() || \"left\",\n hidden: bool(\"hidden\"),\n location: {\n center: [num(\"lng\", 0), num(\"lat\", 0)],\n zoom: num(\"zoom\", 2),\n pitch: num(\"pitch\", 0),\n bearing: num(\"bearing\", 0),\n },\n mapAnimation: get(\"mapanimation\").trim() || \"flyTo\",\n rotateAnimation: bool(\"rotateanimation\"),\n // An absent/blank cell means the default, not Number(\"\") === 0.\n durationSeconds: get(\"durationseconds\").trim() ? Number(get(\"durationseconds\")) : undefined,\n onChapterEnter: jsonArray(\"onchapterenter\"),\n onChapterExit: jsonArray(\"onchapterexit\"),\n };\n });\n\n const normalized = normalizeStoryMap({\n ...(base ?? DEFAULT_STORY_MAP),\n chapters,\n });\n if (!normalized) {\n throw new Error(\"The CSV contained no valid chapters.\");\n }\n return normalized;\n}\n\n/** Quote a CSV field when it contains a comma, quote, or newline. */\nfunction csvEscape(value: string): string {\n if (/[\",\\n\\r]/.test(value)) {\n return `\"${value.replace(/\"/g, '\"\"')}\"`;\n }\n return value;\n}\n\n/** Parse CSV text into rows of fields, honoring quotes and escaped quotes. */\nfunction parseCsvRows(text: string): string[][] {\n const rows: string[][] = [];\n let row: string[] = [];\n let field = \"\";\n let inQuotes = false;\n // Normalize line endings so \\r\\n and \\r behave like \\n.\n const input = text.replace(/\\r\\n?/g, \"\\n\");\n\n for (let i = 0; i < input.length; i++) {\n const ch = input[i];\n if (inQuotes) {\n if (ch === '\"') {\n if (input[i + 1] === '\"') {\n field += '\"';\n i++;\n } else {\n inQuotes = false;\n }\n } else {\n field += ch;\n }\n } else if (ch === '\"') {\n inQuotes = true;\n } else if (ch === \",\") {\n row.push(field);\n field = \"\";\n } else if (ch === \"\\n\") {\n row.push(field);\n rows.push(row);\n row = [];\n field = \"\";\n } else {\n field += ch;\n }\n }\n // Flush the final field/row unless the input ended with a trailing newline.\n if (field !== \"\" || row.length > 0) {\n row.push(field);\n rows.push(row);\n }\n return rows;\n}\n","/**\n * Narrow subscriptions to the store's `layers` array.\n *\n * `updateLayer` (and every other layer action) replaces `layers` with a new\n * array on each change, so a component that subscribes with\n * `useAppStore((s) => s.layers)` re-renders on every opacity tweak, rename or\n * style edit of *any* layer. The hooks here select only what a component reads\n * and compare it so unrelated layer changes do not re-render it:\n *\n * - {@link useLayer} — one layer record, by id. Layer actions copy only the\n * layer they change and keep every other record's identity, so this is\n * stable while other layers change.\n * - {@link useLayerIds} — the ordered id list, shallow-compared.\n * - {@link useLayerSummaries} — `{ id, name, type, visible, groupId }` per\n * layer, for components that only list layers.\n * - {@link useLayersWhen} — the full array, but only while `active` (a dialog\n * is open, a search has a query); a constant empty array otherwise.\n *\n * The plain `select*` functions are exported so the selection semantics can be\n * tested without React.\n */\nimport { useShallow } from \"zustand/react/shallow\";\nimport { type AppState, useAppStore } from \"./store\";\nimport type { GeoLibreLayer, LayerType } from \"./types\";\n\n/**\n * The fields a layer list needs: enough to draw a row, a `<select>` option or\n * a visibility checkbox. It deliberately leaves out opacity, style, source,\n * metadata and feature data, which change far more often.\n */\nexport interface LayerSummary {\n /** The layer id. */\n readonly id: string;\n /** The display name. */\n readonly name: string;\n /** The layer kind. */\n readonly type: LayerType;\n /** Whether the layer is shown on the primary map. */\n readonly visible: boolean;\n /** The layer panel group the layer belongs to, if any. */\n readonly groupId: string | undefined;\n}\n\n/**\n * The array {@link useLayersWhen} returns while inactive. Frozen, and shared so\n * the inactive selection never changes identity.\n */\nexport const NO_LAYERS: GeoLibreLayer[] = Object.freeze([]) as unknown as GeoLibreLayer[];\n\n/**\n * Select one layer record by id.\n *\n * @param state - The app state.\n * @param id - The layer id; `null`/`undefined` selects nothing.\n * @returns The layer record, or `undefined` when there is no such layer.\n */\nexport function selectLayerById(\n state: Pick<AppState, \"layers\">,\n id: string | null | undefined,\n): GeoLibreLayer | undefined {\n if (!id) return undefined;\n return state.layers.find((layer) => layer.id === id);\n}\n\n/**\n * Select the layer ids in store order (bottom to top).\n *\n * Returns a new array on every call; pair it with a shallow comparison (as\n * {@link useLayerIds} does) so an unchanged id list keeps its identity.\n *\n * @param state - The app state.\n * @returns The layer ids.\n */\nexport function selectLayerIds(state: Pick<AppState, \"layers\">): string[] {\n return state.layers.map((layer) => layer.id);\n}\n\n// Summaries from the previous call, by layer id. Reusing an entry whose fields\n// are unchanged keeps each summary's identity across unrelated edits (an\n// opacity change replaces the layer object but not its summary), which is what\n// lets a shallow comparison of the summary array succeed. Rebuilt on every call\n// so removed layers drop out. Summaries are immutable values, so sharing them\n// between subscribers is safe.\n//\n// The cache is module-level on purpose: every caller reads the one app store,\n// and an entry is reused only after `sameSummary` re-checks it against the\n// current layer. Another subscriber, or a React render that is discarded,\n// can therefore only cause a cache miss (a new summary object), never a stale\n// summary. Tests that care about identity should not rely on a clean cache.\nlet summaryCache = new Map<string, LayerSummary>();\n\nfunction sameSummary(summary: LayerSummary, layer: GeoLibreLayer): boolean {\n return (\n summary.name === layer.name &&\n summary.type === layer.type &&\n summary.visible === layer.visible &&\n summary.groupId === layer.groupId\n );\n}\n\n/**\n * Select a {@link LayerSummary} per layer, in store order (bottom to top).\n *\n * Each summary keeps its identity while its own fields are unchanged, so the\n * returned array is shallow-equal to the previous one after an edit that\n * touches none of those fields (opacity, style, data, metadata).\n *\n * @param state - The app state.\n * @returns One summary per layer.\n */\nexport function selectLayerSummaries(state: Pick<AppState, \"layers\">): LayerSummary[] {\n const next = new Map<string, LayerSummary>();\n const summaries = state.layers.map((layer) => {\n const previous = summaryCache.get(layer.id);\n const summary =\n previous && sameSummary(previous, layer)\n ? previous\n : Object.freeze({\n id: layer.id,\n name: layer.name,\n type: layer.type,\n visible: layer.visible,\n groupId: layer.groupId,\n });\n next.set(layer.id, summary);\n return summary;\n });\n summaryCache = next;\n return summaries;\n}\n\n/**\n * Build a selector for the full `layers` array that is live only while\n * `active`.\n *\n * @param active - Whether the caller currently needs the layers.\n * @returns A selector returning `state.layers` while active, and the constant\n * {@link NO_LAYERS} otherwise.\n */\nexport function selectLayersWhen(\n active: boolean,\n): (state: Pick<AppState, \"layers\">) => GeoLibreLayer[] {\n return active ? (state) => state.layers : () => NO_LAYERS;\n}\n\n/**\n * Subscribe to one layer record.\n *\n * Re-renders only when that layer changes (or appears/disappears), not when\n * another layer is edited.\n *\n * @param id - The layer id; `null`/`undefined` subscribes to nothing.\n * @returns The layer record, or `undefined` when there is no such layer.\n */\nexport function useLayer(id: string | null | undefined): GeoLibreLayer | undefined {\n return useAppStore((state) => selectLayerById(state, id));\n}\n\n/**\n * Subscribe to the ordered list of layer ids.\n *\n * Re-renders only when a layer is added, removed or reordered.\n *\n * @returns The layer ids in store order (bottom to top).\n */\nexport function useLayerIds(): string[] {\n return useAppStore(useShallow(selectLayerIds));\n}\n\n/**\n * Subscribe to a {@link LayerSummary} per layer.\n *\n * Re-renders only when a layer is added, removed, reordered, renamed, shown or\n * hidden, retyped or regrouped; opacity, style, data and metadata edits do not\n * re-render the caller.\n *\n * @returns One summary per layer, in store order (bottom to top).\n */\nexport function useLayerSummaries(): LayerSummary[] {\n return useAppStore(useShallow(selectLayerSummaries));\n}\n\n/**\n * Subscribe to the full `layers` array only while `active`.\n *\n * For always-mounted surfaces (dialogs, floating panels) that need every layer\n * record while open but keep local state across closes and so cannot simply be\n * unmounted. While inactive the caller gets the constant {@link NO_LAYERS} and\n * layer edits do not re-render it.\n *\n * @param active - Whether the caller currently needs the layers (for example,\n * whether its dialog is open).\n * @returns `layers` while active, otherwise an empty, frozen array.\n */\nexport function useLayersWhen(active: boolean): GeoLibreLayer[] {\n return useAppStore(selectLayersWhen(active));\n}\n","/**\n * Platform detection shared across the workspace. Carries no React/MapLibre\n * dependency so callers stay unit-testable.\n */\n\n/**\n * Whether a \"Macintosh\" user agent is really an iPad.\n *\n * iPadOS 13+ requests desktop sites by default and reports a macOS user agent,\n * so the UA string alone cannot tell an iPad from a Mac. Multi-touch does: a\n * real Mac reports `maxTouchPoints` 0 or 1, an iPad reports more.\n *\n * This lives in `@geolibre/core` because two packages need it and the rule is\n * an Apple behaviour that could change: the desktop app's `isMobile` (which\n * hides sidecar-backed tools on mobile) and `@geolibre/plugins`'\n * `isEarthEngineAvailable` (which hides Earth Engine sign-in on the Apple App\n * Store builds). Keeping one copy means a future correction lands in both.\n *\n * @param userAgent - The user agent string to test.\n * @param maxTouchPoints - `navigator.maxTouchPoints`, or undefined where the\n * runtime does not provide it (Node exposes a `navigator` without it).\n * @returns True when the UA claims macOS but the device reports multi-touch.\n */\nexport function isIpadDesktopUserAgent(\n userAgent: string,\n maxTouchPoints: number | undefined,\n): boolean {\n return /Macintosh/.test(userAgent) && (maxTouchPoints ?? 0) > 1;\n}\n","import { getGoogleMapsApiKey } from \"./runtime-env\";\n\n// Shared request-header resolution for 3D Tiles layers. Google Photorealistic\n// 3D Tiles authenticate with a Maps API key sent in the `X-GOOG-API-KEY` header,\n// but the key is deliberately stripped from the store/project record (so shared\n// projects never carry it in plain text) and re-injected at render time from\n// runtime env. Both the MapLibre/deck.gl render path (packages/plugins) and the\n// Cesium globe path (packages/map) must resolve headers the same way, so the\n// logic lives here in core rather than being duplicated per renderer.\n\n/** Google Photorealistic 3D Tiles root host + path. */\nconst GOOGLE_PHOTOREALISTIC_TILES_HOST = \"tile.googleapis.com\";\nconst GOOGLE_PHOTOREALISTIC_TILES_PATH = \"/v1/3dtiles/root.json\";\n/** Header Google's tile server expects the Maps API key in. */\nexport const GOOGLE_MAPS_API_KEY_HEADER = \"X-GOOG-API-KEY\";\n\n/** Whether `url` points at Google's Photorealistic 3D Tiles root tileset. */\nexport function isGooglePhotorealisticTilesetUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n return (\n parsed.hostname === GOOGLE_PHOTOREALISTIC_TILES_HOST &&\n parsed.pathname === GOOGLE_PHOTOREALISTIC_TILES_PATH\n );\n } catch {\n return false;\n }\n}\n\n/** A masked placeholder (all asterisks) is a UI stand-in, not a real key. */\nfunction isMaskedKey(value: string): boolean {\n return /^\\*+$/.test(value.trim());\n}\n\n/** Drop the `X-GOOG-API-KEY` header (case-insensitive), keeping the rest. */\nexport function stripGoogleMapsApiKeyHeader(\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const entries = Object.entries(headers).filter(\n ([name]) => name.toLowerCase() !== GOOGLE_MAPS_API_KEY_HEADER.toLowerCase(),\n );\n return entries.length > 0 ? Object.fromEntries(entries) : undefined;\n}\n\n/**\n * The real `X-GOOG-API-KEY` value from `headers`, or `undefined` when absent,\n * blank, or a masked placeholder.\n */\nexport function googleMapsApiKeyHeaderValue(\n headers: Record<string, string> | undefined,\n): string | undefined {\n if (!headers) return undefined;\n const entry = Object.entries(headers).find(\n ([name]) => name.toLowerCase() === GOOGLE_MAPS_API_KEY_HEADER.toLowerCase(),\n );\n const value = entry?.[1].trim();\n if (!value || isMaskedKey(value)) return undefined;\n return value;\n}\n\n/** Collapse an empty record to `undefined`, otherwise pass it through. */\nexport function nonEmptyRecord(\n value: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n return value && Object.keys(value).length > 0 ? value : undefined;\n}\n\n/**\n * Resolve the request headers a 3D Tiles layer should load with. For non-Google\n * tilesets the headers pass through unchanged. For Google Photorealistic tiles\n * the `X-GOOG-API-KEY` header is (re)built from, in order: an explicit key in\n * the given headers, the `googleMapsApiKey` argument, then `getGoogleMapsApiKey()`\n * (runtime env). Returns `undefined` when there is nothing to send.\n */\nexport function resolveThreeDTilesRequestHeaders(\n url: string,\n headers: Record<string, string> | undefined,\n googleMapsApiKey?: string,\n): Record<string, string> | undefined {\n if (!isGooglePhotorealisticTilesetUrl(url)) return headers;\n const nonGoogleHeaders = stripGoogleMapsApiKeyHeader(headers);\n const apiKey = googleMapsApiKeyHeaderValue(headers) ?? googleMapsApiKey ?? getGoogleMapsApiKey();\n if (!apiKey) return nonEmptyRecord(nonGoogleHeaders);\n return {\n ...(nonGoogleHeaders ?? {}),\n [GOOGLE_MAPS_API_KEY_HEADER]: apiKey,\n };\n}\n\n/**\n * The request headers to persist on a 3D Tiles layer record. Non-Google\n * tilesets keep their headers; Google Photorealistic tiles keep any non-key\n * custom headers but never persist the `X-GOOG-API-KEY` value, so shared\n * projects do not carry the key in plain text (it is re-injected at render\n * time by `resolveThreeDTilesRequestHeaders`).\n */\nexport function persistedThreeDTilesRequestHeaders(\n url: string,\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!isGooglePhotorealisticTilesetUrl(url)) return headers;\n return nonEmptyRecord(stripGoogleMapsApiKeyHeader(headers));\n}\n","/**\n * Credentials a project refers to by name rather than carrying inline.\n *\n * On the desktop app the geocoding API keys and secret environment variables\n * of a project live in the OS keychain, keyed device-wide by provider id /\n * variable name. The project file keeps the names with empty values. Request\n * headers never go there: they reference variables as `${NAME}` instead (see\n * `resolveProjectHeaderReferences`). This module is the pure half of that\n * scheme: the account names, the overlay that fills empty values from the\n * stored ones, and the split that moves values out of a project before it is\n * written. The app installs the lookup; without one (web, Jupyter) every\n * function here is the identity, so values stay in the project file.\n *\n * A non-empty value in the project always wins over the stored one: a file\n * that still carries plaintext (older files, the web build) is used as-is for\n * the session.\n */\n\nimport { GEOCODING_PROVIDERS } from \"./geocoding\";\nimport { resolveHeaderReferences } from \"./header-references\";\nimport { useAppStore } from \"./store\";\nimport type {\n GeocodingPreferences,\n GeoLibreProject,\n ProjectPreferences,\n RuntimeEnvironmentVariable,\n} from \"./types\";\n\n// Account names are stable keychain data: renaming one orphans every value a\n// user already stored under it.\nexport function geocodingApiKeyAccount(providerId: string): string {\n return `project.geocoding.apiKey.${providerId}`;\n}\n\nexport function environmentVariableAccount(key: string): string {\n return `project.env.${key}`;\n}\n\n/** Rows are secret unless explicitly marked `secret: false`. */\nexport function isSecretEnvironmentVariable(variable: RuntimeEnvironmentVariable): boolean {\n return variable.secret !== false;\n}\n\nexport type ProjectCredentialLookup = (account: string) => string | undefined;\n\nlet projectCredentialLookup: ProjectCredentialLookup | null = null;\n\n/** Install (or clear, with `null`) the source of stored project credentials. */\nexport function setProjectCredentialLookup(lookup: ProjectCredentialLookup | null): void {\n projectCredentialLookup = lookup;\n}\n\n/** The stored value for `account`, or `undefined` when none (or no lookup). */\nexport function lookupProjectCredential(account: string): string | undefined {\n const value = projectCredentialLookup?.(account);\n return value ? value : undefined;\n}\n\n/** Fill empty geocoding API keys from the stored ones. */\nexport function overlayStoredGeocodingApiKeys(\n geocoding: GeocodingPreferences,\n): GeocodingPreferences {\n let apiKeys: Record<string, string> | null = null;\n for (const { id } of GEOCODING_PROVIDERS) {\n if (geocoding.apiKeys[id]?.trim()) continue;\n const stored = lookupProjectCredential(geocodingApiKeyAccount(id));\n if (!stored) continue;\n apiKeys ??= { ...geocoding.apiKeys };\n apiKeys[id] = stored;\n }\n return apiKeys ? { ...geocoding, apiKeys } : geocoding;\n}\n\n/** Fill empty secret environment variable values from the stored ones. */\nexport function overlayStoredEnvironmentVariables(\n variables: RuntimeEnvironmentVariable[],\n): RuntimeEnvironmentVariable[] {\n let changed = false;\n const overlaid = variables.map((variable) => {\n if (!isSecretEnvironmentVariable(variable) || variable.value !== \"\") return variable;\n const key = variable.key.trim();\n const stored = key ? lookupProjectCredential(environmentVariableAccount(key)) : undefined;\n if (!stored) return variable;\n changed = true;\n return { ...variable, value: stored };\n });\n return changed ? overlaid : variables;\n}\n\n/** Preferences with every empty stored credential filled in. */\nexport function overlayStoredPreferenceCredentials(\n preferences: ProjectPreferences,\n): ProjectPreferences {\n const geocoding = overlayStoredGeocodingApiKeys(preferences.geocoding);\n const environmentVariables = overlayStoredEnvironmentVariables(preferences.environmentVariables);\n if (\n geocoding === preferences.geocoding &&\n environmentVariables === preferences.environmentVariables\n ) {\n return preferences;\n }\n return { ...preferences, geocoding, environmentVariables };\n}\n\n/**\n * The credential changes an edited Settings draft makes, keyed by account\n * (`\"\"` deletes).\n *\n * - `seeded`: the overlaid preferences the dialog opened with.\n * - `next`: the normalized draft being saved.\n *\n * Only values the user changed are returned, so an untouched session override\n * (plaintext from the opened file) stays out of the keychain and opening a\n * file never writes it. Removing, renaming, or un-secreting a variable never\n * deletes its stored value: names are shared by every project on the device.\n * Clearing a field deletes the stored value only when the field showed that\n * stored value; clearing a file override that differed from it just drops the\n * override, so the shared value applies again and other projects keep it.\n */\nexport function changedPreferenceCredentials(\n seeded: ProjectPreferences,\n next: ProjectPreferences,\n): Record<string, string> {\n const changes: Record<string, string> = {};\n const record = (account: string, seededValue: string, nextValue: string) => {\n if (nextValue === seededValue) return;\n if (nextValue === \"\" && seededValue !== lookupProjectCredential(account)) return;\n changes[account] = nextValue;\n };\n\n const providerIds = new Set([\n ...Object.keys(seeded.geocoding.apiKeys),\n ...Object.keys(next.geocoding.apiKeys),\n ]);\n for (const providerId of providerIds) {\n record(\n geocodingApiKeyAccount(providerId),\n seeded.geocoding.apiKeys[providerId]?.trim() ?? \"\",\n next.geocoding.apiKeys[providerId]?.trim() ?? \"\",\n );\n }\n\n const seededSecrets = new Map<string, string>();\n for (const variable of seeded.environmentVariables) {\n if (!isSecretEnvironmentVariable(variable) || seededSecrets.has(variable.key)) continue;\n seededSecrets.set(variable.key, variable.value);\n }\n for (const variable of next.environmentVariables) {\n if (!isSecretEnvironmentVariable(variable)) continue;\n const seededValue = seededSecrets.get(variable.key);\n // A new blank row falls back to any value stored under the name; it\n // never deletes it.\n if (seededValue === undefined && variable.value === \"\") continue;\n record(environmentVariableAccount(variable.key), seededValue ?? \"\", variable.value);\n }\n\n return changes;\n}\n\n/**\n * Move unambiguous geocoding API keys and secret environment variable values\n * out of `project` (a copy; the input is untouched) into `secrets`, keyed by\n * account. Secret environment rows that have no unique name remain in the\n * project for an explicit keep/strip choice. Layers pass through: their\n * request headers reference variables as `${NAME}` rather than holding\n * secrets that need moving.\n */\nexport function splitProjectCredentials(project: GeoLibreProject): {\n project: GeoLibreProject;\n secrets: Record<string, string>;\n} {\n const secrets: Record<string, string> = {};\n\n for (const [providerId, apiKey] of Object.entries(project.preferences.geocoding.apiKeys)) {\n const trimmed = apiKey.trim();\n if (trimmed) secrets[geocodingApiKeyAccount(providerId)] = trimmed;\n }\n // A hand-edited file can contain duplicate names even though Settings\n // rejects them. One keychain account cannot preserve two different values.\n const nameCounts = new Map<string, number>();\n for (const variable of project.preferences.environmentVariables) {\n const key = variable.key.trim();\n if (key) nameCounts.set(key, (nameCounts.get(key) ?? 0) + 1);\n }\n const environmentVariables = project.preferences.environmentVariables.map((variable) => {\n if (!isSecretEnvironmentVariable(variable) || variable.value === \"\") return variable;\n const key = variable.key.trim();\n // Keep ambiguous or nameless rows intact for the local keep/strip prompt.\n if (!key || (nameCounts.get(key) ?? 0) > 1) return variable;\n secrets[environmentVariableAccount(key)] = variable.value;\n return { ...variable, value: \"\" };\n });\n\n return {\n project: {\n ...project,\n preferences: {\n ...project.preferences,\n geocoding: { ...project.preferences.geocoding, apiKeys: {} },\n environmentVariables,\n },\n },\n secrets,\n };\n}\n\n/**\n * `headers` with every `${NAME}` resolved from the project's enabled\n * Environment Variables (stored secret values filled in). Only those rows\n * count, not the OS environment or build-time values that share the runtime\n * env. A header referencing an unset variable is dropped.\n */\nexport function resolveProjectHeaderReferences(\n headers: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!headers) return headers;\n const values: Record<string, string> = {};\n const variables = overlayStoredEnvironmentVariables(\n useAppStore.getState().preferences.environmentVariables,\n );\n for (const variable of variables) {\n const key = variable.key.trim();\n if (variable.enabled && key) values[key] = variable.value;\n }\n return resolveHeaderReferences(headers, values);\n}\n","import type { FeatureCollection } from \"geojson\";\nimport type { GeoLibreProject, FieldVisibility } from \"./types\";\n\n/**\n * Returns a new FeatureCollection with properties marked as \"excluded\" removed.\n */\nexport function excludeHiddenFieldsFromGeojson(\n geojson: FeatureCollection,\n fieldVisibility?: Record<string, FieldVisibility>,\n): FeatureCollection {\n const excludedKeys = new Set(\n Object.entries(fieldVisibility || {})\n .filter(([_, visibility]) => visibility === \"excluded\")\n .map(([key]) => key),\n );\n\n if (excludedKeys.size === 0) {\n return geojson;\n }\n\n // Deep clone to avoid mutating the live store state\n const stripped: FeatureCollection = {\n ...geojson,\n features: geojson.features.map((feature) => {\n const properties = { ...feature.properties };\n for (const key of excludedKeys) {\n delete properties[key];\n }\n return { ...feature, properties };\n }),\n };\n\n return stripped;\n}\n\n/**\n * Returns a new GeoLibreProject where all layers have their excluded fields\n * physically removed from their inline GeoJSON.\n */\nexport function excludeHiddenFieldsFromProject(project: GeoLibreProject): GeoLibreProject {\n let changed = false;\n const layers = project.layers.map((layer) => {\n if (!layer.fieldVisibility) return layer;\n\n let updatedLayer = layer;\n\n if (layer.geojson) {\n const strippedGeojson = excludeHiddenFieldsFromGeojson(layer.geojson, layer.fieldVisibility);\n if (strippedGeojson !== layer.geojson) {\n changed = true;\n const { geoarrow: _staleGeoarrow, ...rest } = updatedLayer;\n updatedLayer = { ...rest, geojson: strippedGeojson };\n }\n }\n\n if (layer.metadata?.embeddedGeoJSON) {\n const strippedEmbedded = excludeHiddenFieldsFromGeojson(\n layer.metadata.embeddedGeoJSON as FeatureCollection,\n layer.fieldVisibility,\n );\n if (strippedEmbedded !== layer.metadata.embeddedGeoJSON) {\n changed = true;\n updatedLayer = {\n ...updatedLayer,\n metadata: {\n ...updatedLayer.metadata,\n embeddedGeoJSON: strippedEmbedded,\n },\n };\n }\n }\n\n return updatedLayer;\n });\n\n return changed ? { ...project, layers } : project;\n}\n","import type { Feature, FeatureCollection } from \"geojson\";\nimport type { EditorTrackingConfig } from \"./types\";\n\n/** Default field names for maintained editor tracking columns. */\nexport const DEFAULT_EDITOR_TRACKING_CONFIG: Required<EditorTrackingConfig> = {\n enabled: false,\n createdByField: \"created_by\",\n createdAtField: \"created_at\",\n editedByField: \"edited_by\",\n editedAtField: \"edited_at\",\n};\n\n/**\n * Identity written to the author fields when nothing better is known.\n *\n * Deliberately NOT translated: it is written into the user's data, where a\n * value that changes with the UI language would make the same editor appear as\n * several different people across a project's history.\n */\nexport const DEFAULT_EDITOR_IDENTITY = \"local-user\";\n\n/**\n * `localStorage` key holding the display name this browser edits under.\n *\n * Shared with the Comments panel, which asks for the same name and stores it\n * here, so a user who has introduced themselves once is recognized by both\n * features instead of being prompted twice.\n */\nexport const EDITOR_IDENTITY_STORAGE_KEY = \"geolibre_author_name\";\n\n/**\n * Choose the identity to stamp, most authoritative source first.\n *\n * A live collaboration session names its participants, so that name wins: it is\n * the one other people in the session see attached to the same edits. Otherwise\n * fall back to the locally configured display name, then to\n * {@link DEFAULT_EDITOR_IDENTITY}.\n *\n * @param collabName Display name from an active collaboration session.\n * @param storedName Locally configured display name.\n * @returns A non-empty identity string.\n */\nexport function pickEditorIdentity(collabName?: string | null, storedName?: string | null): string {\n return collabName?.trim() || storedName?.trim() || DEFAULT_EDITOR_IDENTITY;\n}\n\n/**\n * The four maintained column names, or `null` when tracking is off or the\n * configuration is unusable. Ordered created-then-edited so callers that render\n * the columns get a stable, meaningful order.\n */\nexport function editorTrackingFieldNames(config?: EditorTrackingConfig): string[] | null {\n const resolved = resolveEditorTrackingConfigForQuery(config);\n if (!resolved?.enabled) return null;\n return [\n resolved.createdByField,\n resolved.createdAtField,\n resolved.editedByField,\n resolved.editedAtField,\n ];\n}\n\n/**\n * Options for editor tracking stamping functions.\n */\nexport interface EditorTrackingStampOptions {\n /** Optional custom editor tracking field configuration. */\n config?: EditorTrackingConfig;\n /**\n * Identity string of the author/editor (e.g. username, email, or client ID).\n * Defaults to {@link DEFAULT_EDITOR_IDENTITY}.\n */\n userIdentity?: string;\n /** ISO timestamp override for deterministic testing or batch operations. Defaults to `new Date().toISOString()`. */\n timestamp?: string;\n}\n\n/**\n * Fully resolve an {@link EditorTrackingConfig} with fallback default field names.\n */\nexport function resolveEditorTrackingConfig(\n config?: EditorTrackingConfig,\n): Required<EditorTrackingConfig> {\n // Trimmed on the way in, because these become literal GeoJSON property keys:\n // a name typed as `\" created_by \"` in the settings form would otherwise pass\n // validation and then write a column whose name has invisible padding,\n // matching neither what the user typed nor the default it looks like.\n const resolved = {\n enabled: config?.enabled ?? DEFAULT_EDITOR_TRACKING_CONFIG.enabled,\n createdByField: (\n config?.createdByField ?? DEFAULT_EDITOR_TRACKING_CONFIG.createdByField\n ).trim(),\n createdAtField: (\n config?.createdAtField ?? DEFAULT_EDITOR_TRACKING_CONFIG.createdAtField\n ).trim(),\n editedByField: (config?.editedByField ?? DEFAULT_EDITOR_TRACKING_CONFIG.editedByField).trim(),\n editedAtField: (config?.editedAtField ?? DEFAULT_EDITOR_TRACKING_CONFIG.editedAtField).trim(),\n };\n\n // Tracking that is off writes nothing, so the names cannot matter — and a\n // half-filled or hand-edited config left behind on a disabled layer must not\n // make every save on that layer throw. Validate only what will be used.\n if (!resolved.enabled) return resolved;\n\n const fields = [\n resolved.createdByField,\n resolved.createdAtField,\n resolved.editedByField,\n resolved.editedAtField,\n ];\n if (fields.some((field) => field === \"\") || new Set(fields).size !== fields.length) {\n throw new Error(\"Editor tracking field names must be non-empty and unique\");\n }\n\n return resolved;\n}\n\n/**\n * Resolve a config for the query helpers, yielding `null` rather than throwing when\n * the configured field names are blank or collide.\n *\n * The query helpers run once per field on every Attribute Table / Field Calculator\n * render, and the field names are user-editable strings, so a half-filled settings\n * form or a hand-edited `.geolibre.json` must degrade to \"tracking off\" instead of\n * crashing the panel. The stamping helpers keep the throw: a write that silently\n * skipped tracking would leave a layer looking tracked while recording nothing.\n */\nfunction resolveEditorTrackingConfigForQuery(\n config?: EditorTrackingConfig,\n): Required<EditorTrackingConfig> | null {\n try {\n return resolveEditorTrackingConfig(config);\n } catch {\n return null;\n }\n}\n\n/**\n * Check whether a field name corresponds to one of the maintained editor tracking columns.\n *\n * Returns `false` for an invalid config, so callers can use this as a plain predicate.\n */\nexport function isMaintainedEditorTrackingField(\n fieldName: string,\n config?: EditorTrackingConfig,\n): boolean {\n const resolved = resolveEditorTrackingConfigForQuery(config);\n if (!resolved?.enabled) {\n return false;\n }\n return (\n fieldName === resolved.createdByField ||\n fieldName === resolved.createdAtField ||\n fieldName === resolved.editedByField ||\n fieldName === resolved.editedAtField\n );\n}\n\n/**\n * Ensure all configured editor tracking field names are included in a field list.\n *\n * Returns the list unchanged for an invalid config, matching\n * {@link isMaintainedEditorTrackingField}.\n */\nexport function ensureEditorTrackingFields(\n fields: string[],\n config?: EditorTrackingConfig,\n): string[] {\n const trackingFields = editorTrackingFieldNames(config);\n if (!trackingFields) {\n return fields;\n }\n const result = [...fields];\n for (const tf of trackingFields) {\n if (!result.includes(tf)) {\n result.push(tf);\n }\n }\n return result;\n}\n\n/**\n * Stamp editor tracking metadata onto a feature's properties object.\n *\n * On `\"create\"`: sets all four columns to this editor and this timestamp.\n * `\"create\"` states that the feature is being created here and now, so it is\n * authoritative — a value already sitting in one of these columns describes\n * some other feature, not this one. That case is reachable: the geometry\n * editor's copy action clones a tracked feature's properties, and a Field\n * Collection form can define a capture field whose key happens to be a tracking\n * column. Both would otherwise credit a brand-new feature to whoever created\n * the thing it was copied from.\n *\n * On `\"update\"`: sets `edited_by`/`edited_at` and preserves `created_by` /\n * `created_at`, which record something this edit did not change.\n */\nexport function stampFeaturePropertiesEditorTracking(\n properties: Record<string, unknown> | null | undefined,\n action: \"create\" | \"update\",\n options?: EditorTrackingStampOptions,\n): Record<string, unknown> {\n const resolved = resolveEditorTrackingConfig(options?.config);\n if (!resolved.enabled) {\n return properties ? { ...properties } : {};\n }\n\n const result: Record<string, unknown> = properties ? { ...properties } : {};\n const now = options?.timestamp ?? new Date().toISOString();\n const actor = options?.userIdentity || DEFAULT_EDITOR_IDENTITY;\n\n if (action === \"create\") {\n result[resolved.createdByField] = actor;\n result[resolved.createdAtField] = now;\n }\n\n result[resolved.editedByField] = actor;\n result[resolved.editedAtField] = now;\n\n return result;\n}\n\n/**\n * Stamp editor tracking metadata onto a GeoJSON {@link Feature}.\n */\nexport function stampFeatureEditorTracking<T extends Feature>(\n feature: T,\n action: \"create\" | \"update\",\n options?: EditorTrackingStampOptions,\n): T {\n const resolved = resolveEditorTrackingConfig(options?.config);\n if (!resolved.enabled) {\n return feature;\n }\n\n return {\n ...feature,\n properties: stampFeaturePropertiesEditorTracking(feature.properties, action, options),\n };\n}\n\n/**\n * Stamp editor tracking metadata onto all features in a {@link FeatureCollection}.\n */\nexport function stampFeatureCollectionEditorTracking(\n collection: FeatureCollection,\n action: \"create\" | \"update\",\n options?: EditorTrackingStampOptions,\n): FeatureCollection {\n const resolved = resolveEditorTrackingConfig(options?.config);\n if (!resolved.enabled) {\n return collection;\n }\n\n const timestamp = options?.timestamp ?? new Date().toISOString();\n const opts = { ...options, timestamp };\n\n return {\n ...collection,\n features: collection.features.map((feat) => stampFeatureEditorTracking(feat, action, opts)),\n };\n}\n","import { EDITOR_IDENTITY_STORAGE_KEY, pickEditorIdentity } from \"./editor-tracking\";\nimport { useAppStore } from \"./store\";\n\n/**\n * Runtime resolution of \"who is editing\", for the editor tracking stamps.\n *\n * Split from the pure `editor-tracking.ts` helpers because it reads two ambient\n * sources — the collaboration session in the store and `localStorage` — which\n * the stamping functions must stay free of so they remain unit-testable and\n * usable from Node (the MCP server authors projects without either).\n */\n\n/**\n * The display name this browser edits under, or `\"\"` when none is set.\n *\n * `localStorage` throws rather than returning null in a partitioned or\n * storage-blocked context (Safari private browsing, an embed in a third-party\n * iframe), so a failure here has to read as \"no name set\" — an editor whose\n * browser refuses storage must still be able to edit.\n */\nexport function readStoredAuthorName(): string {\n try {\n if (typeof localStorage === \"undefined\") return \"\";\n return localStorage.getItem(EDITOR_IDENTITY_STORAGE_KEY)?.trim() ?? \"\";\n } catch {\n return \"\";\n }\n}\n\n/**\n * Persist the display name to edit under. A blank name clears it, so the user\n * can go back to the anonymous default rather than being stuck with a typo.\n */\nexport function setStoredAuthorName(name: string): void {\n try {\n if (typeof localStorage === \"undefined\") return;\n const trimmed = name.trim();\n if (trimmed) {\n localStorage.setItem(EDITOR_IDENTITY_STORAGE_KEY, trimmed);\n } else {\n localStorage.removeItem(EDITOR_IDENTITY_STORAGE_KEY);\n }\n } catch {\n // Storage unavailable: the name applies to this session only.\n }\n}\n\n/**\n * The identity to stamp on edits made right now: the collaboration session's\n * name when one is active, else the locally configured name, else the anonymous\n * default.\n *\n * @returns A non-empty identity string.\n */\nexport function currentEditorIdentity(): string {\n const { isActive, selfName } = useAppStore.getState().collaboration;\n return pickEditorIdentity(isActive ? selfName : undefined, readStoredAuthorName());\n}\n","/**\n * Datum shifts (proj4 `+towgs84` Helmert parameters) for coordinate systems\n * whose datum is not WGS84-compatible, by EPSG code.\n *\n * The offline EPSG tables GeoLibre resolves codes with\n * (`geotiff-geokeys-to-proj4`) leave the shift out for many national grids, and\n * epsg.io's PROJJSON for a projected CRS carries no transformation either, so\n * proj4 would treat those datums as WGS84 and place data tens to hundreds of\n * metres off (about 100 m for the British National Grid). The parameters are\n * EPSG's (the transformation epsg.io's proj4 strings use), within a few\n * metres of PROJ's best transformation for each (15 m for the NAD27 mean);\n * grid-based transformations (OSTN15, NTv2, NADCON) are finer but need grid\n * files proj4 does not ship. Datums the tables already shift (ED50, AGD66,\n * AGD84, Tokyo, NZGD49, TM75 and others) are not listed.\n */\n\ninterface DatumShift {\n /** Datum name, for the record. */\n datum: string;\n /** EPSG datum codes (`GeogGeodeticDatumGeoKey`). */\n datums: number[];\n /** EPSG geographic and projected CRS codes on the datum. */\n crs: number[];\n towgs84: string;\n}\n\n/** Inclusive ranges of codes, as a list. */\nconst range = (from: number, to: number) =>\n Array.from({ length: to - from + 1 }, (_, i) => from + i);\n\nconst SHIFTS: DatumShift[] = [\n {\n datum: \"OSGB 1936\",\n datums: [6277],\n crs: [4277, 27700],\n towgs84: \"446.448,-125.157,542.06,0.15,0.247,0.842,-20.489\",\n },\n {\n datum: \"TM65 (Ireland)\",\n datums: [6299],\n crs: [4299, 29902],\n towgs84: \"482.5,-130.6,564.6,-1.042,-0.214,-0.631,8.15\",\n },\n {\n datum: \"DHDN\",\n datums: [6314],\n crs: [4314, 3068, ...range(31466, 31469)],\n towgs84: \"598.1,73.7,418.2,0.202,0.045,-2.455,6.7\",\n },\n {\n datum: \"Amersfoort\",\n datums: [6289],\n crs: [4289, 28992],\n towgs84: \"565.417,50.3319,465.552,-0.398957,0.343988,-1.8774,4.0725\",\n },\n {\n datum: \"CH1903\",\n datums: [6149],\n crs: [4149, 21781],\n towgs84: \"674.4,15.1,405.3,0,0,0,0\",\n },\n {\n datum: \"CH1903+\",\n datums: [6150],\n crs: [4150, 2056],\n towgs84: \"674.374,15.056,405.346,0,0,0,0\",\n },\n {\n datum: \"Belge 1972\",\n datums: [6313],\n crs: [4313, 31370],\n towgs84: \"-106.8686,52.2978,-103.7239,0.3366,-0.457,1.8422,-1.2747\",\n },\n {\n datum: \"S-JTSK\",\n datums: [6156],\n crs: [4156, 5514],\n towgs84: \"589,76,480,0,0,0,0\",\n },\n {\n datum: \"Monte Mario\",\n datums: [6265],\n crs: [4265, 3003, 3004],\n towgs84: \"-104.1,-49.1,-9.9,0.971,-2.917,0.714,-11.68\",\n },\n {\n datum: \"HD72\",\n datums: [6237],\n crs: [4237, 23700],\n towgs84: \"52.17,-71.82,-14.9,0,0,0,0\",\n },\n {\n datum: \"GGRS87\",\n datums: [6121],\n crs: [4121, 2100],\n towgs84: \"-199.87,74.79,246.62,0,0,0,0\",\n },\n {\n datum: \"Hong Kong 1980\",\n datums: [6611],\n crs: [4611, 2326],\n towgs84: \"-162.619,-276.959,-161.764,0.067753,-2.24365,-1.15883,-1.09425\",\n },\n {\n // A conterminous-US mean, so only the UTM zones over it (10 N to 19 N),\n // not the geographic CRS or the datum, which span all of North America.\n datum: \"NAD27 (conterminous US mean)\",\n datums: [],\n crs: range(26710, 26719),\n towgs84: \"-8,160,176,0,0,0,0\",\n },\n];\n\nconst BY_CRS = new Map<number, string>();\nconst BY_DATUM = new Map<number, string>();\nfor (const shift of SHIFTS) {\n for (const code of shift.crs) BY_CRS.set(code, shift.towgs84);\n for (const code of shift.datums) BY_DATUM.set(code, shift.towgs84);\n}\n\n/**\n * The `+towgs84` parameters for an EPSG CRS or datum code, or null when the\n * code is not one whose shift GeoLibre adds.\n *\n * @param crs An EPSG geographic or projected CRS code.\n * @param datum An EPSG datum code, used when the CRS code is unknown.\n */\nexport function datumShiftFor(\n crs: number | null | undefined,\n datum?: number | null,\n): string | null {\n return (\n (crs != null ? BY_CRS.get(crs) : undefined) ??\n (datum != null ? BY_DATUM.get(datum) : undefined) ??\n null\n );\n}\n\n/** GeoTIFF's \"user-defined\" geokey value. */\nconst USER_DEFINED = 32767;\n\n/** A geokey's numeric value, or null for a missing or user-defined one. */\nfunction keyCode(value: unknown): number | null {\n const code = Number(value);\n return Number.isInteger(code) && code > 0 && code !== USER_DEFINED ? code : null;\n}\n\n/**\n * Add the datum shift to a proj4 definition built for an EPSG CRS (or from a\n * GeoTIFF's geokeys), when the definition has none and the CRS is one whose\n * shift is known. A definition that already names a datum transformation\n * (`+towgs84`, `+datum=`, `+nadgrids=`) is returned unchanged.\n *\n * @param definition A proj4 definition string.\n * @param crs The EPSG CRS code it was built for.\n * @param datum The EPSG datum code, for a user-defined CRS.\n */\nexport function withDatumShift(\n definition: string,\n crs: number | null | undefined,\n datum?: number | null,\n): string {\n if (/\\+(towgs84|datum|nadgrids)=/.test(definition)) return definition;\n const shift = datumShiftFor(crs, datum);\n return shift ? `${definition.trim()} +towgs84=${shift}` : definition;\n}\n\n/**\n * Add the datum shift to a proj4 definition built from GeoTIFF geokeys, from\n * the projected or geographic CRS code, else the datum code.\n *\n * @param definition The proj4 definition built from `geoKeys`.\n * @param geoKeys The GeoTIFF's geokeys (geotiff.js names).\n */\nexport function withGeoKeysDatumShift(\n definition: string,\n geoKeys: Record<string, unknown> | null | undefined,\n): string {\n if (!geoKeys) return definition;\n const crs =\n keyCode(geoKeys.ProjectedCSTypeGeoKey) ??\n keyCode(geoKeys.ProjectedCRSGeoKey) ??\n keyCode(geoKeys.GeographicTypeGeoKey) ??\n keyCode(geoKeys.GeodeticCRSGeoKey);\n const datum = keyCode(geoKeys.GeogGeodeticDatumGeoKey) ?? keyCode(geoKeys.GeodeticDatumGeoKey);\n return withDatumShift(definition, crs, datum);\n}\n\n/** The part of `cog-tiler-wasm` the datum-shift hook needs (0.5 and later). */\ninterface CogTilerCrsHook {\n setSourceCrsResolver?: (\n fn: ((def: string, geoKeys: Record<string, unknown>) => string | null | undefined) | null,\n ) => void;\n}\n\n/**\n * Have `cog-tiler-wasm` add the datum shift to each COG's source CRS, so a\n * raster it tiles lands where the rest of GeoLibre puts it. Idempotent; a\n * tiler without the hook is left as it is. WKT definitions are left alone.\n *\n * @param module The imported `cog-tiler-wasm` module.\n */\nexport function installCogTilerDatumShift(module: CogTilerCrsHook): void {\n module.setSourceCrsResolver?.((def, geoKeys) =>\n def.trimStart().startsWith(\"+\") ? withGeoKeysDatumShift(def, geoKeys) : def,\n );\n}\n","// JPEG-in-TIFF tiles that omit their own tables (TIFF tag 347 holds them), which\n// cog-tiler-wasm's decoder rejects as \"use of unset quantization table\". Every\n// engine that opens a COG through the tiler (the MapLibre raster control, the\n// Cesium globe, the ArcGIS view) wraps its `openCog` with {@link\n// withJpegTablesPatch} so those tiles decode through geotiff.js instead.\n\nconst DEFAULT_COEFFICIENTS = [0.299, 0.587, 0.114] as const;\nconst DEFAULT_REFERENCE_BLACK_WHITE = [0, 255, 128, 255, 128, 255] as const;\n\ntype NumericArray = ArrayLike<number> | undefined;\n\nfunction finiteValues(values: NumericArray, length: number): number[] | null {\n if (!values || values.length < length) return null;\n const result = Array.from({ length }, (_, index) => Number(values[index]));\n return result.every(Number.isFinite) ? result : null;\n}\n\nfunction validCoefficients(values: NumericArray): number[] | null {\n const coefficients = finiteValues(values, 3);\n if (!coefficients) return null;\n const [kr, kg, kb] = coefficients;\n const sum = kr + kg + kb;\n return kr >= 0 && kg > 0 && kb >= 0 && kr <= 1 && kg <= 1 && kb <= 1 && Math.abs(sum - 1) <= 1e-6\n ? coefficients\n : null;\n}\n\nfunction validReference(values: NumericArray): number[] | null {\n const reference = finiteValues(values, 6);\n if (!reference) return null;\n return reference[1] > reference[0] && reference[3] > reference[2] && reference[5] > reference[4]\n ? reference\n : null;\n}\n\n/** Convert separate TIFF Y/Cb/Cr planes to RGB using tags 529 and 532. */\nexport function convertTiffYCbCrToRgb(\n yPlane: ArrayLike<number>,\n cbPlane: ArrayLike<number>,\n crPlane: ArrayLike<number>,\n coefficients?: NumericArray,\n referenceBlackWhite?: NumericArray,\n): [Float64Array, Float64Array, Float64Array] {\n const [kr, kg, kb] = validCoefficients(coefficients) ?? DEFAULT_COEFFICIENTS;\n const reference = validReference(referenceBlackWhite) ?? DEFAULT_REFERENCE_BLACK_WHITE;\n const [blackY, whiteY, blackCb, whiteCb, blackCr, whiteCr] = reference;\n const yRange = whiteY - blackY;\n const cbRange = whiteCb - blackCb;\n const crRange = whiteCr - blackCr;\n const size = Math.min(yPlane.length, cbPlane.length, crPlane.length);\n const red = new Float64Array(size);\n const green = new Float64Array(size);\n const blue = new Float64Array(size);\n\n for (let index = 0; index < size; index += 1) {\n const y = ((Number(yPlane[index]) - blackY) * 255) / yRange;\n const cb = ((Number(cbPlane[index]) - blackCb) * 127) / cbRange;\n const cr = ((Number(crPlane[index]) - blackCr) * 127) / crRange;\n const r = y + (2 - 2 * kr) * cr;\n const b = y + (2 - 2 * kb) * cb;\n const g = (y - kr * r - kb * b) / kg;\n red[index] = Math.max(0, Math.min(255, r));\n green[index] = Math.max(0, Math.min(255, g));\n blue[index] = Math.max(0, Math.min(255, b));\n }\n\n return [red, green, blue];\n}\n\n/**\n * Whether a cog-tiler-wasm level's `compression` is baseline JPEG, the only\n * codec whose tiles may omit their tables (TIFF tag 347) and so need the\n * geotiff.js window read in {@link patchJpegCogSource}. The string is whitebox-wasm's\n * enum name, so match `Jpeg`/`OldJpeg` exactly: a looser `/jpeg/i` would also\n * catch `JpegXl`, which the wasm decoder handles and geotiff.js cannot decode\n * at all (#2339).\n */\nexport function isAbbreviatedJpegCompression(compression: string | undefined): boolean {\n return /^(old)?jpeg$/i.test(compression ?? \"\");\n}\n\ntype GeoTiffImage = {\n fileDirectory?: {\n PhotometricInterpretation?: number;\n getValue?: (tag: number) => unknown;\n hasTag?: (tag: number) => boolean;\n loadValue?: (tag: number) => Promise<unknown>;\n };\n readRasters: (options: {\n window: [number, number, number, number];\n }) => Promise<ArrayLike<ArrayLike<number>>>;\n};\ntype CogSourceInternals = {\n levels?: Array<{ compression?: string }>;\n tiff?: unknown;\n _tiffImage?: (level: number) => Promise<GeoTiffImage>;\n _assembleWindow?: (\n level: number,\n x: number,\n y: number,\n width: number,\n height: number,\n band?: number,\n ) => Promise<ArrayLike<number>>;\n geolibreJpegTablesPatched?: boolean;\n};\n\n/**\n * Route an abbreviated-JPEG COG's tile windows through geotiff.js, which reads\n * the shared tables, converting Y/Cb/Cr to the RGB bands the renderers expect.\n * Any other source is returned unchanged.\n *\n * @param source - A cog-tiler-wasm source.\n * @returns The same source, patched in place when it needs it.\n */\nexport function patchJpegCogSource(source: unknown): unknown {\n const cog = source as CogSourceInternals;\n if (\n cog.geolibreJpegTablesPatched ||\n !isAbbreviatedJpegCompression(cog.levels?.[0]?.compression) ||\n !cog.tiff ||\n !cog._tiffImage ||\n !cog._assembleWindow\n ) {\n return source;\n }\n\n const windowCache = new Map<string, Promise<ArrayLike<ArrayLike<number>>>>();\n cog._assembleWindow = async (level, x, y, width, height, band = 0) => {\n const key = `${level}/${x}/${y}/${width}/${height}`;\n let decoded = windowCache.get(key);\n if (decoded) {\n // Re-insert on a hit so the size cap below evicts the least recently used.\n windowCache.delete(key);\n windowCache.set(key, decoded);\n } else {\n decoded = cog._tiffImage!(level).then(async (image) => {\n const rasters = await image.readRasters({\n window: [x, y, x + width, y + height],\n });\n // geotiff.js expands the chroma subsampling but deliberately returns\n // the TIFF's native Y/Cb/Cr samples. The renderer expects RGB bands,\n // like the GPU engine, so perform the TIFF/JPEG color transform once\n // for the shared three-band window.\n const photometric =\n image.fileDirectory?.PhotometricInterpretation ?? image.fileDirectory?.getValue?.(262);\n if (photometric !== 6 || rasters.length < 3) {\n return rasters;\n }\n const directory = image.fileDirectory;\n const readTag = async (tag: number): Promise<ArrayLike<number> | undefined> => {\n if (directory?.hasTag?.(tag) === false) return undefined;\n const value = directory?.loadValue\n ? await directory.loadValue(tag)\n : directory?.getValue?.(tag);\n return value as ArrayLike<number> | undefined;\n };\n const [coefficients, referenceBlackWhite] = await Promise.all([readTag(529), readTag(532)]);\n // Baseline JPEG is 8-bit, so narrow the clamped float planes to one\n // byte per sample before caching: a window held as Float64Array costs\n // 8x the memory across the whole cache.\n return convertTiffYCbCrToRgb(\n rasters[0],\n rasters[1],\n rasters[2],\n coefficients,\n referenceBlackWhite,\n ).map((plane) => Uint8ClampedArray.from(plane));\n });\n windowCache.set(key, decoded);\n void decoded.catch(() => {\n if (windowCache.get(key) === decoded) windowCache.delete(key);\n });\n if (windowCache.size > 32) windowCache.delete(windowCache.keys().next().value!);\n }\n const rasters = await decoded;\n return rasters[band] ?? rasters[0];\n };\n cog.geolibreJpegTablesPatched = true;\n return source;\n}\n\n/**\n * A cog-tiler module whose `openCog` returns {@link patchJpegCogSource}-patched\n * sources; the rest of the module is passed through.\n *\n * @param module - The cog-tiler-wasm module (or a test double).\n * @returns The wrapped module.\n */\nexport function withJpegTablesPatch<M extends { openCog: (input: never) => Promise<unknown> }>(\n module: M,\n): M {\n return {\n ...module,\n openCog: (async (input: never) =>\n patchJpegCogSource(await module.openCog(input))) as M[\"openCog\"],\n };\n}\n"],"mappings":"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"}