@geolibre/core 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/index.d.mts +6632 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +12063 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["isPlainObject","uuidv4","isPlainObject","isGeoJsonPayload","MAX_REDACT_DEPTH","uuidv4"],"sources":["../src/types.ts","../src/diagram.ts","../src/marker-shape.ts","../src/hyperlink.ts","../src/photo.ts","../src/ellipsoids.ts","../src/regional-basemaps.ts","../src/geojson-z.ts","../src/color-ramp.ts","../src/paths.ts","../src/polyline.ts","../src/runtime-env.ts","../src/routing.ts","../src/selection.ts","../src/expressions.ts","../src/vector-color.ts","../src/external-native-paint.ts","../src/attribute-form.ts","../src/virtual-fields.ts","../src/joins.ts","../src/capabilities.ts","../src/history.ts","../src/layer-groups.ts","../src/style-library.ts","../src/layer-ref-scrub.ts","../src/print-layout-config.ts","../src/project.ts","../src/layer-defaults.ts","../src/layer-style-clipboard.ts","../src/credentials.ts","../src/layer-library.ts","../src/store.ts","../src/selection-actions.ts","../src/scale-units.ts","../src/elevation.ts","../src/camera-altitude.ts","../src/storymap-sample.ts","../src/storymap-io.ts","../src/geocoding.ts","../src/platform.ts","../src/three-d-tiles.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 * 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 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 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 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/**\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/** 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\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 /**\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 * 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 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\"`.\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 * 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?: \"maplibre\" | \"cesium\";\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 /**\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, or `\"utm\"` zone easting/northing. Stored as a string\n * rather than a union so `@geolibre/core` does not have to depend on the\n * formatter, which lives with the app's DMS helpers and the Gridlines\n * plugin's UTM projection; the app normalises unknown values to `\"dd\"`.\n */\n coordinateFormat: string;\n}\n\nexport interface RuntimeEnvironmentVariable {\n key: string;\n value: string;\n enabled: 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 coordinateFormat: \"dd\",\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/** Camera target captured for a story chapter. */\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 /**\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 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 * 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 metadata: Record<string, unknown>;\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 * Celestial-body ellipsoids and the planetary basemaps that pair with them.\n *\n * GeoLibre is Earth-centric by construction: MapLibre only renders Web Mercator\n * and all vector data is kept as WGS84 lon/lat GeoJSON. This module does *not*\n * change that — MapLibre still treats the planet as a unit sphere and there is\n * no true multi-CRS rendering (see maplibre-gl-js#168). What it adds is a\n * per-project notion of *which body* the coordinates describe, so that:\n *\n * - distance / area / scale measurements use that body's radius instead of a\n * hardcoded Earth radius, and\n * - Moon / Mars basemaps (published in a Web-Mercator tiling scheme) can be\n * selected like any other basemap.\n *\n * The active ellipsoid is a lightweight module-level singleton kept in sync with\n * the project's map preferences (see the store's `setPreferences`). Measurement\n * helpers read it lazily at call time, so callers in other packages don't need\n * the value threaded through their signatures — a deliberate prototype-friendly\n * shortcut over a full CRS/context refactor.\n */\n\n/** A biaxial (rotational) ellipsoid describing a celestial body. */\nexport interface Ellipsoid {\n /** Stable id persisted in the project (`map.ellipsoidId`). */\n id: string;\n /** Human-readable name shown in the UI. */\n name: string;\n /** Equatorial (semi-major) radius in metres — the \"web mercator\" radius. */\n semiMajorAxisMeters: number;\n /**\n * Inverse flattening `1/f`. `0` denotes a perfect sphere (Moon), in which case\n * the polar radius equals {@link semiMajorAxisMeters}.\n */\n inverseFlattening: number;\n}\n\n/**\n * Built-in ellipsoids. Earth is WGS 84; every other body uses IAU-adopted\n * figures. Only Earth and Mars are appreciably oblate; the rest are modelled as\n * spheres (`inverseFlattening: 0`), which is how the IAU/USGS simple-cylindrical\n * products treat them and is well within the accuracy MapLibre's unit-sphere\n * rendering can represent anyway.\n */\nexport const ELLIPSOIDS = [\n {\n id: \"earth\",\n name: \"Earth (WGS 84)\",\n semiMajorAxisMeters: 6378137,\n inverseFlattening: 298.257223563,\n },\n {\n id: \"moon\",\n name: \"Moon\",\n semiMajorAxisMeters: 1737400,\n inverseFlattening: 0,\n },\n {\n id: \"mars\",\n name: \"Mars (IAU 2000)\",\n semiMajorAxisMeters: 3396190,\n inverseFlattening: 169.894447,\n },\n {\n id: \"mercury\",\n name: \"Mercury\",\n semiMajorAxisMeters: 2439400,\n inverseFlattening: 0,\n },\n {\n id: \"venus\",\n name: \"Venus\",\n semiMajorAxisMeters: 6051800,\n inverseFlattening: 0,\n },\n {\n id: \"io\",\n name: \"Io\",\n semiMajorAxisMeters: 1821600,\n inverseFlattening: 0,\n },\n {\n id: \"europa\",\n name: \"Europa\",\n semiMajorAxisMeters: 1560800,\n inverseFlattening: 0,\n },\n {\n id: \"ganymede\",\n name: \"Ganymede\",\n semiMajorAxisMeters: 2631200,\n inverseFlattening: 0,\n },\n {\n id: \"callisto\",\n name: \"Callisto\",\n semiMajorAxisMeters: 2410300,\n inverseFlattening: 0,\n },\n {\n id: \"titan\",\n name: \"Titan\",\n semiMajorAxisMeters: 2574730,\n inverseFlattening: 0,\n },\n {\n id: \"pluto\",\n name: \"Pluto\",\n semiMajorAxisMeters: 1188300,\n inverseFlattening: 0,\n },\n {\n id: \"charon\",\n name: \"Charon\",\n semiMajorAxisMeters: 606000,\n inverseFlattening: 0,\n },\n] as const satisfies readonly Ellipsoid[];\n\nexport type EllipsoidId = (typeof ELLIPSOIDS)[number][\"id\"];\n\nexport const DEFAULT_ELLIPSOID_ID: EllipsoidId = \"earth\";\n\n/** Look an ellipsoid up by id, falling back to Earth for unknown ids. */\nexport function getEllipsoid(id: string | undefined): Ellipsoid {\n return (\n ELLIPSOIDS.find((e) => e.id === id) ?? ELLIPSOIDS.find((e) => e.id === DEFAULT_ELLIPSOID_ID)!\n );\n}\n\n/**\n * Mean radius `R = (2a + b) / 3` in metres, where `b` is the polar radius\n * derived from the inverse flattening. This is the radius used for spherical\n * (haversine) distance and area math.\n */\nexport function meanRadiusMeters(ellipsoid: Ellipsoid): number {\n const a = ellipsoid.semiMajorAxisMeters;\n if (!ellipsoid.inverseFlattening) return a;\n const f = 1 / ellipsoid.inverseFlattening;\n const b = a * (1 - f);\n return (2 * a + b) / 3;\n}\n\n/** Earth's mean radius in metres — the radius Turf.js and MapLibre bake in. */\nexport const EARTH_MEAN_RADIUS_METERS = meanRadiusMeters(ELLIPSOIDS.find((e) => e.id === \"earth\")!);\n\n// --- Active ellipsoid singleton -------------------------------------------\n\nlet activeEllipsoidId: EllipsoidId = DEFAULT_ELLIPSOID_ID;\n\n/**\n * Point the measurement helpers at a body. Unknown ids fall back to Earth so a\n * malformed project can never break measurements. Safe to call on every\n * preferences change; it is a cheap assignment.\n */\nexport function setActiveEllipsoidId(id: string | undefined): void {\n activeEllipsoidId = getEllipsoid(id).id as EllipsoidId;\n}\n\nexport function getActiveEllipsoid(): Ellipsoid {\n return getEllipsoid(activeEllipsoidId);\n}\n\n/** Mean radius (metres) of the active body — for haversine distance/area. */\nexport function getActiveMeanRadiusMeters(): number {\n return meanRadiusMeters(getActiveEllipsoid());\n}\n\n/** Semi-major axis (metres) of the active body — for its Web-Mercator scale. */\nexport function getActiveSemiMajorAxisMeters(): number {\n return getActiveEllipsoid().semiMajorAxisMeters;\n}\n\n// --- Earth-locked geodesy correction ---------------------------------------\n\n/**\n * The active body's mean radius as a fraction of Earth's — `R_body / R_earth`,\n * and exactly `1` on Earth.\n *\n * Everything GeoLibre measures ultimately comes from lon/lat *angles*, and the\n * only thing that turns an angle into a ground distance is the radius it is\n * multiplied by. Turf.js hardcodes Earth's, with no per-call override (that is\n * the \"Turf is Earth-locked\" half of GeoLibre#1128), so rather than patch or\n * replace Turf we post-scale what it returns and pre-scale what we hand it. The\n * result is exact for a sphere, which is how every non-Earth body GeoLibre\n * offers is modelled anyway, and within Mars' 0.6% flattening at worst.\n *\n * Suggested by @thareUSGS (USGS Astrogeology) on GeoLibre#1128 as the practical\n * workaround planetary web maps have long used on Earth-locked engines.\n */\nexport function getActiveBodyRadiusRatio(): number {\n return getActiveMeanRadiusMeters() / EARTH_MEAN_RADIUS_METERS;\n}\n\n/**\n * Convert a length Turf.js measured on Earth into the active body's true ground\n * length. A no-op on Earth.\n *\n * Use on anything Turf *returns* — a distance, a length, a perimeter.\n *\n * The correction is a dimensionless ratio, so it is unit-agnostic: pass the\n * length in whatever unit Turf gave it back (metres, kilometres, miles) and the\n * result is in that same unit. There is no hidden unit conversion here.\n */\nexport function earthLengthToBody(length: number): number {\n return length * getActiveBodyRadiusRatio();\n}\n\n/**\n * Convert a true ground length on the active body into the Earth-equivalent\n * length to hand Turf.js. A no-op on Earth.\n *\n * Use on anything passed *into* Turf as a distance — a buffer width, a circle\n * or sector radius, a search threshold — so the resulting geometry spans that\n * distance on this body rather than on Earth.\n *\n * Unit-agnostic like {@link earthLengthToBody}: pass the length in whatever\n * unit you are about to hand Turf alongside it, and the result is in that unit.\n */\nexport function bodyLengthToEarth(length: number): number {\n return length / getActiveBodyRadiusRatio();\n}\n\n/**\n * Convert an area Turf.js measured on Earth into the active body's true ground\n * area. A no-op on Earth.\n *\n * Areas scale with the *square* of the radius ratio, so this is not the same\n * correction as {@link earthLengthToBody}. Unit-agnostic in the same way, for\n * whatever squared unit the area is expressed in.\n */\nexport function earthAreaToBody(area: number): number {\n const ratio = getActiveBodyRadiusRatio();\n return area * ratio * ratio;\n}\n\n// --- Planetary basemaps ----------------------------------------------------\n\n/**\n * A raster basemap for a celestial body — the Moon and Mars mosaics, plus the\n * Earth satellite imagery the planet switcher pairs with Earth. The tiles are\n * XYZ (or TMS, see {@link scheme}) images in that body's Web-Mercator scheme, so\n * MapLibre renders them directly. The `styleUrl` is a `geolibre://basemap/<id>`\n * sentinel; the map controller expands it into a raster style at apply time (it\n * is not a fetchable URL).\n */\nexport interface PlanetaryBasemap {\n id: string;\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 * Tile row ordering. OpenPlanetaryMap's single-layer raster mosaics (the S3\n * datasets) are published as **TMS** (tile origin bottom-left, Y flipped),\n * whereas the CARTO `opmbuilder` named maps are standard **XYZ**. Omit for\n * XYZ; MapLibre defaults to `\"xyz\"` when `scheme` is 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 /** The body this basemap depicts, so selecting it can set the ellipsoid. */\n ellipsoidId: EllipsoidId;\n}\n\nexport const PLANETARY_BASEMAP_SENTINEL_PREFIX = \"geolibre://basemap/\";\n\nconst OPM_ATTRIBUTION = '<a href=\"https://www.openplanetary.org/opm\">OpenPlanetaryMap</a>';\n\n/** Data-source credit joined with the OpenPlanetaryMap attribution. */\nconst opmCredit = (source: string) => `${source} · ${OPM_ATTRIBUTION}`;\n\n// The GeoLibre tiles Worker (workers/tiles), which adds CORS to the OPM S3\n// mosaics that lack it. Its dataset keys mirror the DATASETS map in that Worker.\n// Tile path: `${TILE_PROXY_BASE}/<dataset>/{z}/{x}/{y}.png`.\nconst TILE_PROXY_BASE = \"https://tiles.geolibre.app/opm\";\n\n// The GeoLibre tiles Worker's reprojection endpoint, which warps the USGS\n// Astrogeology equirectangular WMS layers to Web Mercator so MapLibre can render\n// them (the USGS server offers no EPSG:3857 for these bodies). Its dataset keys\n// mirror the WMS_DATASETS map in that Worker (workers/tiles/src/index.ts).\n// Tile path: `${WMS_PROXY_BASE}/<dataset>/{z}/{x}/{y}.png` — standard XYZ.\nconst WMS_PROXY_BASE = \"https://tiles.geolibre.app/wms\";\n\n// USGS Astrogeology, the origin of every reprojected WMS basemap below.\nconst USGS_ASTRO_ATTRIBUTION = '<a href=\"https://astrogeology.usgs.gov/\">USGS Astrogeology</a>';\n\n/** Data-source credit joined with the USGS Astrogeology attribution. */\nconst usgsCredit = (source: string) => `${source} · ${USGS_ASTRO_ATTRIBUTION}`;\n\n// The OpenPlanetaryMap Mars and Moon basemaps\n// (https://openplanetarymap.org/basemaps/). Every one is a pre-rendered raster\n// served from a CDN — fast and reliable, unlike the USGS Astrogeology MapServer\n// these replace, which rendered every tile per request from a cgi-bin.\n//\n// Two tiling schemes are in play (see PlanetaryBasemap.scheme):\n// - The single-layer OPM mosaics (MOLA/Viking/hillshade/Moon albedo) are TMS.\n// - The multi-layer CARTO `opmbuilder` named maps are standard XYZ.\n// maxZoom is each source's probed max native zoom, so MapLibre overzooms (blurs)\n// rather than 404s.\n//\n// CORS: MapLibre GL fetches raster tiles with `fetch()`, which enforces CORS.\n// The CARTO `opmbuilder` named maps send `Access-Control-Allow-Origin: *`, so\n// they are referenced directly. The single-layer OPM mosaics are served from S3\n// buckets that send NO CORS header, so the browser blocks them — those go\n// through the GeoLibre tiles Worker (workers/tiles, tiles.geolibre.app), which\n// re-emits each tile with CORS and edge-caches it. See TILE_PROXY_BASE.\nexport const PLANETARY_BASEMAPS: readonly PlanetaryBasemap[] = [\n // --- Mars ---------------------------------------------------------------\n {\n id: \"mars-colour-mola-elevation\",\n name: \"Colour MOLA Elevation\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-colour-mola-elevation`,\n tileUrl: `${TILE_PROXY_BASE}/mars-mola-color-noshade/{z}/{x}/{y}.png`,\n scheme: \"tms\",\n maxZoom: 6,\n attribution: opmCredit(\"NASA / MOLA\"),\n ellipsoidId: \"mars\",\n },\n {\n id: \"mars-viking-mdim21\",\n name: \"Viking MDIM2.1\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-viking-mdim21`,\n tileUrl: `${TILE_PROXY_BASE}/mars-viking-mdim21/{z}/{x}/{y}.png`,\n scheme: \"tms\",\n maxZoom: 7,\n attribution: opmCredit(\"NASA / Viking / USGS\"),\n ellipsoidId: \"mars\",\n },\n {\n id: \"mars-hillshade\",\n name: \"Hillshade\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-hillshade`,\n tileUrl: `${TILE_PROXY_BASE}/mars-hillshade/{z}/{x}/{y}.png`,\n scheme: \"tms\",\n maxZoom: 6,\n attribution: opmCredit(\"NASA / MOLA\"),\n ellipsoidId: \"mars\",\n },\n {\n id: \"mars-basemap-v0-2\",\n name: \"OPM Basemap v0.2\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-basemap-v0-2`,\n tileUrl:\n \"https://cartocdn-gusc.global.ssl.fastly.net/opmbuilder/api/v1/map/named/opm-mars-basemap-v0-2/all/{z}/{x}/{y}.png\",\n maxZoom: 6,\n attribution: OPM_ATTRIBUTION,\n ellipsoidId: \"mars\",\n },\n {\n id: \"mars-shaded-colour-mola-elevation\",\n name: \"Shaded Colour MOLA Elevation\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-shaded-colour-mola-elevation`,\n tileUrl: `${TILE_PROXY_BASE}/mars-mola-color/{z}/{x}/{y}.png`,\n scheme: \"tms\",\n maxZoom: 6,\n attribution: opmCredit(\"NASA / MOLA\"),\n ellipsoidId: \"mars\",\n },\n {\n id: \"mars-shaded-grayscale-mola-elevation\",\n name: \"Shaded Grayscale MOLA Elevation\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mars-shaded-grayscale-mola-elevation`,\n tileUrl: `${TILE_PROXY_BASE}/mars-mola-gray/{z}/{x}/{y}.png`,\n scheme: \"tms\",\n maxZoom: 9,\n attribution: opmCredit(\"NASA / MOLA\"),\n ellipsoidId: \"mars\",\n },\n // --- The Moon -----------------------------------------------------------\n {\n id: \"moon-hillshaded-albedo\",\n name: \"Hillshaded Albedo\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}moon-hillshaded-albedo`,\n tileUrl: `${TILE_PROXY_BASE}/moon-hillshaded-albedo/{z}/{x}/{y}.png`,\n scheme: \"tms\",\n maxZoom: 6,\n attribution: opmCredit(\"NASA / LOLA / USGS\"),\n ellipsoidId: \"moon\",\n },\n {\n id: \"moon-basemap-v0-1\",\n name: \"OPM Basemap v0.1\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}moon-basemap-v0-1`,\n tileUrl:\n \"https://cartocdn-gusc.global.ssl.fastly.net/opmbuilder/api/v1/map/named/opm-moon-basemap-v0-1/all/{z}/{x}/{y}.png\",\n maxZoom: 6,\n attribution: OPM_ATTRIBUTION,\n ellipsoidId: \"moon\",\n },\n // --- Mercury ------------------------------------------------------------\n // USGS Astrogeology WMS layers reprojected to Web Mercator by the tiles\n // Worker (see WMS_PROXY_BASE). Standard XYZ, so no `scheme`.\n {\n id: \"mercury-messenger-color\",\n name: \"MESSENGER Colour Mosaic\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mercury-messenger-color`,\n tileUrl: `${WMS_PROXY_BASE}/mercury-messenger-color/{z}/{x}/{y}.png`,\n maxZoom: 7,\n attribution: usgsCredit(\"NASA / JHU APL / CIW\"),\n ellipsoidId: \"mercury\",\n },\n {\n id: \"mercury-messenger\",\n name: \"MESSENGER Basemap\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}mercury-messenger`,\n tileUrl: `${WMS_PROXY_BASE}/mercury-messenger/{z}/{x}/{y}.png`,\n maxZoom: 7,\n attribution: usgsCredit(\"NASA / JHU APL\"),\n ellipsoidId: \"mercury\",\n },\n // --- Venus --------------------------------------------------------------\n {\n id: \"venus-magellan\",\n name: \"Magellan FMAP\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}venus-magellan`,\n tileUrl: `${WMS_PROXY_BASE}/venus-magellan/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL\"),\n ellipsoidId: \"venus\",\n },\n {\n id: \"venus-magellan-color\",\n name: \"Magellan C3-MDIR Colour\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}venus-magellan-color`,\n tileUrl: `${WMS_PROXY_BASE}/venus-magellan-color/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL\"),\n ellipsoidId: \"venus\",\n },\n // --- Jupiter's Galilean moons -------------------------------------------\n {\n id: \"io-galileo-color\",\n name: \"Galileo SSI Colour\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}io-galileo-color`,\n tileUrl: `${WMS_PROXY_BASE}/io-galileo-color/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL\"),\n ellipsoidId: \"io\",\n },\n {\n id: \"europa-galileo-voyager\",\n name: \"Galileo / Voyager\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}europa-galileo-voyager`,\n tileUrl: `${WMS_PROXY_BASE}/europa-galileo-voyager/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL\"),\n ellipsoidId: \"europa\",\n },\n {\n id: \"ganymede-galileo-voyager\",\n name: \"Galileo / Voyager\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}ganymede-galileo-voyager`,\n tileUrl: `${WMS_PROXY_BASE}/ganymede-galileo-voyager/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL\"),\n ellipsoidId: \"ganymede\",\n },\n {\n id: \"callisto-galileo-voyager\",\n name: \"Galileo / Voyager\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}callisto-galileo-voyager`,\n tileUrl: `${WMS_PROXY_BASE}/callisto-galileo-voyager/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL\"),\n ellipsoidId: \"callisto\",\n },\n // --- Saturn's moon Titan ------------------------------------------------\n {\n id: \"titan-cassini\",\n name: \"Cassini ISS Mosaic\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}titan-cassini`,\n tileUrl: `${WMS_PROXY_BASE}/titan-cassini/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL / SSI\"),\n ellipsoidId: \"titan\",\n },\n {\n id: \"titan-hisar\",\n name: \"Cassini HiSAR Mosaic\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}titan-hisar`,\n tileUrl: `${WMS_PROXY_BASE}/titan-hisar/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JPL / USGS\"),\n ellipsoidId: \"titan\",\n },\n // --- Pluto & Charon -----------------------------------------------------\n {\n id: \"pluto-mosaic\",\n name: \"New Horizons Mosaic\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}pluto-mosaic`,\n tileUrl: `${WMS_PROXY_BASE}/pluto-mosaic/{z}/{x}/{y}.png`,\n maxZoom: 7,\n attribution: usgsCredit(\"NASA / JHU APL / SwRI\"),\n ellipsoidId: \"pluto\",\n },\n {\n id: \"pluto-color\",\n name: \"New Horizons Colour Shaded Relief\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}pluto-color`,\n tileUrl: `${WMS_PROXY_BASE}/pluto-color/{z}/{x}/{y}.png`,\n maxZoom: 7,\n attribution: usgsCredit(\"NASA / JHU APL / SwRI\"),\n ellipsoidId: \"pluto\",\n },\n {\n id: \"charon-mosaic\",\n name: \"New Horizons Mosaic\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}charon-mosaic`,\n tileUrl: `${WMS_PROXY_BASE}/charon-mosaic/{z}/{x}/{y}.png`,\n maxZoom: 6,\n attribution: usgsCredit(\"NASA / JHU APL / SwRI\"),\n ellipsoidId: \"charon\",\n },\n // --- Earth --------------------------------------------------------------\n // Satellite imagery the planet switcher pairs with Earth. Not shown in the\n // basemap picker's Moon/Mars sections (PLANETARY_BODY_ORDER excludes Earth) —\n // Earth basemaps live in the OpenFreeMap/Protomaps picker. USGS's National Map\n // ImageryOnly tiles are global Web-Mercator XYZ with open CORS.\n {\n id: \"earth-usgs-imagery\",\n name: \"USGS Imagery\",\n styleUrl: `${PLANETARY_BASEMAP_SENTINEL_PREFIX}earth-usgs-imagery`,\n tileUrl:\n \"https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}\",\n maxZoom: 16,\n attribution:\n '<a href=\"https://www.usgs.gov/programs/national-geospatial-program/national-map\">USGS The National Map</a>',\n ellipsoidId: \"earth\",\n },\n] as const;\n\n/**\n * The section a planetary basemap is grouped under in the New Project and\n * Change Basemap pickers. The Moon and Mars keep dedicated sections (they are\n * the most-used bodies and each has several mosaics); every other body is\n * collapsed into a single \"other\" section so the picker isn't a wall of\n * one-item headings.\n */\nexport type PlanetaryBasemapSectionId = \"moon\" | \"mars\" | \"other\";\n\n/** A picker section paired with the planetary basemaps it lists. */\nexport interface PlanetaryBasemapGroup {\n id: PlanetaryBasemapSectionId;\n basemaps: readonly PlanetaryBasemap[];\n}\n\n// Bodies that get their own picker section. Earth is excluded entirely (its\n// basemaps live in the OpenFreeMap/Protomaps picker); everything else lands in\n// \"other\".\nconst DEDICATED_SECTION_BODIES: readonly EllipsoidId[] = [\"moon\", \"mars\"];\n\n/**\n * {@link PLANETARY_BASEMAPS} grouped for the pickers: one section each for the\n * Moon and Mars, then a single \"Other celestial bodies\" section for the rest,\n * in {@link PLANETARY_BASEMAPS} order (grouped by body). Empty sections are\n * dropped.\n */\nexport const PLANETARY_BASEMAP_GROUPS: readonly PlanetaryBasemapGroup[] = (\n [\n { id: \"moon\", ellipsoidId: \"moon\" },\n { id: \"mars\", ellipsoidId: \"mars\" },\n { id: \"other\", ellipsoidId: null },\n ] as const\n)\n .map(({ id, ellipsoidId }) => {\n const basemaps = PLANETARY_BASEMAPS.filter((b) =>\n ellipsoidId\n ? b.ellipsoidId === ellipsoidId\n : b.ellipsoidId !== \"earth\" && !DEDICATED_SECTION_BODIES.includes(b.ellipsoidId),\n );\n // The combined \"other\" section spans many bodies, so order it alphabetically\n // by body name; a stable sort keeps each body's basemaps grouped together.\n return {\n id,\n basemaps:\n id === \"other\"\n ? [...basemaps].sort((a, b) =>\n getEllipsoid(a.ellipsoidId).name.localeCompare(getEllipsoid(b.ellipsoidId).name),\n )\n : basemaps,\n };\n })\n .filter((group) => group.basemaps.length > 0);\n\n// Basemap ids the OpenPlanetaryMap migration renamed or dropped, mapped to the\n// nearest current basemap. Lets a project saved before the migration keep a\n// planetary basemap (instead of falling back to Earth) the next time it opens:\n// the two OPM composites kept their exact tiles under new ids, Viking maps to\n// the equivalent OPM mosaic, and the retired LRO imagery maps to the flagship\n// Moon basemap.\nconst LEGACY_PLANETARY_BASEMAP_IDS: Record<string, string> = {\n \"mars-opm\": \"mars-basemap-v0-2\",\n \"moon-opm\": \"moon-basemap-v0-1\",\n \"mars-viking\": \"mars-viking-mdim21\",\n \"moon-lroc\": \"moon-basemap-v0-1\",\n};\n\n/** Resolve a `geolibre://basemap/<id>` sentinel to its planetary basemap. */\nexport function getPlanetaryBasemapByStyleUrl(\n styleUrl: string | undefined,\n): PlanetaryBasemap | undefined {\n if (!styleUrl?.startsWith(PLANETARY_BASEMAP_SENTINEL_PREFIX)) return undefined;\n const id = styleUrl.slice(PLANETARY_BASEMAP_SENTINEL_PREFIX.length);\n const resolvedId = Object.prototype.hasOwnProperty.call(LEGACY_PLANETARY_BASEMAP_IDS, id)\n ? LEGACY_PLANETARY_BASEMAP_IDS[id]\n : id;\n return PLANETARY_BASEMAPS.find((b) => b.id === resolvedId);\n}\n\n/** Look a planetary basemap up by its stable id. */\nexport function getPlanetaryBasemapById(id: string): PlanetaryBasemap | undefined {\n return PLANETARY_BASEMAPS.find((b) => b.id === id);\n}\n\n/**\n * The celestial bodies offered by the Layers-panel planet switcher, in menu\n * order (like Google Earth's planet dropdown). Each body is paired with the\n * basemap the switcher applies for it — USGS satellite imagery for Earth, the\n * OpenPlanetaryMap Hillshaded Albedo mosaic for the Moon, and the OpenPlanetaryMap\n * Viking mosaic for Mars.\n */\nexport const PLANET_SWITCHER_OPTIONS = [\n // The three most-used bodies lead; the rest follow alphabetically.\n { ellipsoidId: \"earth\", basemapId: \"earth-usgs-imagery\" },\n { ellipsoidId: \"moon\", basemapId: \"moon-hillshaded-albedo\" },\n { ellipsoidId: \"mars\", basemapId: \"mars-viking-mdim21\" },\n { ellipsoidId: \"callisto\", basemapId: \"callisto-galileo-voyager\" },\n { ellipsoidId: \"charon\", basemapId: \"charon-mosaic\" },\n { ellipsoidId: \"europa\", basemapId: \"europa-galileo-voyager\" },\n { ellipsoidId: \"ganymede\", basemapId: \"ganymede-galileo-voyager\" },\n { ellipsoidId: \"io\", basemapId: \"io-galileo-color\" },\n { ellipsoidId: \"mercury\", basemapId: \"mercury-messenger-color\" },\n { ellipsoidId: \"pluto\", basemapId: \"pluto-mosaic\" },\n { ellipsoidId: \"titan\", basemapId: \"titan-cassini\" },\n { ellipsoidId: \"venus\", basemapId: \"venus-magellan-color\" },\n] as const satisfies readonly {\n ellipsoidId: EllipsoidId;\n basemapId: string;\n}[];\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 = '© <a href=\"https://www.amap.com\">高德地图 Amap</a>';\nconst TENCENT_ATTRIBUTION = '© <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","import type { Feature, FeatureCollection, GeoJSON, Geometry, Position } from \"geojson\";\n\n/**\n * Helpers for vector data whose coordinates carry a Z value (elevation), e.g.\n * GPX tracks with `<ele>` readings or LineStringZ/PointZ geometries. MapLibre's\n * 2D style layers ignore the third coordinate, so layers that want to render\n * their Z values use these helpers to detect and rescale elevations before\n * handing the data to a deck.gl overlay.\n */\n\n// Proving the *negative* (no Z anywhere) walks every coordinate, so cache the\n// verdict per GeoJSON object — the map sync, the deck overlay, and the Style\n// panel all ask the same question about the same (immutable) data.\nconst hasZCache = new WeakMap<object, boolean>();\n\n/**\n * Returns true when any coordinate in the GeoJSON carries a finite, non-zero\n * Z value. All-zero Z arrays are treated as flat data because rendering them\n * \"in 3D\" would be indistinguishable from the 2D result. The result is\n * cached per GeoJSON object, so repeated calls on the same data are free.\n *\n * @param geojson - Any GeoJSON object (geometry, feature, or collection).\n */\nexport function geojsonHasZCoordinates(geojson: GeoJSON | null | undefined): boolean {\n if (!geojson) return false;\n const cached = hasZCache.get(geojson);\n if (cached !== undefined) return cached;\n const result = someGeometry(geojson, (geometry) =>\n somePosition(geometry, (position) => {\n const z = position[2];\n return typeof z === \"number\" && Number.isFinite(z) && z !== 0;\n }),\n );\n hasZCache.set(geojson, result);\n return result;\n}\n\n/**\n * Returns a copy of the GeoJSON with every coordinate's Z value mapped to\n * `z * verticalScale + offset` (missing or non-finite Z treated as 0). Used\n * to apply vertical exaggeration and a constant altitude offset before 3D\n * rendering. The identity transform (`verticalScale === 1 && offset === 0`)\n * still produces a copy so the non-finite-Z sanitization applies on every\n * path — WebGL does not handle NaN positions gracefully.\n *\n * @param geojson - The feature collection to transform.\n * @param verticalScale - Multiplier applied to each Z value.\n * @param offset - Constant altitude in meters added after scaling.\n */\nexport function transformGeojsonElevation(\n geojson: FeatureCollection,\n verticalScale: number,\n offset: number,\n): FeatureCollection {\n // Positions are normalized to [x, y, z]; a 4th measure value (M), which\n // some producers emit, is intentionally dropped — nothing downstream of\n // this render path consumes it.\n const mapPosition = (position: Position): Position => [\n position[0],\n position[1],\n (typeof position[2] === \"number\" && Number.isFinite(position[2]) ? position[2] : 0) *\n verticalScale +\n offset,\n ];\n return {\n ...geojson,\n features: geojson.features.map((feature) => ({\n ...feature,\n geometry: mapGeometryPositions(feature.geometry, mapPosition),\n })),\n };\n}\n\nfunction someGeometry(geojson: GeoJSON, predicate: (geometry: Geometry) => boolean): boolean {\n switch (geojson.type) {\n case \"FeatureCollection\":\n return geojson.features.some((feature) =>\n feature.geometry ? someGeometry(feature.geometry, predicate) : false,\n );\n case \"Feature\":\n return geojson.geometry ? someGeometry(geojson.geometry, predicate) : false;\n case \"GeometryCollection\":\n return geojson.geometries.some((geometry) => someGeometry(geometry, predicate));\n default:\n return predicate(geojson);\n }\n}\n\nfunction somePosition(geometry: Geometry, predicate: (position: Position) => boolean): boolean {\n switch (geometry.type) {\n case \"Point\":\n return predicate(geometry.coordinates);\n case \"MultiPoint\":\n case \"LineString\":\n return geometry.coordinates.some(predicate);\n case \"MultiLineString\":\n case \"Polygon\":\n return geometry.coordinates.some((ring) => ring.some(predicate));\n case \"MultiPolygon\":\n return geometry.coordinates.some((polygon) => polygon.some((ring) => ring.some(predicate)));\n case \"GeometryCollection\":\n return geometry.geometries.some((child) => somePosition(child, predicate));\n default:\n return false;\n }\n}\n\nfunction mapGeometryPositions(\n geometry: Feature[\"geometry\"],\n mapPosition: (position: Position) => Position,\n): Feature[\"geometry\"] {\n if (!geometry) return geometry;\n switch (geometry.type) {\n case \"Point\":\n return { ...geometry, coordinates: mapPosition(geometry.coordinates) };\n case \"MultiPoint\":\n case \"LineString\":\n return { ...geometry, coordinates: geometry.coordinates.map(mapPosition) };\n case \"MultiLineString\":\n case \"Polygon\":\n return {\n ...geometry,\n coordinates: geometry.coordinates.map((ring) => ring.map(mapPosition)),\n };\n case \"MultiPolygon\":\n return {\n ...geometry,\n coordinates: geometry.coordinates.map((polygon) =>\n polygon.map((ring) => ring.map(mapPosition)),\n ),\n };\n case \"GeometryCollection\":\n return {\n ...geometry,\n geometries: geometry.geometries.map(\n (child) => mapGeometryPositions(child, mapPosition) as Geometry,\n ),\n };\n default:\n return geometry;\n }\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","import type {\n Feature,\n FeatureCollection,\n GeoJsonProperties,\n LineString,\n MultiLineString,\n} from \"geojson\";\n\n/**\n * Pure TypeScript Google Encoded Polyline algorithm codec.\n *\n * Supports arbitrary precision:\n * - precision = 5: Google Maps standard, OSRM, HERE (factor 1e5, ~1.1m precision)\n * - precision = 6: Valhalla, Mapbox, Open Source Routing Machine 6 (factor 1e6, ~0.11m precision)\n *\n * Coordinates are represented as `[lon, lat]` pairs in GeoJSON format.\n */\n\n/**\n * Unescapes backslashes in a polyline string.\n *\n * When polyline strings are copied from JSON payloads, log files, or source code,\n * backslashes are frequently double-escaped as `\\\\` or formatted with escape sequences.\n *\n * @param str - Input string with potentially escaped backslashes.\n * @returns Cleaned polyline string.\n */\nexport function unescapePolyline(str: string): string {\n if (!str || typeof str !== \"string\") return \"\";\n return str.replace(/\\\\\\\\/g, \"\\\\\").replace(/\\\\\"/g, '\"').replace(/\\\\'/g, \"'\");\n}\n\nexport interface PolylineDecodeResult {\n coordinates: [number, number][];\n complete: boolean;\n}\n\n/**\n * Decodes an encoded polyline string into an array of `[lon, lat]` coordinates,\n * along with a `complete` flag indicating whether the entire string was consumed\n * as complete coordinate pairs without truncation or malformed characters.\n *\n * @param encoded - The ASCII-encoded polyline string.\n * @param precision - Number of decimal digits (default: 5).\n * @param unescape - Whether to unescape double-escaped backslashes before decoding (default: false).\n * @returns Object with decoded `coordinates` and `complete` boolean status.\n */\nexport function decodePolylineDetailed(\n encoded: string,\n precision = 5,\n unescape = false,\n): PolylineDecodeResult {\n if (!encoded || typeof encoded !== \"string\") {\n return { coordinates: [], complete: true };\n }\n const cleanEncoded = unescape ? unescapePolyline(encoded) : encoded;\n const factor = 10 ** precision;\n const len = cleanEncoded.length;\n const coordinates: [number, number][] = [];\n let index = 0;\n let lat = 0;\n let lon = 0;\n\n while (index < len) {\n let shift = 0;\n let result = 0;\n let byte: number;\n\n // Decode latitude delta\n let latComplete = false;\n do {\n if (index >= len) return { coordinates, complete: false };\n byte = cleanEncoded.charCodeAt(index++) - 63;\n if (byte < 0 || byte > 63) return { coordinates, complete: false };\n if (shift > 30 || (shift === 30 && (byte & 0x1f) > 0x03)) {\n return { coordinates, complete: false };\n }\n result |= (byte & 0x1f) << shift;\n shift += 5;\n if (byte < 0x20) {\n latComplete = true;\n break;\n }\n } while (byte >= 0x20);\n\n if (!latComplete) return { coordinates, complete: false };\n\n // Unsigned right shift (>>>) for 32-bit safe zigzag decoding\n lat += result & 1 ? ~(result >>> 1) : result >>> 1;\n\n shift = 0;\n result = 0;\n\n // Decode longitude delta\n let lonComplete = false;\n do {\n if (index >= len) return { coordinates, complete: false };\n byte = cleanEncoded.charCodeAt(index++) - 63;\n if (byte < 0 || byte > 63) return { coordinates, complete: false };\n if (shift > 30 || (shift === 30 && (byte & 0x1f) > 0x03)) {\n return { coordinates, complete: false };\n }\n result |= (byte & 0x1f) << shift;\n shift += 5;\n if (byte < 0x20) {\n lonComplete = true;\n break;\n }\n } while (byte >= 0x20);\n\n if (!lonComplete) return { coordinates, complete: false };\n\n lon += result & 1 ? ~(result >>> 1) : result >>> 1;\n\n coordinates.push([lon / factor, lat / factor]);\n }\n\n return { coordinates, complete: true };\n}\n\n/**\n * Decodes an encoded polyline string into an array of `[lon, lat]` coordinates.\n *\n * @param encoded - The ASCII-encoded polyline string.\n * @param precision - Number of decimal digits (default: 5).\n * @param unescape - Whether to unescape double-escaped backslashes before decoding (default: false).\n * @returns Array of `[lon, lat]` coordinate pairs in GeoJSON order.\n */\nexport function decodePolyline(\n encoded: string,\n precision = 5,\n unescape = false,\n): [number, number][] {\n return decodePolylineDetailed(encoded, precision, unescape).coordinates;\n}\n\n/**\n * Encodes a single integer delta value into polyline character chunks.\n */\nfunction encodeSignedNumber(num: number): string {\n let sgnNum = num < 0 ? ~(num << 1) : num << 1;\n let out = \"\";\n while (sgnNum >= 0x20) {\n out += String.fromCharCode((0x20 | (sgnNum & 0x1f)) + 63);\n sgnNum >>>= 5;\n }\n out += String.fromCharCode(sgnNum + 63);\n return out;\n}\n\n/**\n * Encodes an array of `[lon, lat]` coordinates into a Google Encoded Polyline string.\n *\n * @param coordinates - Array of `[lon, lat]` coordinate pairs.\n * @param precision - Number of decimal digits (default: 5).\n * @returns The encoded polyline string.\n */\nexport function encodePolyline(coordinates: [number, number][], precision = 5): string {\n if (!Array.isArray(coordinates) || coordinates.length === 0) return \"\";\n const factor = 10 ** precision;\n let output = \"\";\n let prevLat = 0;\n let prevLon = 0;\n\n for (const [lon, lat] of coordinates) {\n if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;\n const roundLat = Math.round(lat * factor);\n const roundLon = Math.round(lon * factor);\n\n const deltaLat = roundLat - prevLat;\n const deltaLon = roundLon - prevLon;\n\n output += encodeSignedNumber(deltaLat);\n output += encodeSignedNumber(deltaLon);\n\n prevLat = roundLat;\n prevLon = roundLon;\n }\n\n return output;\n}\n\n/**\n * Decodes an encoded polyline string into a GeoJSON `Feature<LineString>`.\n *\n * @param encoded - The polyline string.\n * @param precision - Coordinate precision (default: 5).\n * @param properties - Optional feature properties.\n * @returns A GeoJSON LineString Feature.\n */\nexport function polylineStrToGeoJSON(\n encoded: string,\n precision = 5,\n properties: GeoJsonProperties = {},\n): Feature<LineString> {\n return {\n type: \"Feature\",\n properties: { ...properties },\n geometry: {\n type: \"LineString\",\n coordinates: decodePolyline(encoded, precision),\n },\n };\n}\n\n/**\n * Encodes a GeoJSON LineString, MultiLineString, Feature, or FeatureCollection into encoded polyline string(s).\n *\n * @param geojson - The GeoJSON input geometry or feature.\n * @param precision - Coordinate precision (default: 5).\n * @returns An encoded polyline string for LineString, or an array of strings for MultiLineString / FeatureCollection.\n */\nexport function geoJSONToPolylineStr(\n geojson:\n | LineString\n | MultiLineString\n | Feature<LineString | MultiLineString>\n | FeatureCollection<LineString | MultiLineString>,\n precision = 5,\n): string | string[] {\n if (!geojson) return \"\";\n\n // Handle Feature wrapper\n if (geojson.type === \"Feature\") {\n return geoJSONToPolylineStr(geojson.geometry, precision);\n }\n\n // Handle FeatureCollection\n if (geojson.type === \"FeatureCollection\") {\n const results: string[] = [];\n for (const feature of geojson.features) {\n const res = geoJSONToPolylineStr(feature.geometry, precision);\n if (Array.isArray(res)) results.push(...res);\n else if (res) results.push(res);\n }\n return results;\n }\n\n // Handle LineString\n if (geojson.type === \"LineString\") {\n return encodePolyline(geojson.coordinates as [number, number][], precision);\n }\n\n // Handle MultiLineString\n if (geojson.type === \"MultiLineString\") {\n return geojson.coordinates.map((lineCoords) =>\n encodePolyline(lineCoords as [number, number][], precision),\n );\n }\n\n return \"\";\n}\n\nexport interface PolylineBatchOptions {\n /** Coordinate precision (default: 5) */\n precision?: number;\n /** Whether to unescape `\\\\` backslashes (default: true) */\n unescape?: boolean;\n /** Custom delimiter string or RegExp separating individual polylines (default: newline) */\n delimiter?: string | RegExp;\n /** Whether to merge lines into a single MultiLineString feature (default: false) */\n asMultiLine?: boolean;\n /** Optional base properties to assign to output features */\n baseProperties?: GeoJsonProperties;\n}\n\n/**\n * Parses and batch-decodes multiple polylines from a text block or file content.\n *\n * @param input - Multiline string or delimited text containing encoded polylines.\n * @param options - Decoding and parsing configuration.\n * @returns A FeatureCollection containing LineString (or MultiLineString) features.\n */\nexport function batchDecodePolylines(\n input: string,\n options: PolylineBatchOptions = {},\n): FeatureCollection<LineString | MultiLineString> {\n if (!input || typeof input !== \"string\") {\n return { type: \"FeatureCollection\", features: [] };\n }\n\n const {\n precision = 5,\n unescape = true,\n delimiter = /\\r?\\n/,\n asMultiLine = false,\n baseProperties = {},\n } = options;\n\n const rawChunks = input.split(delimiter);\n const validLineCoords: [number, number][][] = [];\n const features: Feature<LineString>[] = [];\n\n for (let i = 0; i < rawChunks.length; i++) {\n const raw = rawChunks[i].trim();\n if (!raw) continue;\n // Strip optional surrounding quotes (e.g. from CSV or JSON extract)\n const cleaned = raw.replace(/^[\"']|[\"']$/g, \"\").trim();\n if (!cleaned) continue;\n\n const decodeResult = decodePolylineDetailed(cleaned, precision, unescape);\n if (decodeResult.complete && decodeResult.coordinates.length >= 2) {\n const coords = decodeResult.coordinates;\n validLineCoords.push(coords);\n features.push({\n type: \"Feature\",\n properties: {\n ...baseProperties,\n line_index: features.length + 1,\n point_count: coords.length,\n },\n geometry: {\n type: \"LineString\",\n coordinates: coords,\n },\n });\n }\n }\n\n if (asMultiLine && validLineCoords.length > 0) {\n const multiFeature: Feature<MultiLineString> = {\n type: \"Feature\",\n properties: {\n ...baseProperties,\n line_count: validLineCoords.length,\n point_count: validLineCoords.reduce((acc, c) => acc + c.length, 0),\n },\n geometry: {\n type: \"MultiLineString\",\n coordinates: validLineCoords,\n },\n };\n return {\n type: \"FeatureCollection\",\n features: [multiFeature],\n };\n }\n\n return {\n type: \"FeatureCollection\",\n features,\n };\n}\n","/**\n * Resolves runtime environment variables shared by the external-service clients\n * (geocoding, routing). Build-time Vite vars (`import.meta.env`) 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\nconst buildEnv = (\n import.meta as ImportMeta & {\n env?: Record<string, string | undefined>;\n }\n).env;\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 Cesium Ion access token from the runtime environment.\n *\n * The 3D-globe view's world imagery and terrain 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). When unset, the globe cannot\n * render and the 3D view is not offered.\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 type { Feature } from \"geojson\";\n\n/**\n * How a freshly matched feature set combines with the existing selection,\n * mirroring QGIS's four selection behaviors: replace the selection, add to\n * it, remove from it, or keep only features in both.\n */\nexport type SelectionMode = \"new\" | \"add\" | \"remove\" | \"intersect\";\n\n/** Selection modes in display order. */\nexport const SELECTION_MODES: readonly SelectionMode[] = [\"new\", \"add\", \"remove\", \"intersect\"];\n\n/**\n * The id under which a feature participates in the selection model. Mirrors\n * the convention shared by the attribute table and the map highlight\n * (`String(feature.id ?? index)`): the feature's own `id` when present,\n * otherwise its index in the layer's feature array.\n */\nexport function featureSelectionId(feature: Feature, index: number): string {\n return String(feature.id ?? index);\n}\n\n/**\n * Combines the current selection with a freshly matched id set according to\n * `mode`. The result is deduplicated and order-stable: \"new\" follows the\n * matched order, \"add\" appends unseen matches after the current ids, and\n * \"remove\"/\"intersect\" preserve the current selection's order.\n */\nexport function applySelectionMode(\n current: readonly string[],\n matched: readonly string[],\n mode: SelectionMode,\n): string[] {\n const matchedSet = new Set(matched);\n switch (mode) {\n case \"new\":\n return Array.from(matchedSet);\n case \"add\": {\n const merged = new Set(current);\n for (const id of matched) merged.add(id);\n return Array.from(merged);\n }\n case \"remove\":\n return Array.from(new Set(current.filter((id) => !matchedSet.has(id))));\n case \"intersect\":\n return Array.from(new Set(current.filter((id) => matchedSet.has(id))));\n }\n}\n\n/**\n * The complement of `current` within `allIds` (QGIS \"Invert selection\"):\n * every feature id not currently selected, in layer order. With an empty\n * selection this selects everything.\n */\nexport function invertSelection(allIds: readonly string[], current: readonly string[]): string[] {\n const selected = new Set(current);\n return allIds.filter((id) => !selected.has(id));\n}\n","import { Color, createExpression } from \"@maplibre/maplibre-gl-style-spec\";\nimport type { StyleExpression } from \"@maplibre/maplibre-gl-style-spec\";\nimport type { Feature } from \"geojson\";\nimport { featureSelectionId } from \"./selection\";\n\n/**\n * Shared Expression Builder logic (GH #1306): a curated MapLibre expression\n * function reference, real parse/type validation via the MapLibre style spec,\n * live preview evaluation against a sample feature, `@`-style variable\n * substitution, and attribute field type inference. The UI dialog that\n * consumes this lives in the desktop app; keeping the logic here lets every\n * expression entry point (rule filters, expression style mode, labels, and\n * later the field calculator and atlas) share one implementation.\n */\n\n/** One documented function/operator the builder can insert. */\nexport interface ExpressionFunctionDoc {\n /** The MapLibre operator name, e.g. `get` or `==`. */\n name: string;\n /** The snippet inserted into the expression text (valid JSON). */\n snippet: string;\n /**\n * i18n key suffix (under `style.expressionBuilder.functions`) for the\n * one-line description. Kept as a suffix so this package stays free of\n * i18next; the app resolves it with `t()`.\n */\n docKey: string;\n}\n\n/** A group of related functions shown as one section of the reference tree. */\nexport interface ExpressionFunctionCategory {\n /** Stable key, also the i18n suffix under `style.expressionBuilder.categories`. */\n key: string;\n functions: ExpressionFunctionDoc[];\n}\n\nconst fn = (name: string, snippet: string, docKey: string): ExpressionFunctionDoc => ({\n name,\n snippet,\n docKey,\n});\n\n/**\n * The curated function reference, grouped by category. Snippets are valid,\n * standalone-compilable MapLibre expressions (enforced by tests) so inserting\n * one into an empty editor always yields a working starting point.\n */\nexport const EXPRESSION_FUNCTION_CATEGORIES: ExpressionFunctionCategory[] = [\n {\n key: \"data\",\n functions: [\n fn(\"get\", '[\"get\", \"field\"]', \"get\"),\n fn(\"has\", '[\"has\", \"field\"]', \"has\"),\n fn(\"id\", '[\"id\"]', \"id\"),\n fn(\"properties\", '[\"properties\"]', \"properties\"),\n fn(\"geometry-type\", '[\"geometry-type\"]', \"geometryType\"),\n fn(\"at\", '[\"at\", 0, [\"literal\", [1, 2, 3]]]', \"at\"),\n fn(\"in\", '[\"in\", \"value\", [\"get\", \"field\"]]', \"in\"),\n fn(\"index-of\", '[\"index-of\", \"value\", [\"get\", \"field\"]]', \"indexOf\"),\n fn(\"length\", '[\"length\", [\"get\", \"field\"]]', \"length\"),\n fn(\"literal\", '[\"literal\", [\"a\", \"b\"]]', \"literal\"),\n fn(\"typeof\", '[\"typeof\", [\"get\", \"field\"]]', \"typeofOp\"),\n ],\n },\n {\n key: \"decision\",\n functions: [\n fn(\"case\", '[\"case\", [\"==\", [\"get\", \"field\"], \"value\"], \"#ff0000\", \"#cccccc\"]', \"caseOp\"),\n fn(\n \"match\",\n '[\"match\", [\"get\", \"field\"], \"a\", \"#2563eb\", \"b\", \"#16a34a\", \"#94a3b8\"]',\n \"match\",\n ),\n fn(\"coalesce\", '[\"coalesce\", [\"get\", \"field\"], \"fallback\"]', \"coalesce\"),\n fn(\"==\", '[\"==\", [\"get\", \"field\"], \"value\"]', \"eq\"),\n fn(\"!=\", '[\"!=\", [\"get\", \"field\"], \"value\"]', \"neq\"),\n fn(\">\", '[\">\", [\"get\", \"field\"], 0]', \"gt\"),\n fn(\">=\", '[\">=\", [\"get\", \"field\"], 0]', \"gte\"),\n fn(\"<\", '[\"<\", [\"get\", \"field\"], 0]', \"lt\"),\n fn(\"<=\", '[\"<=\", [\"get\", \"field\"], 0]', \"lte\"),\n fn(\"all\", '[\"all\", [\">\", [\"get\", \"field\"], 0], [\"<\", [\"get\", \"field\"], 100]]', \"all\"),\n fn(\"any\", '[\"any\", [\"==\", [\"get\", \"field\"], \"a\"], [\"==\", [\"get\", \"field\"], \"b\"]]', \"any\"),\n fn(\"!\", '[\"!\", [\"has\", \"field\"]]', \"not\"),\n ],\n },\n {\n key: \"math\",\n functions: [\n fn(\"+\", '[\"+\", [\"get\", \"field\"], 1]', \"add\"),\n fn(\"-\", '[\"-\", [\"get\", \"field\"], 1]', \"subtract\"),\n fn(\"*\", '[\"*\", [\"get\", \"field\"], 2]', \"multiply\"),\n fn(\"/\", '[\"/\", [\"get\", \"field\"], 2]', \"divide\"),\n fn(\"%\", '[\"%\", [\"get\", \"field\"], 2]', \"modulo\"),\n fn(\"^\", '[\"^\", [\"get\", \"field\"], 2]', \"power\"),\n fn(\"abs\", '[\"abs\", [\"get\", \"field\"]]', \"abs\"),\n fn(\"ceil\", '[\"ceil\", [\"get\", \"field\"]]', \"ceil\"),\n fn(\"floor\", '[\"floor\", [\"get\", \"field\"]]', \"floor\"),\n fn(\"round\", '[\"round\", [\"get\", \"field\"]]', \"round\"),\n fn(\"sqrt\", '[\"sqrt\", [\"get\", \"field\"]]', \"sqrt\"),\n fn(\"ln\", '[\"ln\", [\"get\", \"field\"]]', \"ln\"),\n fn(\"log10\", '[\"log10\", [\"get\", \"field\"]]', \"log10\"),\n fn(\"min\", '[\"min\", [\"get\", \"field\"], 0]', \"min\"),\n fn(\"max\", '[\"max\", [\"get\", \"field\"], 0]', \"max\"),\n ],\n },\n {\n key: \"string\",\n functions: [\n fn(\"concat\", '[\"concat\", [\"get\", \"field\"], \" suffix\"]', \"concat\"),\n fn(\"upcase\", '[\"upcase\", [\"get\", \"field\"]]', \"upcase\"),\n fn(\"downcase\", '[\"downcase\", [\"get\", \"field\"]]', \"downcase\"),\n fn(\"slice\", '[\"slice\", [\"get\", \"field\"], 0, 3]', \"slice\"),\n ],\n },\n {\n key: \"conversion\",\n functions: [\n fn(\"to-number\", '[\"to-number\", [\"get\", \"field\"]]', \"toNumber\"),\n fn(\"to-string\", '[\"to-string\", [\"get\", \"field\"]]', \"toString\"),\n fn(\"to-boolean\", '[\"to-boolean\", [\"get\", \"field\"]]', \"toBoolean\"),\n fn(\"to-color\", '[\"to-color\", \"#2563eb\"]', \"toColor\"),\n ],\n },\n {\n key: \"camera\",\n functions: [\n fn(\"zoom\", '[\"zoom\"]', \"zoom\"),\n fn(\n \"interpolate\",\n '[\"interpolate\", [\"linear\"], [\"zoom\"], 5, [\"to-color\", \"#2563eb\"], 12, [\"to-color\", \"#dc2626\"]]',\n \"interpolate\",\n ),\n fn(\"step\", '[\"step\", [\"zoom\"], \"#2563eb\", 10, \"#dc2626\"]', \"step\"),\n fn(\"rgb\", '[\"rgb\", 37, 99, 235]', \"rgb\"),\n fn(\"rgba\", '[\"rgba\", 37, 99, 235, 0.5]', \"rgba\"),\n ],\n },\n];\n\n/**\n * Strips commas that directly precede a closing bracket/brace outside of\n * strings, so user-typed expressions with trailing commas still parse.\n * Shared by {@link parseJsonExpression} (vector-color) and the builder.\n */\nexport function removeTrailingJsonCommas(value: string): string {\n let result = \"\";\n let inString = false;\n let escaped = false;\n\n for (let index = 0; index < value.length; index += 1) {\n const char = value[index];\n\n if (inString) {\n result += char;\n if (escaped) {\n escaped = false;\n } else if (char === \"\\\\\") {\n escaped = true;\n } else if (char === '\"') {\n inString = false;\n }\n continue;\n }\n\n if (char === '\"') {\n inString = true;\n result += char;\n continue;\n }\n\n if (char === \",\") {\n const nextSignificant = value.slice(index + 1).match(/\\S/)?.[0];\n if (nextSignificant === \"]\" || nextSignificant === \"}\") continue;\n }\n\n result += char;\n }\n\n return result;\n}\n\n/** Result types the builder can require of a whole expression. */\nexport type ExpressionExpectedType = \"boolean\" | \"color\" | \"string\" | \"number\";\n\n/** Options for {@link validateMapExpression}. */\nexport interface ValidateMapExpressionOptions {\n /**\n * Variables substituted before compiling, so a numeric `@token` used in a\n * numeric slot type-checks against its value instead of the raw string.\n */\n variables?: ExpressionVariable[];\n /**\n * When set, the style spec type-checks the expression's result against\n * this type (e.g. a filter must produce a boolean), rejecting expressions\n * that compile but cannot fit the destination.\n */\n expectedType?: ExpressionExpectedType;\n}\n\n/**\n * The `rootKey` `createExpression` (style-spec v26+) requires as its second\n * argument — a location label it only surfaces when attaching a runtime\n * warning to `console.warn` during evaluation. These expressions come from the\n * Expression Builder dialog, not a style JSON layer, so a generic label is the\n * honest value; it never appears in the parsing errors we report.\n */\nconst EXPRESSION_ROOT_KEY = \"expression\";\n\n/**\n * Builds the minimal property spec that makes `createExpression` enforce a\n * result type while still allowing zoom- and feature-dependent input.\n *\n * This mirrors the (unexported) `StylePropertySpecification` shape that\n * `@maplibre/maplibre-gl-style-spec` expects — the cast below means the\n * compiler cannot catch a change to that contract, so whenever the\n * style-spec package is bumped (including Dependabot PRs) re-check that\n * expected-type enforcement still works, and that the property spec is still\n * `createExpression`'s third parameter (v26 inserted a `rootKey` at index 1,\n * shifting it from `[1]` to `[2]`). The \"enforces an expected result type\"\n * test in `tests/expressions.test.ts` fails if this shape stops being honored.\n */\nfunction propertySpecFor(expectedType: ExpressionExpectedType) {\n return {\n type: expectedType,\n \"property-type\": \"data-driven\",\n expression: { parameters: [\"zoom\", \"feature\"] },\n } as unknown as NonNullable<Parameters<typeof createExpression>[2]>;\n}\n\n/** Outcome of validating an expression source string. */\nexport interface ExpressionValidation {\n ok: boolean;\n /**\n * Failure class, for callers that want to localize the shape errors:\n * `not-json` / `not-array` / `not-operator` are structural, `compile` means\n * the style spec rejected it (with messages in {@link errors}).\n */\n code?: \"not-json\" | \"not-array\" | \"not-operator\" | \"compile\";\n /** Human-readable problems; empty when `ok`. */\n errors: string[];\n /** The parsed JSON array when the source at least parsed as JSON. */\n parsed?: unknown[];\n}\n\n/**\n * Validates a user-entered MapLibre expression: tolerant JSON parse, shape\n * checks, then a real compile through the MapLibre style spec so operator\n * misuse (wrong arity, unknown operator, type mismatch) is caught in the\n * dialog instead of silently failing on the map. Variables are substituted\n * before compiling (so tokens type-check as their values), and\n * `expectedType` additionally enforces the destination's result type. The\n * returned `parsed` array is the raw, unsubstituted parse. An empty/blank\n * source is valid (surfaces treat it as \"no expression\").\n */\nexport function validateMapExpression(\n source: string,\n options: ValidateMapExpressionOptions = {},\n): ExpressionValidation {\n return compileMapExpression(source, options).validation;\n}\n\n/**\n * The single parse + substitute + compile pass shared by validation and\n * preview evaluation, so a preview never compiles the same source twice.\n * `expression` is set only when the validation succeeded on a non-empty\n * source.\n */\nfunction compileMapExpression(\n source: string,\n options: ValidateMapExpressionOptions,\n): { validation: ExpressionValidation; expression?: StyleExpression } {\n const trimmed = source.trim();\n if (!trimmed) return { validation: { ok: true, errors: [] } };\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(removeTrailingJsonCommas(trimmed));\n } catch (error) {\n return {\n validation: {\n ok: false,\n code: \"not-json\",\n errors: [error instanceof Error ? error.message : \"Invalid JSON\"],\n },\n };\n }\n if (!Array.isArray(parsed)) {\n return { validation: { ok: false, code: \"not-array\", errors: [] } };\n }\n if (typeof parsed[0] !== \"string\") {\n return {\n validation: { ok: false, code: \"not-operator\", errors: [], parsed },\n };\n }\n\n const substituted = substituteExpressionVariables(parsed, options.variables ?? []) as unknown[];\n const compiled = createExpression(\n substituted,\n EXPRESSION_ROOT_KEY,\n options.expectedType ? propertySpecFor(options.expectedType) : undefined,\n );\n if (compiled.result === \"error\") {\n return {\n validation: {\n ok: false,\n code: \"compile\",\n errors: compiled.value.map((issue) =>\n issue.key ? `${issue.key}: ${issue.message}` : issue.message,\n ),\n parsed,\n },\n };\n }\n return { validation: { ok: true, errors: [], parsed }, expression: compiled.value };\n}\n\n/** A named `@` variable with its current value. */\nexport interface ExpressionVariable {\n /** Token including the `@` prefix, e.g. `@project_name`. */\n token: string;\n value: string | number | boolean;\n}\n\n/**\n * Replaces every string node that exactly matches a known `@token` with that\n * variable's value, returning a new structure (the input is not mutated).\n * Unknown `@` strings are left untouched so field values that happen to start\n * with `@` never get mangled.\n */\nexport function substituteExpressionVariables(\n node: unknown,\n variables: ExpressionVariable[],\n): unknown {\n if (typeof node === \"string\" && node.startsWith(\"@\")) {\n const match = variables.find((variable) => variable.token === node);\n return match ? match.value : node;\n }\n if (Array.isArray(node)) {\n return node.map((entry) => substituteExpressionVariables(entry, variables));\n }\n if (node && typeof node === \"object\") {\n return Object.fromEntries(\n Object.entries(node as Record<string, unknown>).map(([key, value]) => [\n key,\n substituteExpressionVariables(value, variables),\n ]),\n );\n }\n return node;\n}\n\n/** Result of a live preview evaluation. */\nexport interface ExpressionPreview {\n kind: \"empty\" | \"error\" | \"value\";\n /** Problems when `kind` is \"error\" (parse or runtime). */\n errors?: string[];\n /** The evaluated value when `kind` is \"value\". */\n value?: unknown;\n}\n\n/** Options for {@link evaluateMapExpression}. */\nexport interface EvaluateMapExpressionOptions {\n /** The sample feature the expression is evaluated against. */\n feature?: Feature | null;\n /** Map zoom used for `[\"zoom\"]`; defaults to 0. */\n zoom?: number;\n /** Variables substituted before compiling. */\n variables?: ExpressionVariable[];\n /** Enforce the destination's result type, as in validation. */\n expectedType?: ExpressionExpectedType;\n}\n\n/**\n * Evaluates an expression source against a sample feature for the builder's\n * live preview. Variables are substituted first, then the expression is\n * compiled with the MapLibre style spec and evaluated with the given zoom.\n * Runtime failures (e.g. `to-color` on garbage) come back as errors instead\n * of throwing.\n */\nexport function evaluateMapExpression(\n source: string,\n options: EvaluateMapExpressionOptions = {},\n): ExpressionPreview {\n const trimmed = source.trim();\n if (!trimmed) return { kind: \"empty\" };\n\n const { validation, expression } = compileMapExpression(trimmed, {\n variables: options.variables,\n expectedType: options.expectedType,\n });\n if (!validation.ok || !expression) {\n return { kind: \"error\", errors: validation.errors };\n }\n\n const feature = options.feature ?? null;\n const geometryType = feature?.geometry?.type ?? \"Unknown\";\n try {\n // evaluateWithoutErrorHandling: the plain evaluate() swallows runtime\n // failures into console.warn, which (a) hides the error from the preview\n // and (b) feeds the app's diagnostics console interceptor from inside a\n // React render, which can re-render the panel and re-evaluate in an\n // endless loop. Throwing lets the catch below surface the message.\n const value = expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {\n type: geometryType,\n properties: feature?.properties ?? {},\n ...(feature?.id !== undefined ? { id: feature.id } : {}),\n geometry: feature?.geometry,\n } as never);\n return { kind: \"value\", value };\n } catch (error) {\n return {\n kind: \"error\",\n errors: [error instanceof Error ? error.message : String(error)],\n };\n }\n}\n\n/**\n * A compiled per-feature evaluator: the expression is parsed and compiled\n * once, then `evaluate` runs it against each feature. `evaluate` is absent\n * when compilation failed (see `errors`); it throws on runtime evaluation\n * failures (same rationale as {@link evaluateMapExpression}: throwing keeps\n * errors out of console.warn) so callers decide per-feature error handling.\n */\nexport interface CompiledFeatureExpression {\n ok: boolean;\n /** Parse/compile problems when `ok` is false. */\n errors: string[];\n evaluate?: (feature: Feature) => unknown;\n}\n\n/**\n * Compiles an expression source once for bulk per-feature evaluation (virtual\n * fields, and any other caller that runs one expression over a whole layer).\n * Unlike {@link evaluateMapExpression} the compile cost is paid once, not per\n * feature. An empty source is reported as a failure with no errors — there is\n * nothing to evaluate.\n */\nexport function compileFeatureExpression(\n source: string,\n options: {\n zoom?: number;\n variables?: ExpressionVariable[];\n expectedType?: ExpressionExpectedType;\n } = {},\n): CompiledFeatureExpression {\n const trimmed = source.trim();\n if (!trimmed) return { ok: false, errors: [] };\n\n const { validation, expression } = compileMapExpression(trimmed, {\n variables: options.variables,\n expectedType: options.expectedType,\n });\n if (!validation.ok || !expression) {\n return { ok: false, errors: validation.errors };\n }\n const zoom = options.zoom ?? 0;\n return {\n ok: true,\n errors: [],\n evaluate: (feature) =>\n expression.evaluateWithoutErrorHandling({ zoom }, {\n type: feature.geometry?.type ?? \"Unknown\",\n properties: feature.properties ?? {},\n ...(feature.id !== undefined ? { id: feature.id } : {}),\n geometry: feature.geometry,\n } as never),\n };\n}\n\n/** Result of matching a feature array against a boolean expression. */\nexport interface FeatureExpressionMatches {\n /** False when the expression failed to parse or compile. */\n ok: boolean;\n /** Parse/compile problems when `ok` is false. */\n errors: string[];\n /** Selection ids ({@link featureSelectionId}) of the matched features. */\n ids: string[];\n /**\n * Features whose evaluation threw at runtime (e.g. a boolean assertion on\n * a string field). Counted as non-matches so one bad feature cannot abort\n * the whole selection, but surfaced so the UI can warn.\n */\n errorCount: number;\n}\n\n/**\n * Evaluates a boolean expression against every feature and returns the\n * selection ids of the matches (Select by Expression). Unlike\n * {@link evaluateMapExpression} this compiles the source once and reuses the\n * compiled expression across the whole array, so it stays fast on large\n * layers. An empty source is reported as `ok: false` with no errors — there\n * is nothing to select by.\n */\nexport function matchFeaturesByExpression(\n features: readonly Feature[],\n source: string,\n options: { zoom?: number; variables?: ExpressionVariable[] } = {},\n): FeatureExpressionMatches {\n const trimmed = source.trim();\n if (!trimmed) return { ok: false, errors: [], ids: [], errorCount: 0 };\n\n const { validation, expression } = compileMapExpression(trimmed, {\n variables: options.variables,\n expectedType: \"boolean\",\n });\n if (!validation.ok || !expression) {\n return { ok: false, errors: validation.errors, ids: [], errorCount: 0 };\n }\n\n const ids: string[] = [];\n let errorCount = 0;\n features.forEach((feature, index) => {\n try {\n // Same rationale as evaluateMapExpression: evaluateWithoutErrorHandling\n // so runtime failures throw instead of feeding console.warn.\n const value = expression.evaluateWithoutErrorHandling({ zoom: options.zoom ?? 0 }, {\n type: feature.geometry?.type ?? \"Unknown\",\n properties: feature.properties ?? {},\n ...(feature.id !== undefined ? { id: feature.id } : {}),\n geometry: feature.geometry,\n } as never);\n if (value === true) ids.push(featureSelectionId(feature, index));\n } catch {\n errorCount += 1;\n }\n });\n return { ok: true, errors: [], ids, errorCount };\n}\n\n/**\n * True when a preview value is a real style-spec `Color` instance (its\n * evaluated color representation: premultiplied r/g/b/a floats). Shared by\n * the preview formatter and the dialog's color-swatch rendering so the two\n * checks cannot drift apart. An instanceof check (not duck typing) so user\n * attribute data that happens to carry numeric r/g/b/a fields (e.g. via\n * `[\"properties\"]`) is never misclassified as a color.\n */\nexport function isStyleSpecColor(\n value: unknown,\n): value is { r: number; g: number; b: number; a: number } {\n return value instanceof Color;\n}\n\n/**\n * Renders a preview value as a short display string: style-spec Color\n * instances become rgba() strings, plain values render as JSON, and\n * `null`/`undefined` render as \"null\".\n */\nexport function formatExpressionPreviewValue(value: unknown): string {\n if (value === null || value === undefined) return \"null\";\n if (isStyleSpecColor(value)) {\n const color = value;\n // Style-spec colors are premultiplied floats in [0, 1]; undo the\n // premultiplication so the displayed rgba matches the authored color.\n const alpha = color.a;\n const channel = (component: number) => Math.round((alpha === 0 ? 0 : component / alpha) * 255);\n return `rgba(${channel(color.r)}, ${channel(color.g)}, ${channel(color.b)}, ${Number(alpha.toFixed(3))})`;\n }\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/** Attribute field types the builder badges fields with. */\nexport type ExpressionFieldType = \"string\" | \"number\" | \"boolean\" | \"mixed\" | \"unknown\";\n\n/**\n * Infers a display type for each named field by scanning feature properties:\n * a single consistent primitive type wins, disagreements become \"mixed\", and\n * fields with no non-null values are \"unknown\".\n */\nexport function inferFieldTypes(\n features: ReadonlyArray<{ properties?: Record<string, unknown> | null }>,\n names: string[],\n): Record<string, ExpressionFieldType> {\n const result: Record<string, ExpressionFieldType> = {};\n for (const name of names) result[name] = \"unknown\";\n\n for (const feature of features) {\n const properties = feature.properties;\n if (!properties) continue;\n for (const name of names) {\n const value = properties[name];\n if (value === null || value === undefined) continue;\n const valueType = typeof value;\n const fieldType: ExpressionFieldType =\n valueType === \"string\" || valueType === \"number\" || valueType === \"boolean\"\n ? valueType\n : \"mixed\";\n if (result[name] === \"unknown\") result[name] = fieldType;\n else if (result[name] !== fieldType) result[name] = \"mixed\";\n }\n }\n return result;\n}\n","import { styleValue, type LayerStyle, type VectorRule } from \"./types\";\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/** 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","/**\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 * 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","import type { Feature, GeoJsonProperties } from \"geojson\";\nimport {\n compileFeatureExpression,\n formatExpressionPreviewValue,\n isStyleSpecColor,\n} from \"./expressions\";\nimport type { LayerVirtualField } from \"./types\";\n\n/**\n * Virtual fields (QGIS Field Calculator → \"Create virtual field\", issue\n * #1321): expression-backed columns that recompute live instead of being\n * written once as static values.\n *\n * The engine mirrors the persistent-joins engine (`joins.ts`): computed\n * values are materialized into feature properties, so every consumer of\n * attributes — the attribute table, Expression Builder, data-driven styling,\n * labels, selection, export — sees the column with no further wiring.\n * Idempotency uses the same bookkeeping: each applied field records the\n * column it actually added (`addedField`), applying always strips those\n * first, and an existing column always wins a name collision (the virtual\n * field is skipped entirely), so stripping exactly restores the pre-apply\n * properties.\n *\n * Expressions are declarative MapLibre expressions compiled through the\n * style spec — never arbitrary code — so persisting them in `.geolibre.json`\n * and re-evaluating them on project load is safe (unlike the attribute\n * table's Field Calculator, whose JavaScript evaluator must never run\n * persisted expressions). Fields apply in list order and each sees the\n * columns materialized by earlier fields, so a virtual field can build on\n * another. Zoom-dependent expressions evaluate at zoom 0 and `@` variables\n * are not substituted: a virtual field is a property of the data, not of the\n * current view.\n *\n * Known trade-off, shared with the joins engine (see `joins.ts`): when a\n * layer's data is replaced wholesale, the replacement is stripped with the\n * *previous* bookkeeping. That is required because a replacement can also be\n * a write-back of the derived output (the attribute table round-trips\n * `layer.geojson`), which must not freeze a stale computed value into base\n * data — but it means a replacement dataset's own column whose name matches a\n * previously-materialized virtual column is replaced by the expression's\n * value for that column. Renaming or removing the virtual field first keeps\n * such collisions from arising.\n */\n\n/**\n * Remove every column previously added by `fields` (per their `addedField`\n * bookkeeping) from a copy of `features`, restoring the pre-apply properties.\n * Features without any tracked column are returned unchanged (same\n * reference).\n */\nexport function stripVirtualFieldColumns(\n features: Feature[],\n fields: LayerVirtualField[] | undefined,\n): Feature[] {\n const tracked = new Set<string>();\n for (const field of fields ?? []) {\n if (field.addedField) tracked.add(field.addedField);\n }\n if (tracked.size === 0) return features;\n return features.map((feature) => {\n const props = feature.properties;\n if (!props) return feature;\n let hasTracked = false;\n for (const key of tracked) {\n if (key in props) {\n hasTracked = true;\n break;\n }\n }\n if (!hasTracked) return feature;\n const next: GeoJsonProperties = {};\n for (const [key, value] of Object.entries(props)) {\n if (!tracked.has(key)) next[key] = value;\n }\n return { ...feature, properties: next };\n });\n}\n\n/** Result of {@link applyLayerVirtualFields}: features plus refreshed bookkeeping. */\nexport interface ApplyVirtualFieldsResult {\n features: Feature[];\n /** Input fields with `addedField`/`error`/`errorCount` recomputed for this run. */\n fields: LayerVirtualField[];\n}\n\n/**\n * Normalize an evaluated expression value into a property value: `undefined`\n * and non-finite numbers become null (a clean empty cell), style-spec Color\n * instances become their rgba() string (a Color object would neither\n * serialize nor display usefully), everything else passes through.\n */\nfunction normalizeComputedValue(value: unknown): unknown {\n if (value === undefined) return null;\n if (typeof value === \"number\" && !Number.isFinite(value)) return null;\n if (isStyleSpecColor(value)) return formatExpressionPreviewValue(value);\n return value;\n}\n\n/**\n * Apply `fields` in order to base (already-stripped) `features`, returning\n * new feature objects with each computed column merged into their properties.\n *\n * Semantics per field: a disabled field, an empty/colliding output name, or\n * an expression that fails to compile contributes nothing (with the compile\n * error recorded on the field); a runtime evaluation failure nulls that one\n * feature's cell and increments the field's `errorCount` instead of aborting\n * the column. Fields evaluate sequentially, so a later field's expression\n * can `[\"get\", ...]` an earlier field's column.\n */\nexport function applyLayerVirtualFields(\n features: Feature[],\n fields: LayerVirtualField[] | undefined,\n): ApplyVirtualFieldsResult {\n const fieldList = fields ?? [];\n const clearBookkeeping = (field: LayerVirtualField): LayerVirtualField => ({\n ...field,\n addedField: undefined,\n error: undefined,\n errorCount: undefined,\n });\n // Only bail out when no field is active. A zero-feature layer still runs\n // the per-field pass below: compiling validates the expression (so a real\n // compile error is surfaced, not swallowed) and a free name is still\n // recorded as `addedField` — otherwise an empty dataset would be\n // indistinguishable from a name collision in the UI.\n const active = fieldList.filter((field) => field.enabled !== false);\n if (active.length === 0) {\n return { features, fields: fieldList.map(clearBookkeeping) };\n }\n\n // Existing columns always win a name collision: a virtual field never\n // shadows a base column, a joined column, or an earlier virtual field.\n const usedNames = new Set<string>();\n for (const feature of features) {\n for (const key of Object.keys(feature.properties ?? {})) usedNames.add(key);\n }\n\n // One clone up front; per-field application then mutates our own copies.\n const out = features.map((feature) => ({\n ...feature,\n properties: { ...(feature.properties ?? {}) },\n }));\n\n const outFields = fieldList.map((field): LayerVirtualField => {\n if (field.enabled === false) return clearBookkeeping(field);\n const name = field.name.trim();\n if (!name || usedNames.has(name)) return clearBookkeeping(field);\n\n const compiled = compileFeatureExpression(field.expression);\n if (!compiled.ok || !compiled.evaluate) {\n return {\n ...clearBookkeeping(field),\n error: compiled.errors[0] ?? \"Invalid expression\",\n };\n }\n\n usedNames.add(name);\n let errorCount = 0;\n for (const feature of out) {\n let value: unknown = null;\n try {\n value = normalizeComputedValue(compiled.evaluate(feature));\n } catch {\n errorCount += 1;\n }\n feature.properties[name] = value;\n }\n return {\n ...clearBookkeeping(field),\n addedField: name,\n ...(errorCount > 0 ? { errorCount } : {}),\n };\n });\n\n return { features: out, fields: outFields };\n}\n","import type { Feature, FeatureCollection, GeoJsonProperties } from \"geojson\";\nimport type { GeoLibreLayer, LayerJoin, LayerVirtualField } from \"./types\";\nimport { applyLayerVirtualFields, stripVirtualFieldColumns } from \"./virtual-fields\";\n\n/**\n * Persistent attribute joins (QGIS Layer Properties → Joins), issue #1315.\n *\n * A layer's `joins` are live left joins that materialize columns from another\n * layer's attribute table into this layer's feature properties. Materializing\n * (rather than resolving lazily) means every consumer of attributes — the\n * attribute table, Expression Builder, data-driven styling, labels, diagrams,\n * export — sees the joined columns with no further wiring.\n *\n * Idempotency without a duplicate base copy: each applied join records the\n * output column names it added (`addedFields`), and applying joins always\n * strips those first. Base columns win every name collision (a joined column\n * whose output name already exists is skipped entirely), so stripping the\n * added columns exactly restores the pre-join properties.\n *\n * Known trade-off of that bookkeeping: when a layer's data is replaced\n * wholesale (file reload), the replacement is stripped with the *previous*\n * `addedFields`. That is required because a replacement can also be a\n * write-back of the joined output (the attribute table round-trips\n * `layer.geojson`), which must not freeze stale joined values into base data —\n * but it means a reloaded base column whose name collides with a\n * previously-joined output column is replaced by the join's value for that\n * column. A field-name prefix (the QGIS convention the UI suggests) keeps\n * such collisions from arising.\n *\n * The data is already in memory as JS feature objects, so the join is a plain\n * hash join here rather than the DuckDB-WASM SQL statement sketched in the\n * issue — the result is identical for equality keys and the engine stays\n * synchronous and dependency-free. Key semantics deliberately mirror the\n * Processing → Vector attribute join (and the sidecar's `_attribute_join_key`):\n * empty values never match, and non-empty values compare stringified.\n */\n\n/** Canonical JSON for object values (sorted keys), matching the processing engine. */\nfunction stableStringify(value: unknown): string {\n if (Array.isArray(value)) {\n return `[${value.map(stableStringify).join(\",\")}]`;\n }\n if (value !== null && typeof value === \"object\") {\n const entries = Object.entries(value as Record<string, unknown>)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);\n return `{${entries.join(\",\")}}`;\n }\n return JSON.stringify(value) ?? \"null\";\n}\n\nfunction valueToString(value: unknown): string {\n if (typeof value === \"boolean\") return value ? \"true\" : \"false\";\n if (value !== null && typeof value === \"object\") return stableStringify(value);\n return String(value);\n}\n\n/**\n * Match key for a persistent join: empty values (null/undefined/NaN/empty\n * string) never match a row, mirroring a SQL/pandas NaN join key. Non-empty\n * values are keyed by their string form, so a numeric `5` and the string `\"5\"`\n * join while a zero-padded code like `\"01001\"` only matches another `\"01001\"`.\n * Kept in sync with the Processing attribute join's `attributeJoinKey`.\n */\nexport function layerJoinKey(value: unknown): string | null {\n const isEmpty =\n value === null ||\n value === undefined ||\n (typeof value === \"number\" && Number.isNaN(value)) ||\n valueToString(value) === \"\";\n return isEmpty ? null : valueToString(value);\n}\n\n/**\n * Remove every column previously added by `joins` (per their `addedFields`\n * bookkeeping) from a copy of `features`, restoring the pre-join properties.\n * Features without any tracked column are returned unchanged (same reference).\n */\nexport function stripJoinFields(features: Feature[], joins: LayerJoin[] | undefined): Feature[] {\n const tracked = new Set<string>();\n for (const join of joins ?? []) {\n for (const field of join.addedFields ?? []) tracked.add(field);\n }\n if (tracked.size === 0) return features;\n return features.map((feature) => {\n const props = feature.properties;\n if (!props) return feature;\n let hasTracked = false;\n for (const key of tracked) {\n if (key in props) {\n hasTracked = true;\n break;\n }\n }\n if (!hasTracked) return feature;\n const next: GeoJsonProperties = {};\n for (const [key, value] of Object.entries(props)) {\n if (!tracked.has(key)) next[key] = value;\n }\n return { ...feature, properties: next };\n });\n}\n\n/** Result of {@link applyLayerJoins}: joined features plus refreshed join records. */\nexport interface ApplyLayerJoinsResult {\n features: Feature[];\n /** Input joins with `addedFields` and `stats` recomputed for this run. */\n joins: LayerJoin[];\n}\n\n/**\n * Apply `joins` in order to base (already-stripped) `features`, returning new\n * feature objects with the joined columns merged into their properties.\n *\n * Semantics per join: left join, first matching join row wins, and joined\n * columns are null-filled on unmatched features so the schema stays\n * consistent. An output name (prefix + field) that collides with an existing\n * column — from the base data or an earlier join — drops that one column;\n * the join's other columns still apply. A disabled join, or one whose source\n * cannot be resolved, contributes nothing and gets empty `addedFields` and no\n * stats.\n *\n * @param features - The layer's base features (strip previous joins first).\n * @param joins - Join definitions in application order.\n * @param resolveSource - Maps a join's `joinLayerId` to that layer's current\n * feature collection, or `undefined` when the layer is gone (or is the\n * target itself; self-joins are refused by the caller).\n */\nexport function applyLayerJoins(\n features: Feature[],\n joins: LayerJoin[] | undefined,\n resolveSource: (joinLayerId: string) => FeatureCollection | undefined,\n): ApplyLayerJoinsResult {\n const joinList = joins ?? [];\n // Resolve each source once, shared between the early-exit check and the\n // per-join application below.\n const resolved = new Map<string, FeatureCollection | undefined>();\n const resolveOnce = (joinLayerId: string): FeatureCollection | undefined => {\n if (!resolved.has(joinLayerId)) {\n resolved.set(joinLayerId, resolveSource(joinLayerId));\n }\n return resolved.get(joinLayerId);\n };\n const active = joinList.filter((join) => join.enabled !== false && resolveOnce(join.joinLayerId));\n if (active.length === 0) {\n return {\n features,\n joins: joinList.map((join) => ({\n ...join,\n addedFields: [],\n stats: undefined,\n })),\n };\n }\n\n // One clone up front; per-join application then mutates our own copies.\n const out = features.map((feature) => ({\n ...feature,\n properties: { ...(feature.properties ?? {}) },\n }));\n\n // Schema-level collision tracking: a joined column never shadows a base\n // column (or an earlier join's column) on any feature.\n const usedNames = new Set<string>();\n for (const feature of features) {\n for (const key of Object.keys(feature.properties ?? {})) usedNames.add(key);\n }\n\n const outJoins = joinList.map((join): LayerJoin => {\n const source = join.enabled === false ? undefined : resolveOnce(join.joinLayerId);\n if (!source) return { ...join, addedFields: [], stats: undefined };\n\n const joinFeatures = (source.features ?? []).filter(Boolean);\n\n // Collect join-table columns in first-seen order for a deterministic schema.\n const joinKeysOrder: string[] = [];\n const joinKeySet = new Set<string>();\n for (const jf of joinFeatures) {\n for (const key of Object.keys(jf.properties ?? {})) {\n if (!joinKeySet.has(key)) {\n joinKeySet.add(key);\n joinKeysOrder.push(key);\n }\n }\n }\n\n // An explicit `fields` array is honored verbatim (an empty subset joins no\n // columns); only an absent array means \"every field except the key\".\n const requested =\n join.fields !== undefined\n ? join.fields.filter((field) => joinKeySet.has(field))\n : joinKeysOrder.filter((key) => key !== join.joinField);\n const prefix = join.prefix ?? \"\";\n const fieldPairs: Array<[source: string, output: string]> = [];\n for (const field of requested) {\n const outputName = prefix + field;\n if (usedNames.has(outputName)) continue;\n usedNames.add(outputName);\n fieldPairs.push([field, outputName]);\n }\n\n // First-match lookup: when several join rows share a key, the first wins.\n const lookup = new Map<string, GeoJsonProperties>();\n for (const jf of joinFeatures) {\n const key = layerJoinKey(jf.properties?.[join.joinField]);\n if (key === null || lookup.has(key)) continue;\n lookup.set(key, jf.properties ?? {});\n }\n\n let matched = 0;\n const targetKeys = new Set<string>();\n for (const feature of out) {\n const props = feature.properties;\n const key = layerJoinKey(props[join.targetField]);\n if (key !== null) targetKeys.add(key);\n const row = key === null ? undefined : lookup.get(key);\n if (row) {\n matched += 1;\n for (const [field, outputName] of fieldPairs) {\n props[outputName] = row[field] !== undefined ? row[field] : null;\n }\n } else {\n for (const [, outputName] of fieldPairs) props[outputName] = null;\n }\n }\n\n // Counts rows, not unique keys: duplicate unmatched rows each count, as\n // the stat (and the UI copy) promise.\n let unmatchedJoin = 0;\n for (const jf of joinFeatures) {\n const key = layerJoinKey(jf.properties?.[join.joinField]);\n if (key !== null && !targetKeys.has(key)) unmatchedJoin += 1;\n }\n\n return {\n ...join,\n addedFields: fieldPairs.map(([, outputName]) => outputName),\n stats: {\n matchedCount: matched,\n unmatchedTargetCount: out.length - matched,\n unmatchedJoinCount: unmatchedJoin,\n },\n };\n });\n\n return { features: out, joins: outJoins };\n}\n\n/**\n * Strip-and-reapply this layer's derived columns — persistent joins, then\n * virtual fields (so an expression can read joined columns) — resolving join\n * sources from `allLayers`. Pass `nextJoins`/`nextVirtualFields` to replace\n * the definitions (the layer's current bookkeeping still drives the strip, so\n * removed definitions clean up after themselves); omit them to refresh the\n * existing definitions against current data. Layers without a feature\n * collection are returned unchanged.\n */\nexport function applyJoinsToLayer(\n layer: GeoLibreLayer,\n allLayers: GeoLibreLayer[],\n nextJoins?: LayerJoin[],\n nextVirtualFields?: LayerVirtualField[],\n): GeoLibreLayer {\n const geojson = layer.geojson;\n const joins = nextJoins ?? layer.joins ?? [];\n const virtualFields = nextVirtualFields ?? layer.virtualFields ?? [];\n if (!geojson) {\n return {\n ...layer,\n joins: joins.length > 0 ? joins : undefined,\n virtualFields: virtualFields.length > 0 ? virtualFields : undefined,\n };\n }\n // Virtual columns strip before join columns so a join output name held by a\n // stale virtual column frees up; the reverse order would also work today\n // (both strips are pure column removals), but this matches apply order.\n const base = stripJoinFields(\n stripVirtualFieldColumns(geojson.features ?? [], layer.virtualFields),\n layer.joins,\n );\n const applied = applyLayerJoins(base, joins, (joinLayerId) => {\n if (joinLayerId === layer.id) return undefined;\n return allLayers.find((candidate) => candidate.id === joinLayerId)?.geojson;\n });\n const withVirtual = applyLayerVirtualFields(applied.features, virtualFields);\n return {\n ...layer,\n geojson: { ...geojson, features: withVirtual.features },\n joins: applied.joins.length > 0 ? applied.joins : undefined,\n virtualFields: withVirtual.fields.length > 0 ? withVirtual.fields : undefined,\n };\n}\n\n/**\n * Ids of every layer whose data (directly or transitively) feeds `layerId`'s\n * joins, following `joinLayerId` edges through each source's own joins.\n * Disabled joins count too: re-enabling one must not spring a cycle. The Joins\n * UI uses this to keep a layer that already consumes the target — however\n * indirectly — out of the join-source picker, so circular joins cannot be\n * authored (the refresh cascade still breaks them defensively for hand-edited\n * projects).\n */\nexport function collectTransitiveJoinSourceIds(\n layers: GeoLibreLayer[],\n layerId: string,\n): Set<string> {\n const byId = new Map(layers.map((layer) => [layer.id, layer]));\n const visited = new Set<string>();\n const queue = [layerId];\n while (queue.length > 0) {\n const current = byId.get(queue.shift() as string);\n for (const join of current?.joins ?? []) {\n if (visited.has(join.joinLayerId)) continue;\n visited.add(join.joinLayerId);\n queue.push(join.joinLayerId);\n }\n }\n return visited;\n}\n\n/**\n * Order `candidates` (layers with joins) so every layer comes after the\n * candidates its joins consume — Kahn's algorithm over \"join source → join\n * target\" edges within the candidate set. When a cycle blocks progress\n * (possible only in hand-edited projects; the UI refuses circular joins), an\n * actual cycle member is forced through — found by walking unmet-source edges\n * until a node repeats — so consumers *downstream* of the cycle still order\n * after it rather than falling back ahead of it.\n */\nfunction topologicalJoinOrder(candidates: GeoLibreLayer[]): string[] {\n const candidateIds = new Set(candidates.map((layer) => layer.id));\n const indegree = new Map<string, number>();\n const dependents = new Map<string, string[]>();\n const sourcesOf = new Map<string, string[]>();\n for (const layer of candidates) indegree.set(layer.id, 0);\n for (const layer of candidates) {\n for (const join of layer.joins ?? []) {\n if (\n join.enabled === false ||\n join.joinLayerId === layer.id ||\n !candidateIds.has(join.joinLayerId)\n ) {\n continue;\n }\n indegree.set(layer.id, (indegree.get(layer.id) ?? 0) + 1);\n const list = dependents.get(join.joinLayerId) ?? [];\n list.push(layer.id);\n dependents.set(join.joinLayerId, list);\n const sources = sourcesOf.get(layer.id) ?? [];\n sources.push(join.joinLayerId);\n sourcesOf.set(layer.id, sources);\n }\n }\n const order: string[] = [];\n const orderedSet = new Set<string>();\n const queue = candidates.filter((layer) => indegree.get(layer.id) === 0).map((layer) => layer.id);\n while (order.length < candidates.length) {\n if (queue.length === 0) {\n // Every unordered layer has at least one unordered source, so walking\n // unmet-source edges from any unordered layer must revisit a node, and\n // that node sits on a cycle.\n const start = candidates.find((layer) => !orderedSet.has(layer.id));\n if (!start) break;\n let cursor = start.id;\n const walked = new Set<string>();\n while (!walked.has(cursor)) {\n walked.add(cursor);\n const next = (sourcesOf.get(cursor) ?? []).find((source) => !orderedSet.has(source));\n if (next === undefined) break;\n cursor = next;\n }\n queue.push(cursor);\n }\n const id = queue.shift() as string;\n if (orderedSet.has(id)) continue;\n orderedSet.add(id);\n order.push(id);\n for (const dependent of dependents.get(id) ?? []) {\n const remaining = (indegree.get(dependent) ?? 1) - 1;\n indegree.set(dependent, remaining);\n if (remaining === 0 && !orderedSet.has(dependent)) queue.push(dependent);\n }\n }\n return order;\n}\n\n/**\n * Refresh every layer whose joins (directly or transitively) consume the\n * layer identified by `seedId`, after that layer's data changed — or after it\n * was removed (a gone source resolves to nothing, so its frozen columns strip\n * away). The affected layers re-derive in topological dependency order, so a\n * layer joining two other affected layers sees both of them fresh regardless\n * of their positions in the layer panel. The seed itself is not touched.\n */\nexport function cascadeLayerJoinRefresh(layers: GeoLibreLayer[], seedId: string): GeoLibreLayer[] {\n // Collect the transitive dependents of the seed.\n const affected = new Set<string>();\n const queue = [seedId];\n while (queue.length > 0) {\n const sourceId = queue.shift() as string;\n for (const layer of layers) {\n if (layer.id === seedId || affected.has(layer.id)) continue;\n if (layer.joins?.some((join) => join.enabled !== false && join.joinLayerId === sourceId)) {\n affected.add(layer.id);\n queue.push(layer.id);\n }\n }\n }\n if (affected.size === 0) return layers;\n\n let current = layers;\n const order = topologicalJoinOrder(layers.filter((layer) => affected.has(layer.id)));\n for (const id of order) {\n current = current.map((layer) => (layer.id === id ? applyJoinsToLayer(layer, current) : layer));\n }\n return current;\n}\n\n/**\n * Re-resolve every layer's persistent joins against the freshly loaded layer\n * set (project open). Layers re-derive in topological order of their join\n * dependencies, so a chain (`C joins A, A joins B`) resolves A before C; a\n * hand-edited cycle is broken by forcing one of its members through first\n * (see {@link topologicalJoinOrder}), so consumers downstream of the cycle\n * still re-derive after it. Layers with virtual fields but no joins re-derive\n * too (their expressions run against the freshly loaded features). Layer sets\n * without joins or virtual fields pass through by reference.\n */\nexport function reapplyLayerJoins(layers: GeoLibreLayer[]): GeoLibreLayer[] {\n const joined = layers.filter((layer) => layer.joins?.length || layer.virtualFields?.length);\n if (joined.length === 0) return layers;\n\n let current = layers;\n for (const id of topologicalJoinOrder(joined)) {\n current = current.map((layer) => (layer.id === id ? applyJoinsToLayer(layer, current) : layer));\n }\n return current;\n}\n","import type { GeoLibreLayer, LayerCapabilities } 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 * 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","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 // 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/**\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","// 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","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","import { 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_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 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} from \"./types\";\nimport { DEFAULT_LAYER_GROUP_OPACITY, normalizeGroupContiguity } from \"./layer-groups\";\nimport { normalizeStyleLibraryEntries } from \"./style-library\";\nimport { normalizeLayerCapabilities } from \"./capabilities\";\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 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 layers: [],\n layerGroups: [],\n styles: {},\n preferences: options.ellipsoidId\n ? {\n ...DEFAULT_PROJECT_PREFERENCES,\n map: {\n ...DEFAULT_PROJECT_PREFERENCES.map,\n ellipsoidId: getEllipsoid(options.ellipsoidId).id,\n },\n }\n : DEFAULT_PROJECT_PREFERENCES,\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 * @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): string | undefined {\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) ?? \"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);\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 serializeProjectValue(project, 0, \"\", new Set()) ?? \"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 // 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 return {\n version: data.version,\n name: data.name,\n mapView,\n basemapStyleUrl,\n basemapVisible,\n basemapOpacity,\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 ...(styleLibrary.length > 0 ? { styleLibrary } : {}),\n ...(parsedComments.length > 0 ? { comments: parsedComments } : {}),\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 `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 const viewKind =\n candidate.viewKind === \"cesium\" || candidate.viewKind === \"maplibre\"\n ? candidate.viewKind\n : undefined;\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\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 // 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 },\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\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 };\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\nfunction normalizeLayer(layer: GeoLibreLayer): GeoLibreLayer {\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 { capabilities: rawCapabilities, ...rest } = layer;\n const capabilities = normalizeLayerCapabilities(rawCapabilities);\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 };\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 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 /** Project-scoped Style Manager entries (the store's `projectStyleLibrary`). */\n styleLibrary?: StyleLibraryEntry[] | null;\n comments?: ProjectComment[] | 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 // 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 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 ...(styleLibrary.length > 0 ? { styleLibrary } : {}),\n ...(comments.length > 0 ? { comments } : {}),\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 // 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\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 // 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 !== \"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 return {\n ...layer,\n source: {\n ...layer.source,\n tiles: [originalUrl],\n url: originalUrl,\n },\n metadata,\n };\n}\n\nexport function applyProjectToStore(project: GeoLibreProject): {\n projectName: string;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\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 projectStyleLibrary: StyleLibraryEntry[];\n comments: ProjectComment[];\n metadata: Record<string, unknown>;\n} {\n const layers = project.layers.map((layer) => ({\n ...layer,\n style: project.styles[layer.id]\n ? { ...DEFAULT_LAYER_STYLE, ...project.styles[layer.id] }\n : { ...DEFAULT_LAYER_STYLE, ...layer.style },\n }));\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 // 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 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 projectStyleLibrary: normalizeStyleLibraryEntries(project.styleLibrary),\n comments: scrubbedComments,\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","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","import 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};\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\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 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 * 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.\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 const preferences = project.preferences\n ? { ...project.preferences, environmentVariables: [], geocoding }\n : project.preferences;\n const populatedEnvironmentVariables =\n project.preferences?.environmentVariables?.filter((variable) => variable.key.trim()) ?? [];\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 = Object.prototype.hasOwnProperty.call(PUBLISHABLE_PLUGIN_SETTINGS, id)\n ? PUBLISHABLE_PLUGIN_SETTINGS[id]\n : undefined;\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/** 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 type { FeatureCollection } from \"geojson\";\nimport {\n DEFAULT_LAYER_STYLE,\n LAYER_TYPES,\n type AttributeFormConfig,\n type GeoLibreLayer,\n type LayerJoin,\n type LayerLibraryEntry,\n type LayerStyle,\n type LayerType,\n type LayerVirtualField,\n} from \"./types\";\nimport { sanitizeLayerStylePatch } from \"./style-library\";\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 */\nconst TRANSIENT_METADATA_KEYS = [\"resolvedUrl\"] 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 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 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 };\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}\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 },\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 };\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/** 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 return {\n ...(joins ? { joins } : {}),\n ...(virtualFields ? { virtualFields } : {}),\n ...(attributeForm ? { attributeForm } : {}),\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","import type { FeatureCollection } from \"geojson\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport { create } from \"zustand\";\nimport { shallow } from \"zustand/shallow\";\nimport { temporal } from \"zundo\";\nimport {\n getHistoryCoalesceMs,\n getMaxHistoryFeatureCount,\n leadingDebounce,\n trimHistoryBySize,\n} from \"./history\";\nimport {\n applyProjectToStore,\n type CreateProjectOptions,\n createDefaultMapView,\n createEmptyProject,\n DEFAULT_PROJECT_NAME,\n} from \"./project\";\nimport { initialLayerStyle } from \"./layer-defaults\";\nimport {\n createDefaultPrintLayout,\n printLayoutConfigsEqual,\n scrubPrintLayoutForRemovedLayers,\n type PrintLayoutConfig,\n} from \"./print-layout-config\";\nimport {\n DEFAULT_LAYER_GROUP_OPACITY,\n normalizeGroupContiguity,\n reorderLayerGroupInPanel,\n} from \"./layer-groups\";\nimport {\n DEFAULT_BASEMAP,\n DEFAULT_DASHBOARD_COLUMNS,\n DEFAULT_LAYER_STYLE,\n DEFAULT_LEGEND_CONFIG,\n DEFAULT_MAP_GRID_LAYOUT,\n DEFAULT_PROJECT_PREFERENCES,\n MAX_MAP_GRID_DIM,\n DEFAULT_STORY_MAP,\n MAX_DASHBOARD_COLUMNS,\n MAX_PROCESSING_HISTORY,\n MIN_DASHBOARD_COLUMNS,\n type AddTileLayerOptions,\n type CollabInvite,\n type CollaborationChatMessage,\n type CollaborationParticipant,\n type CollaborationPresence,\n type CollaborationState,\n type DashboardWidget,\n type GeoLibreLayer,\n type GeoLibreProject,\n type LayerGroup,\n type LayerLibraryEntry,\n type AttributeFormConfig,\n type EditorTrackingConfig,\n type LayerJoin,\n type LayerVirtualField,\n type LayerStyle,\n type LegendConfig,\n type MapGridLayout,\n type MapViewState,\n type ProcessingModel,\n type ProcessingRerunRequest,\n type ProcessingRun,\n type SecondaryMapView,\n type ProjectPluginState,\n type ProjectPreferences,\n type RecentProjectEntry,\n type StoryChapter,\n type StoryMap,\n type StyleLibraryEntry,\n type ProjectTemplateEntry,\n type CommentAnchor,\n type CommentAuthor,\n type CommentReply,\n type ProjectComment,\n} from \"./types\";\nimport {\n removedLayerIdSet,\n scrubWidgetsForRemovedLayers,\n scrubCommentsForRemovedLayers,\n scrubLegendForRemovedLayers,\n} from \"./layer-ref-scrub\";\nimport { hasSimpleStyleProperties } from \"./vector-color\";\nimport {\n applyCopiedLayerStyle,\n type CopiedLayerStyle,\n extractCopiedLayerStyle,\n} from \"./layer-style-clipboard\";\nimport { applyJoinsToLayer, cascadeLayerJoinRefresh, reapplyLayerJoins } from \"./joins\";\nimport { MAX_LAYER_LIBRARY_ENTRIES } from \"./layer-library\";\nimport {\n DEFAULT_ELLIPSOID_ID,\n getPlanetaryBasemapByStyleUrl,\n setActiveEllipsoidId,\n} from \"./ellipsoids\";\nimport type { PlanetaryBasemap } from \"./ellipsoids\";\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\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\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\nexport interface AppState {\n projectName: string;\n projectPath: string | null;\n projectGeneration: number;\n isDirty: boolean;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n layers: GeoLibreLayer[];\n layerGroups: LayerGroup[];\n preferences: ProjectPreferences;\n projectPlugins: ProjectPluginState | null;\n legend: LegendConfig;\n /** Print Layout composer settings for the open project (discussion #1992). */\n printLayout: PrintLayoutConfig;\n storymap: StoryMap | null;\n /** Saved processing pipelines (batch/model chaining; issue #344). */\n models: ProcessingModel[];\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 /** Recorded processing tool runs, oldest first (Processing History; #1292). */\n processingHistory: ProcessingRun[];\n /** Saved Dashboard panel chart widgets (issue #401). */\n widgets: DashboardWidget[];\n /** Number of columns in the Dashboard widget grid. */\n dashboardColumns: number;\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 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 identifyLayerId: 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 /** Anchored review comments on map points or features (issue #1518). */\n comments: ProjectComment[];\n metadata: Record<string, unknown>;\n recentProjects: RecentProjectEntry[];\n attributeFilter: string;\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 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 /** 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 // 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 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 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 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 /** 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 setPreferences: (preferences: ProjectPreferences) => void;\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 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 setAttributeFilter: (filter: string) => void;\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 setRasterAttributeTableOpen: (open: boolean) => void;\n setDashboardOpen: (open: boolean) => void;\n setStorymapPanelOpen: (open: boolean) => void;\n setStorymapPresenting: (presenting: boolean, returnToEditor?: boolean) => 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\n setStyleManagerOpen: (open: boolean) => void;\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 /** 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 /** 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\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 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 * 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 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 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 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 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\nconst MAX_RECENT_PROJECTS = 10;\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\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\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\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/** 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/**\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 */\nfunction 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 */\nfunction 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. */\nfunction 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/** 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\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 = useAppStore.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\nexport const useAppStore = create<AppState>()(\n temporal(\n (set, get) => ({\n projectName: DEFAULT_PROJECT_NAME,\n projectPath: null,\n projectGeneration: 0,\n isDirty: false,\n mapView: createDefaultMapView(),\n basemapStyleUrl: DEFAULT_BASEMAP,\n basemapVisible: true,\n basemapOpacity: 1,\n layers: [],\n layerGroups: [],\n preferences: DEFAULT_PROJECT_PREFERENCES,\n projectPlugins: null,\n legend: { ...DEFAULT_LEGEND_CONFIG },\n printLayout: createDefaultPrintLayout(),\n storymap: null,\n models: [],\n styleLibrary: [],\n layerLibrary: [],\n templateLibrary: [],\n projectStyleLibrary: [],\n processingHistory: [],\n widgets: [],\n dashboardColumns: DEFAULT_DASHBOARD_COLUMNS,\n mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT },\n secondaryMapViews: [],\n primaryMapLabel: \"\",\n copiedLayerStyle: null,\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n identifyLayerId: null,\n pointerCoords: null,\n pointerElevation: null,\n cameraAltitude: null,\n gpsStatus: null,\n comments: [],\n metadata: {},\n recentProjects: [],\n attributeFilter: \"\",\n collaboration: DEFAULT_COLLABORATION_STATE,\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 rasterAttributeTableOpen: false,\n dashboardOpen: false,\n storymapPanelOpen: false,\n storymapPresenting: false,\n storymapReturnToEditor: false,\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 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 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 setCollaboration: (patch) =>\n 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 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 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) => set({ basemapStyleUrl: url, isDirty: true }),\n applyPlanetaryBasemap: (basemap) =>\n set((state) => ({\n basemapStyleUrl: basemap.styleUrl,\n preferences:\n state.preferences.map.ellipsoidId === basemap.ellipsoidId\n ? state.preferences\n : {\n ...state.preferences,\n map: {\n ...state.preferences.map,\n ellipsoidId: basemap.ellipsoidId,\n },\n },\n isDirty: true,\n })),\n restoreEarthBasemap: (styleUrl) =>\n set((state) => ({\n basemapStyleUrl: styleUrl,\n preferences:\n state.preferences.map.ellipsoidId === DEFAULT_ELLIPSOID_ID\n ? state.preferences\n : {\n ...state.preferences,\n map: {\n ...state.preferences.map,\n ellipsoidId: DEFAULT_ELLIPSOID_ID,\n },\n },\n isDirty: true,\n })),\n setBasemapVisible: (visible) => set({ basemapVisible: visible, isDirty: true }),\n setBasemapOpacity: (opacity) => set({ basemapOpacity: opacity, isDirty: true }),\n setPreferences: (preferences) => set({ preferences, isDirty: true }),\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 selectLayer: (id) =>\n set({\n selectedLayerId: id,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n }),\n selectFeature: (id) => set({ selectedFeatureId: id, selectedFeatureIds: id ? [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 }),\n setAttributeFilter: (filter) => set({ attributeFilter: filter }),\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) =>\n 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 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 },\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 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) =>\n 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 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) =>\n s.processingHistory.length === 0 ? s : { processingHistory: [], isDirty: true },\n ),\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\n ? chapters.length\n : 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 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 addLayer: (layer, beforeLayerId = null) =>\n set((s) => {\n const layers = [...s.layers];\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 identifyLayerId: s.identifyLayerId === id ? null : s.identifyLayerId,\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) =>\n l.id === id ? applyJoinsToLayer(l, s.layers, joins) : l,\n );\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\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 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) =>\n l.id === id ? { ...l, style: { ...l.style, ...style } } : l,\n ),\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 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 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: options.type ?? \"xyz\",\n source: {\n // Extra source fields (e.g. WMS layers/styles) merge first so the\n // required raster descriptor below always wins.\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 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\n ? scrubWidgetsForRemovedLayers(s.widgets, removedIds)\n : 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 identifyLayerId:\n s.identifyLayerId !== null && removedIds.has(s.identifyLayerId)\n ? null\n : s.identifyLayerId,\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 below) so Ctrl-Z never toggles a folder.\n toggleLayerGroupCollapsed: (id) =>\n set((s) => ({\n layerGroups: s.layerGroups.map((g) =>\n g.id === id ? { ...g, collapsed: !g.collapsed } : g,\n ),\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 &&\n 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) =>\n g.id === id ? { ...g, parentId: nextParentId } : g,\n ),\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 newProject: (options = {}) => {\n const project = createEmptyProject(options.name, options);\n const applied = applyProjectToStore(project);\n set((s) => ({\n ...applied,\n projectPath: null,\n projectGeneration: s.projectGeneration + 1,\n isDirty: false,\n selectedLayerId: null,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n identifyLayerId: 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 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 projectPath: path,\n projectGeneration: s.projectGeneration + 1,\n isDirty: false,\n selectedLayerId,\n selectedFeatureId: null,\n selectedFeatureIds: [],\n identifyLayerId: null,\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 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 // 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 partialize: (s) => ({\n layers: s.layers,\n layerGroups: s.layerGroups,\n basemapStyleUrl: s.basemapStyleUrl,\n basemapVisible: s.basemapVisible,\n basemapOpacity: s.basemapOpacity,\n storymap: s.storymap,\n comments: s.comments,\n }),\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.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 = useAppStore.temporal.getState().pastStates;\n debounced(...args);\n if (useAppStore.temporal.getState().pastStates !== before) {\n if (!applyingProjectRestoreHistory && projectRestoreRedo) {\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\n }\n pruneHistoryBySize();\n }\n };\n },\n },\n ),\n);\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 * 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 = useAppStore.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 useAppStore.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 = useAppStore.temporal.getState();\n if (temporal.pastStates.length === 0 && projectRestoreUndo) {\n const entry = projectRestoreUndo;\n applyingProjectRestoreHistory = true;\n try {\n useAppStore.getState().loadProject(entry.before, entry.beforePath, {\n rememberRecent: false,\n presenting: false,\n });\n projectRestoreUndo = null;\n useAppStore.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 = useAppStore.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 = useAppStore.temporal.getState();\n if (temporal.futureStates.length === 0 && projectRestoreRedo) {\n const entry = projectRestoreRedo;\n applyingProjectRestoreHistory = true;\n try {\n useAppStore.getState().loadProject(entry.after, entry.afterPath, {\n rememberRecent: false,\n presenting: false,\n });\n projectRestoreRedo = null;\n useAppStore.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 = useAppStore.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 useAppStore.temporal.getState().clear();\n if (!applyingProjectRestoreHistory) {\n projectRestoreUndo = null;\n projectRestoreRedo = null;\n notifyProjectRestoreHistory();\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 * 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 network error, a non-2xx\n * 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\n const doFetch: FetchLike = fetchImpl ?? ((url, init) => fetch(url, init));\n const latitudes = points.map((p) => p[1].toFixed(6)).join(\",\");\n const longitudes = points.map((p) => 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","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","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 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","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","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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"}
|