@geolibre/core 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +0,0 @@
1
- {"version":3,"file":"project-diff-Bua7fBME.mjs","names":["newLayerId","isPlainObject","isPlainObject","uuidv4"],"sources":["../src/types.ts","../src/ellipsoids.ts","../src/regional-basemaps.ts","../src/cesium-imagery.ts","../src/arcgis-projection.ts","../src/geojson-z.ts","../src/polyline.ts","../src/selection.ts","../src/expressions.ts","../src/layer-descriptive-metadata.ts","../src/virtual-fields.ts","../src/joins.ts","../src/capabilities.ts","../src/layer-groups.ts","../src/layer-ref-scrub.ts","../src/print-layout-config.ts","../src/style-library.ts","../src/czml.ts","../src/cesium-kml.ts","../src/cesium-ion.ts","../src/header-references.ts","../src/credentials.ts","../src/project.ts","../src/project-diff.ts"],"sourcesContent":["import type { FeatureCollection } from \"geojson\";\nimport type { PrintLayoutConfig } from \"./print-layout-config\";\n\nexport const OPENFREEMAP_BASEMAPS = [\n {\n id: \"liberty\",\n name: \"Liberty\",\n styleUrl: \"https://tiles.openfreemap.org/styles/liberty\",\n },\n {\n id: \"liberty-3d\",\n name: \"Liberty 3D\",\n styleUrl: \"https://tiles.openfreemap.org/styles/liberty\",\n },\n {\n id: \"positron\",\n name: \"Positron\",\n styleUrl: \"https://tiles.openfreemap.org/styles/positron\",\n },\n {\n id: \"bright\",\n name: \"Bright\",\n styleUrl: \"https://tiles.openfreemap.org/styles/bright\",\n },\n {\n id: \"dark\",\n name: \"Dark\",\n styleUrl: \"https://tiles.openfreemap.org/styles/dark\",\n },\n {\n id: \"fiord\",\n name: \"Fiord\",\n styleUrl: \"https://tiles.openfreemap.org/styles/fiord\",\n },\n] as const;\n\n/**\n * Protomaps v5 basemap flavors. These are resolved to full style URLs at use\n * time by `getProtomapsStyleUrl`, which injects the `VITE_PROTOMAPS_API_KEY`\n * runtime env var. The key is only present in builds configured with it (e.g.\n * the GitHub Pages web demo), so consumers should hide these options when no\n * key is available.\n */\nexport const PROTOMAPS_BASEMAPS = [\n { id: \"protomaps-light\", name: \"Light\", flavor: \"light\" },\n { id: \"protomaps-dark\", name: \"Dark\", flavor: \"dark\" },\n { id: \"protomaps-white\", name: \"White\", flavor: \"white\" },\n { id: \"protomaps-grayscale\", name: \"Grayscale\", flavor: \"grayscale\" },\n { id: \"protomaps-black\", name: \"Black\", flavor: \"black\" },\n] as const;\n\nexport const DEFAULT_BASEMAP = \"https://tiles.openfreemap.org/styles/liberty\";\n\nexport const BLANK_BASEMAP = \"\";\n\nexport const PROJECT_VERSION = \"0.2.0\";\n\n/**\n * Every layer type, as a runtime list so untrusted input (an imported Layer\n * Library bundle, a hand-edited project) can be validated against it.\n * {@link LayerType} is derived from this array, so the two cannot drift.\n */\nexport const LAYER_TYPES = [\n \"geojson\",\n \"raster\",\n \"wms\",\n \"wmts\",\n \"xyz\",\n \"vector-tiles\",\n \"arcgis\",\n \"pmtiles\",\n \"mbtiles\",\n \"zarr\",\n \"lidar\",\n \"gaussian-splat\",\n \"3d-tiles\",\n \"cog\",\n \"flatgeobuf\",\n \"geoparquet\",\n \"duckdb-query\",\n \"deckgl-viz\",\n \"video\",\n \"image\",\n] as const;\n\nexport type LayerType = (typeof LAYER_TYPES)[number];\n\nexport type VectorStyleMode = \"single\" | \"graduated\" | \"categorized\" | \"rule-based\" | \"expression\";\n\n/**\n * One entry in a {@link LayerStyle.vectorRules} rule-based renderer. Each rule\n * pairs a MapLibre filter expression (a boolean expression array, serialized as\n * JSON) with a symbol color. Rules are evaluated top to bottom; the first whose\n * filter matches wins. The catch-all rule (`isElse`) has no filter and supplies\n * the color for features no other rule matched. Mirrors the QGIS/ArcGIS\n * rule-based renderer.\n */\nexport interface VectorRule {\n id: string;\n /** Human-readable label shown in the editor and the legend. */\n label: string;\n /**\n * A MapLibre boolean filter expression serialized as JSON, e.g.\n * `[\"==\", [\"get\", \"TYPE\"], \"park\"]`. Ignored when {@link isElse} is set or\n * the JSON is invalid.\n */\n filter: string;\n /** The symbol fill/circle color for features this rule matches (6-digit hex). */\n color: string;\n /**\n * When true this is the catch-all \"else\" rule: {@link filter} is unused and\n * the rule is an unconditional fallback for features no other rule matched,\n * so the {@link minZoom}/{@link maxZoom} and {@link parentId} fields are\n * ignored on it. Its symbol overrides and {@link enabled} toggle do apply.\n */\n isElse: boolean;\n /**\n * Whether the rule participates in rendering. `false` temporarily disables\n * the rule (and, for a group, its whole subtree) without deleting it,\n * mirroring the QGIS rule checkbox. Absent means enabled.\n */\n enabled?: boolean;\n /**\n * Lowest zoom (inclusive) the rule applies at, mirroring MapLibre's layer\n * `minzoom` convention. Absent means no lower bound.\n */\n minZoom?: number;\n /**\n * Zoom the rule stops applying at (exclusive, like MapLibre's layer\n * `maxzoom`). Absent means no upper bound. Together with {@link minZoom}\n * this is the QGIS per-rule \"scale range\".\n */\n maxZoom?: number;\n /**\n * The id of the parent rule when this rule is nested inside a group (QGIS\n * rule tree). A child rule matches only features that also match every\n * ancestor's filter, and inherits the intersection of the ancestors' zoom\n * ranges and enabled state. A rule that has children acts as a group: its\n * own symbol is not rendered; only leaf rules draw. Absent or dangling\n * means a top-level rule.\n */\n parentId?: string;\n /**\n * Per-rule stroke/outline color override (6-digit hex): the polygon outline\n * and circle stroke color for matching features. Absent inherits the layer\n * {@link LayerStyle.strokeColor}.\n */\n strokeColor?: string;\n /**\n * Per-rule stroke width override in pixels: the line width (lines and\n * polygon outlines) and circle stroke width for matching features. Absent\n * inherits the layer {@link LayerStyle.strokeWidth}.\n */\n strokeWidth?: number;\n /**\n * Per-rule fill/circle opacity override (0..1) for matching features.\n * Absent inherits the layer {@link LayerStyle.fillOpacity}.\n */\n fillOpacity?: number;\n /**\n * Per-rule circle radius override in pixels for matching point features.\n * Absent inherits the layer {@link LayerStyle.circleRadius}.\n */\n circleRadius?: number;\n}\n\n/**\n * The fill pattern applied to polygon layers. `\"none\"` keeps a flat fill; the\n * named patterns are generated as recolorable sprite tiles; `\"svg\"` rasterizes\n * the user-supplied markup in {@link LayerStyle.fillPatternSvg}.\n */\nexport type FillPattern =\n | \"none\"\n | \"hatch\"\n | \"cross-hatch\"\n | \"horizontal\"\n | \"vertical\"\n | \"dots\"\n | \"svg\";\n\n/**\n * The built-in marker shape for a point layer, or `\"custom\"` to rasterize the\n * user-supplied SVG in {@link LayerStyle.markerSvg}. Built-in shapes are drawn\n * on a sprite tile and recolored via {@link LayerStyle.markerColor}.\n */\nexport type MarkerShape =\n | \"circle\"\n | \"square\"\n | \"triangle\"\n | \"diamond\"\n | \"star\"\n | \"cross\"\n | \"pin\"\n | \"custom\";\n\n/**\n * How a point layer is rendered: as individual markers, a density heatmap, or\n * clustered bubbles. Only applies to point geometry.\n */\nexport type PointRenderer = \"single\" | \"heatmap\" | \"cluster\";\n\n/**\n * The repeated decoration symbol drawn along line features (and polygon\n * outlines), or `\"none\"` when decorations are off. Mirrors the QGIS\n * marker-line / arrow symbol layers: `\"arrow\"` renders directional arrowheads\n * that follow the line, the other shapes render as repeated markers.\n */\nexport type LineDecoration = \"none\" | \"arrow\" | \"triangle\" | \"circle\" | \"square\";\n\n/**\n * The per-feature derived geometry rendered by the geometry generator, or\n * `\"none\"` when the generator is off. Mirrors QGIS geometry-generator symbol\n * layers: each feature's derived geometry (its centroid, bounding box, convex\n * hull, or a buffer) is drawn as an extra symbol over the layer's normal\n * symbology. `\"centroid\"` derives points; the rest derive polygons.\n */\nexport type GeometryGeneratorType = \"none\" | \"centroid\" | \"bounding-box\" | \"convex-hull\" | \"buffer\";\n\n/**\n * Unit a stroke/line width is measured in. `\"pixels\"` is constant screen space;\n * `\"meters\"` is ground distance, so the rendered width scales with the map\n * scale (zoom). See {@link LayerStyle.strokeWidthUnit}.\n */\nexport type StrokeWidthUnit = \"pixels\" | \"meters\";\n\n/**\n * The chart drawn on top of each feature by the diagram renderer (QGIS-style\n * diagram symbology), or `\"none\"` when diagrams are off. Diagrams visualize\n * several numeric attributes per feature at once — e.g. election results by\n * party per county — and render through the shared deck.gl overlay on the\n * feature's point location or polygon centroid.\n */\nexport type DiagramType = \"none\" | \"pie\" | \"donut\" | \"bar\" | \"stacked-bar\";\n\n/**\n * How the overall diagram size is determined.\n *\n * - `\"fixed\"`: every diagram renders at {@link LayerStyle.diagramSize} pixels.\n * - `\"sum\"`: scaled by the sum of the mapped attribute values, so the largest\n * total renders at {@link LayerStyle.diagramSize} pixels (area-true square\n * root scaling).\n * - `\"attribute\"`: scaled the same way by the single numeric attribute in\n * {@link LayerStyle.diagramSizeProperty}.\n */\nexport type DiagramSizeMode = \"fixed\" | \"sum\" | \"attribute\";\n\n/** One attribute rendered as a slice/bar of a feature diagram. */\nexport interface DiagramField {\n /** Numeric feature property visualized by this slice/bar. */\n property: string;\n /** Slice/bar color (6-digit hex). */\n color: string;\n}\n\nexport interface VectorStyleStop {\n value: string | number;\n color: string;\n label?: string;\n}\n\n/** Attribute-driven labeling for a vector layer (rendered as a MapLibre symbol layer). */\nexport interface LabelStyle {\n /** Whether labels are shown for the layer. */\n enabled: boolean;\n /** Attribute field whose value becomes the label text. */\n field: string;\n /**\n * Optional MapLibre expression (JSON string) for the label text, which\n * overrides {@link field} when non-empty (e.g. concatenating several fields).\n */\n expression: string;\n /** `\"point\"` labels at the feature/centroid; `\"line\"` places them along lines. */\n placement: \"point\" | \"line\";\n /** Label text size in pixels. */\n size: number;\n /** CSS color string for the label text. */\n color: string;\n /** CSS color string for the text halo drawn behind the label. */\n haloColor: string;\n /** Width of the text halo in pixels. */\n haloWidth: number;\n /** Scale range for labels; `0` / `24` inherit the layer's own zoom range. */\n minZoom: number;\n maxZoom: number;\n /** Let labels overlap instead of hiding colliding ones. */\n allowOverlap: boolean;\n /**\n * Where the label sits relative to its anchor point (MapLibre `text-anchor`),\n * e.g. `\"top\"` places the text above the point. Ignored for line placement.\n */\n anchor: LabelAnchor;\n /** Horizontal label offset in ems (MapLibre `text-offset` x). */\n offsetX: number;\n /** Vertical label offset in ems (MapLibre `text-offset` y; positive is down). */\n offsetY: number;\n /** Label rotation in degrees clockwise (MapLibre `text-rotate`). */\n rotation: number;\n /** Maximum line width in ems before the label wraps (MapLibre `text-max-width`). */\n maxWidth: number;\n /** Letter-case transform applied to the label text (MapLibre `text-transform`). */\n transform: LabelTransform;\n /**\n * Render a numeric {@link field} with the locale's thousands and decimal\n * separators (issue #2336), so `1234567.5` labels as `1,234,567.5` instead\n * of running together. Non-numeric values are unaffected, and it is not\n * applied to {@link expression}, which formats its own output (MapLibre's\n * `number-format`, offered in the Expression Builder).\n */\n numberFormatEnabled: boolean;\n /** Decimal places kept while {@link numberFormatEnabled} is on (0-10). */\n numberDecimals: number;\n /**\n * BCP 47 tag picking the separators for {@link numberFormatEnabled}, from\n * {@link LABEL_NUMBER_LOCALES}. Empty (the default) follows the app's own\n * language, the way popup number fields do.\n */\n numberLocale: string;\n /**\n * How to handle features that share a label.\n *\n * - `\"off\"`: every feature is labeled (the historical behavior).\n * - `\"unique\"`: features stacked at the same point are collapsed to a single\n * label, so co-located points (e.g. several antennas at one cell site) do not\n * stack overlapping text.\n * - `\"concatenate\"`: co-located points are merged into one label that joins\n * their distinct {@link field} values, one per line.\n *\n * Applies to point layers rendered through the inline GeoJSON path and uses\n * {@link field} (not {@link expression}) as the label value.\n */\n dedupe: LabelDedupe;\n /**\n * Data-defined override for {@link size}: a MapLibre expression (JSON\n * string) producing a number, e.g. sizing labels by population. Empty means\n * \"use the literal {@link size}\". Like the other data-defined overrides\n * below, it reads source feature attributes, so it is skipped while\n * {@link dedupe} collapsing is active (the aggregated features carry only\n * the label value).\n */\n sizeExpression: string;\n /**\n * Data-defined override for {@link color}: a MapLibre expression (JSON\n * string) producing a color, e.g. coloring labels by category.\n */\n colorExpression: string;\n /**\n * Data-defined label opacity: a MapLibre expression (JSON string) producing\n * a number in 0..1. When set it replaces the layer-wide opacity for labels\n * (wrapping it would invalidate top-level `[\"zoom\"]` interpolations).\n */\n opacityExpression: string;\n /**\n * Per-feature label visibility: a MapLibre expression (JSON string)\n * producing a boolean. Features evaluating false get no label (e.g. hide\n * labels below an attribute threshold). Combined with the layer's other\n * feature filters.\n */\n visibilityExpression: string;\n /**\n * Per-feature placement priority: a MapLibre expression (JSON string)\n * producing a number, applied as `symbol-sort-key`. Labels with lower\n * values are placed first, so they win when space is tight.\n */\n priorityExpression: string;\n}\n\n/** MapLibre `text-anchor` positions offered for {@link LabelStyle.anchor}. */\nexport type LabelAnchor =\n | \"center\"\n | \"left\"\n | \"right\"\n | \"top\"\n | \"bottom\"\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\";\n\n/** Letter-case transform offered for {@link LabelStyle.transform}. */\nexport type LabelTransform = \"none\" | \"uppercase\" | \"lowercase\";\n\n/** Duplicate-label handling offered for {@link LabelStyle.dedupe}. */\nexport type LabelDedupe = \"off\" | \"unique\" | \"concatenate\";\n\n/**\n * Cartographic blend modes offered for {@link LayerStyle.blendMode}.\n *\n * The list is bounded by what WebGL *fixed-function* blending can express --\n * one `blendFunc` factor pair plus one `blendEquation` -- because that is the\n * only blend state MapLibre's renderer exposes, and it applies to colour and\n * alpha together. Two consequences shaped this list:\n *\n * - Modes needing the destination colour inside a fragment shader (`overlay`,\n * `color-dodge`, `soft-light`, `difference`, and the non-separable HSL modes)\n * are absent. MapLibre draws straight into the map framebuffer, which a\n * shader cannot sample, so computing them would need an extra copy pass.\n * - `darken` and `subtract` are absent even though `MIN` and\n * `FUNC_REVERSE_SUBTRACT` look like they would serve. Both also apply to the\n * alpha channel, where they drive the map canvas transparent: `MIN` takes the\n * whole canvas to `min(0, dst) = 0` everywhere the layer does not cover, and\n * `FUNC_REVERSE_SUBTRACT` leaves `dstA - srcA` inside it. Correcting either\n * needs `blendEquationSeparate` / `blendFuncSeparate`, which MapLibre's state\n * tracker does not offer.\n *\n * Every mode here leaves the canvas opaque and leaves the map untouched\n * wherever the layer contributes nothing. See `docs/user-guide/layers.md`.\n */\nexport const BLEND_MODES = [\"normal\", \"multiply\", \"screen\", \"lighten\", \"add\"] as const;\n\n/**\n * How a layer's pixels combine with the map beneath it. `\"normal\"` is ordinary\n * alpha compositing (the default, and what every layer did before blend modes\n * existed). See {@link BLEND_MODES}.\n */\nexport type BlendMode = (typeof BLEND_MODES)[number];\n\n/**\n * The mode a layer renders with unless it says otherwise. Declared separately\n * from {@link DEFAULT_LAYER_STYLE} so consumers get a non-optional\n * {@link BlendMode} to fall back to: `blendMode` is optional on\n * {@link LayerStyle}, so `DEFAULT_LAYER_STYLE.blendMode` is typed\n * `BlendMode | undefined` however it is initialized.\n */\nexport const DEFAULT_BLEND_MODE: BlendMode = \"normal\";\n\nexport interface LayerStyle {\n minZoom: number;\n maxZoom: number;\n fillColor: string;\n strokeColor: string;\n strokeWidth: number;\n /**\n * Unit the {@link strokeWidth} value is expressed in.\n *\n * - `\"pixels\"` (default): a constant screen-space width that never changes\n * with zoom — the historical behavior.\n * - `\"meters\"`: a ground-distance width, so the rendered line stays\n * proportional to the map scale (thicker zoomed in, thinner zoomed out),\n * matching QGIS \"map units\". Only affects line/polygon-outline rendering;\n * point/circle outlines remain pixel-based.\n */\n strokeWidthUnit: StrokeWidthUnit;\n fillOpacity: number;\n circleRadius: number;\n textColor: string;\n textHaloColor: string;\n textHaloWidth: number;\n textSize: number;\n labels: LabelStyle;\n extrusionEnabled: boolean;\n extrusionColor: string;\n extrusionOpacity: number;\n extrusionHeightProperty: string;\n extrusionHeightScale: number;\n extrusionBase: number;\n extrusionAdvancedStyleEnabled: boolean;\n extrusionColorExpression: string;\n extrusionHeightExpression: string;\n /**\n * When true, a vector layer whose coordinates carry Z values (e.g. a GPX\n * track with elevations) renders in true 3D through the shared deck.gl\n * overlay instead of MapLibre's flat 2D layers, so features sit at their\n * own altitude. Orthogonal to {@link extrusionEnabled}, which extrudes flat\n * polygons by an attribute; only one of the two should be on at a time.\n */\n elevation3dEnabled: boolean;\n /** Multiplier applied to each coordinate's Z value (vertical exaggeration). */\n elevation3dVerticalScale: number;\n /** Constant altitude offset in meters added after the vertical scale. */\n elevation3dOffset: number;\n vectorStyleMode: VectorStyleMode;\n vectorStyleProperty: string;\n vectorStyleClassCount: number;\n vectorStyleColorRamp: string;\n vectorStyleClassificationScheme: string;\n vectorStyleStops: VectorStyleStop[];\n vectorStyleExpression: string;\n /**\n * Ordered rules for the `\"rule-based\"` {@link vectorStyleMode}. Compiled to a\n * MapLibre `case` color expression (first matching filter wins, catch-all\n * last). See {@link VectorRule}.\n */\n vectorRules: VectorRule[];\n /**\n * When true, the point circle radius (or line width) is sized by a numeric\n * field via an `interpolate` between {@link proportionalSizeMinValue} ..\n * {@link proportionalSizeMaxValue} mapped onto {@link proportionalSizeMinRadius}\n * .. {@link proportionalSizeMaxRadius} (QGIS \"graduated → size\" / proportional\n * symbols). Orthogonal to the color {@link vectorStyleMode}.\n */\n proportionalSizeEnabled: boolean;\n proportionalSizeProperty: string;\n proportionalSizeMinValue: number;\n proportionalSizeMaxValue: number;\n proportionalSizeMinRadius: number;\n proportionalSizeMaxRadius: number;\n /**\n * Polygon fill pattern. `\"none\"` keeps the flat fill; other values render a\n * recolorable sprite tile ({@link fillPatternColor}); `\"svg\"` rasterizes\n * {@link fillPatternSvg}. See {@link FillPattern}.\n */\n fillPattern: FillPattern;\n fillPatternColor: string;\n /** Raw SVG markup (or a data URL) used when {@link fillPattern} is `\"svg\"`. */\n fillPatternSvg: string;\n /**\n * When true, point features render as a marker icon ({@link markerShape})\n * instead of a plain circle. Built-in shapes are recolored via\n * {@link markerColor}; `\"custom\"` rasterizes {@link markerSvg}.\n */\n markerEnabled: boolean;\n markerShape: MarkerShape;\n markerColor: string;\n markerSize: number;\n /**\n * The custom marker image used when {@link markerShape} is `\"custom\"`: raw\n * SVG markup, a `data:` URL (SVG or a raster PNG/JPEG/GIF), or an `http(s)`\n * URL. Only SVG sources take {@link markerColor}; raster images draw as-is.\n */\n markerSvg: string;\n /**\n * When true, per-feature [simplestyle-spec](https://github.com/mapbox/simplestyle-spec)\n * properties (`fill`, `fill-opacity`, `stroke`, `stroke-width`,\n * `stroke-opacity`, `marker-color`) override the flat layer style on a\n * per-feature basis. Set automatically when a GeoJSON layer is added whose\n * features carry these properties (e.g. styled KML/KMZ), so embedded\n * symbology renders without manual configuration.\n */\n simpleStyleEnabled: boolean;\n /**\n * Per-feature chart symbology (QGIS-style diagrams). `\"none\"` disables it;\n * any other value renders one {@link DiagramType} chart per feature over the\n * layer's normal symbology, built from the numeric attributes in\n * {@link diagramFields}. See `@geolibre/core`'s `diagram.ts` helpers.\n */\n diagramType: DiagramType;\n /** Attributes (and their colors) charted by the diagram renderer, in order. */\n diagramFields: DiagramField[];\n /** How the per-feature diagram size is determined. */\n diagramSizeMode: DiagramSizeMode;\n /**\n * Diagram size in pixels: the rendered diameter/height for `\"fixed\"` sizing,\n * or the diameter/height of the largest feature for scaled sizing.\n */\n diagramSize: number;\n /** Numeric attribute driving `\"attribute\"` sizing (see {@link DiagramSizeMode}). */\n diagramSizeProperty: string;\n /** Minimum zoom at which diagrams are drawn, to avoid clutter when zoomed out. */\n diagramMinZoom: number;\n /**\n * When true, diagrams that would overlap an already-placed diagram on screen\n * are skipped (largest first), decluttering dense areas. Recomputed as the\n * view changes.\n */\n diagramDeclutter: boolean;\n pointRenderer: PointRenderer;\n heatmapRadius: number;\n heatmapIntensity: number;\n /** Built-in color ramp used by the heatmap density renderer. */\n heatmapColorRamp: string;\n /** Numeric feature property used as heatmap weight; blank gives every point equal weight. */\n heatmapWeightProperty: string;\n clusterRadius: number;\n clusterMaxZoom: number;\n /**\n * When true, the polygon fill renders *inverted*: the area outside the\n * features is filled (with {@link fillColor}/{@link fillOpacity}) and the\n * features themselves become holes, mirroring the QGIS \"Inverted polygons\"\n * renderer. Feature outlines still render normally. Only applies to layers\n * with polygon geometry; ignored while {@link extrusionEnabled} is on.\n */\n invertedFillEnabled: boolean;\n /**\n * Repeated decoration symbol drawn along line features and polygon outlines\n * (QGIS marker-line / arrow lines). `\"none\"` disables it. See\n * {@link LineDecoration}.\n */\n lineDecoration: LineDecoration;\n /**\n * Decoration symbol color (6-digit hex). An empty string inherits\n * {@link strokeColor} so decorations follow the stroke by default.\n */\n lineDecorationColor: string;\n /** Decoration symbol size in pixels. */\n lineDecorationSize: number;\n /** Distance between consecutive decoration symbols in pixels. */\n lineDecorationSpacing: number;\n /**\n * Per-feature derived geometry drawn over the layer's normal symbology\n * (QGIS geometry generator). `\"none\"` disables it. See\n * {@link GeometryGeneratorType}.\n */\n geometryGenerator: GeometryGeneratorType;\n /** Buffer distance in meters for the `\"buffer\"` generator. */\n geometryGeneratorBufferDistance: number;\n /**\n * Attribute driving the `\"buffer\"` generator's distance, so each feature is\n * buffered by its own value in meters (QGIS data-defined override on a\n * geometry-generator symbol). An empty string buffers every feature by the\n * flat {@link geometryGeneratorBufferDistance}, which also stands in for\n * features whose value is missing or non-numeric.\n */\n geometryGeneratorBufferProperty: string;\n /** Fill color (6-digit hex) for generated polygons and centroid points. */\n geometryGeneratorFillColor: string;\n /** Outline color (6-digit hex) for generated geometry. */\n geometryGeneratorStrokeColor: string;\n /** Outline width in pixels for generated geometry. */\n geometryGeneratorStrokeWidth: number;\n /** Fill opacity (0..1) for generated polygons and centroid points. */\n geometryGeneratorOpacity: number;\n /** Circle radius in pixels for generated centroid points. */\n geometryGeneratorCircleRadius: number;\n /**\n * Attribute driving the radius of generated centroid points, scaling them\n * between {@link geometryGeneratorSizeMinRadius} and\n * {@link geometryGeneratorSizeMaxRadius} across\n * {@link geometryGeneratorSizeMinValue} ..\n * {@link geometryGeneratorSizeMaxValue} (proportional symbols on the derived\n * centroids). An empty string draws every centroid at the flat\n * {@link geometryGeneratorCircleRadius}.\n *\n * Deliberately separate from {@link proportionalSizeProperty}: that one also\n * drives line width, so reusing it here would resize a polygon layer's\n * outlines as a side effect of sizing its centroids.\n */\n geometryGeneratorSizeProperty: string;\n geometryGeneratorSizeMinValue: number;\n geometryGeneratorSizeMaxValue: number;\n geometryGeneratorSizeMinRadius: number;\n geometryGeneratorSizeMaxRadius: number;\n rasterBrightnessMin: number;\n rasterBrightnessMax: number;\n rasterSaturation: number;\n rasterContrast: number;\n rasterHueRotate: number;\n /**\n * How this layer composites onto whatever is drawn beneath it. `\"normal\"`\n * (the default) is ordinary alpha compositing; `\"multiply\"` is the classic\n * cartographic case of laying colour over a hillshade so the relief still\n * reads through. Applies to the layer's own rendered geometry and raster\n * tiles, not to its labels, which stay legible on top. See\n * {@link BLEND_MODES} for the available modes and why the list is what it is.\n */\n blendMode?: BlendMode;\n}\n\nexport const DEFAULT_LAYER_STYLE: LayerStyle = {\n minZoom: 0,\n maxZoom: 24,\n fillColor: \"#3b82f6\",\n strokeColor: \"#1e40af\",\n strokeWidth: 2,\n strokeWidthUnit: \"pixels\",\n fillOpacity: 0.6,\n circleRadius: 6,\n textColor: \"#111827\",\n textHaloColor: \"#ffffff\",\n textHaloWidth: 2,\n textSize: 16,\n labels: {\n enabled: false,\n field: \"\",\n expression: \"\",\n placement: \"point\",\n size: 13,\n color: \"#111827\",\n haloColor: \"#ffffff\",\n haloWidth: 1.5,\n minZoom: 0,\n maxZoom: 24,\n allowOverlap: false,\n anchor: \"center\",\n offsetX: 0,\n offsetY: 0,\n rotation: 0,\n maxWidth: 10,\n transform: \"none\",\n numberFormatEnabled: false,\n numberDecimals: 0,\n numberLocale: \"\",\n dedupe: \"off\",\n sizeExpression: \"\",\n colorExpression: \"\",\n opacityExpression: \"\",\n visibilityExpression: \"\",\n priorityExpression: \"\",\n },\n extrusionEnabled: false,\n extrusionColor: \"#3b82f6\",\n extrusionOpacity: 0.8,\n extrusionHeightProperty: \"height\",\n extrusionHeightScale: 1,\n extrusionBase: 0,\n extrusionAdvancedStyleEnabled: false,\n extrusionColorExpression: \"\",\n extrusionHeightExpression: \"\",\n elevation3dEnabled: false,\n elevation3dVerticalScale: 1,\n elevation3dOffset: 0,\n vectorStyleMode: \"single\",\n vectorStyleProperty: \"\",\n vectorStyleClassCount: 5,\n vectorStyleColorRamp: \"viridis\",\n vectorStyleClassificationScheme: \"equal-interval\",\n vectorStyleStops: [\n { value: 0, color: \"#dbeafe\" },\n { value: 1, color: \"#2563eb\" },\n ],\n vectorStyleExpression: \"\",\n vectorRules: [],\n proportionalSizeEnabled: false,\n proportionalSizeProperty: \"\",\n proportionalSizeMinValue: 0,\n proportionalSizeMaxValue: 100,\n proportionalSizeMinRadius: 4,\n proportionalSizeMaxRadius: 24,\n fillPattern: \"none\",\n fillPatternColor: \"#1e40af\",\n fillPatternSvg: \"\",\n markerEnabled: false,\n markerShape: \"circle\",\n markerColor: \"#3b82f6\",\n markerSize: 18,\n markerSvg: \"\",\n simpleStyleEnabled: false,\n diagramType: \"none\",\n diagramFields: [],\n diagramSizeMode: \"fixed\",\n diagramSize: 30,\n diagramSizeProperty: \"\",\n diagramMinZoom: 0,\n diagramDeclutter: false,\n pointRenderer: \"single\",\n heatmapRadius: 30,\n heatmapIntensity: 1,\n heatmapColorRamp: \"turbo\",\n heatmapWeightProperty: \"\",\n clusterRadius: 50,\n clusterMaxZoom: 14,\n invertedFillEnabled: false,\n lineDecoration: \"none\",\n lineDecorationColor: \"\",\n lineDecorationSize: 12,\n lineDecorationSpacing: 80,\n geometryGenerator: \"none\",\n geometryGeneratorBufferDistance: 1000,\n geometryGeneratorBufferProperty: \"\",\n geometryGeneratorFillColor: \"#f59e0b\",\n geometryGeneratorStrokeColor: \"#b45309\",\n geometryGeneratorStrokeWidth: 2,\n geometryGeneratorOpacity: 0.4,\n geometryGeneratorCircleRadius: 5,\n geometryGeneratorSizeProperty: \"\",\n geometryGeneratorSizeMinValue: 0,\n geometryGeneratorSizeMaxValue: 100,\n geometryGeneratorSizeMinRadius: 4,\n geometryGeneratorSizeMaxRadius: 24,\n rasterBrightnessMin: 0,\n rasterBrightnessMax: 1,\n rasterSaturation: 0,\n rasterContrast: 0,\n rasterHueRotate: 0,\n blendMode: DEFAULT_BLEND_MODE,\n};\n\n/**\n * Read a layer style property, falling back to the shared default when the\n * layer does not define it. Shared by `@geolibre/map` and the desktop app so\n * the two consumers cannot drift.\n */\nexport function styleValue<K extends keyof LayerStyle>(style: LayerStyle, key: K): LayerStyle[K] {\n return style[key] ?? DEFAULT_LAYER_STYLE[key];\n}\n\n/**\n * Feature-count threshold above which a local vector (GeoJSON) layer is rendered\n * through client-side vector tiles (geojson-vt / supercluster served by a custom\n * MapLibre protocol) instead of one in-memory geojson source pushed via\n * `setData`. Small layers stay on the simpler inline path. Mirrors the\n * `MAX_CEREUS_FEATURES` precedent in the desktop SQL engine.\n */\nexport const LARGE_VECTOR_FEATURE_THRESHOLD = 50_000;\n\n/**\n * Decide whether a GeoJSON layer should use the tiled rendering path.\n *\n * @param geojson - The layer's feature collection (may be undefined for\n * non-vector layers).\n * @returns `true` when the collection exceeds\n * {@link LARGE_VECTOR_FEATURE_THRESHOLD} features.\n */\nexport function shouldUseTiledRendering(geojson: GeoJSON.FeatureCollection | undefined): boolean {\n return (geojson?.features.length ?? 0) > LARGE_VECTOR_FEATURE_THRESHOLD;\n}\n\n/**\n * Match statistics from the last time a {@link LayerJoin} was applied, shown in\n * the Joins UI so silent key mismatches are visible (mirrors QGIS's join\n * feedback). Recomputed on every apply; persisted harmlessly with the join.\n */\nexport interface LayerJoinStats {\n /** Target features whose key matched a join-table row. */\n matchedCount: number;\n /** Target features with no matching join-table row (their joined columns are null). */\n unmatchedTargetCount: number;\n /** Join-table rows (with a non-empty key) that matched no target feature. */\n unmatchedJoinCount: number;\n}\n\n/**\n * A persistent attribute join attached to a layer (QGIS Layer Properties →\n * Joins): a live left join that augments the layer's attribute table with\n * columns from another layer (typically a geometry-less table added via\n * Delimited Text with no coordinate fields) matched on a key field. Unlike the\n * Processing → Vector attribute join, the layer keeps its identity — styles,\n * labels, and position — and the joined columns refresh whenever the join\n * table's data changes. Definitions persist in `.geolibre.json` and re-resolve\n * on project load. See `joins.ts` for the engine.\n */\nexport interface LayerJoin {\n /** Stable id for list edits. */\n id: string;\n /** Id of the layer providing the joined columns (its geometry is ignored). */\n joinLayerId: string;\n /** Key field on the layer that owns this join. */\n targetField: string;\n /** Key field on the join layer. */\n joinField: string;\n /**\n * Join-layer fields to bring over; `undefined` brings every field except the\n * key. Names are the join layer's own (pre-prefix) field names.\n */\n fields?: string[];\n /** Optional prefix prepended to every joined column name (as in QGIS). */\n prefix?: string;\n /** `false` detaches the joined columns without deleting the definition. */\n enabled?: boolean;\n /**\n * Bookkeeping written by the engine: the output column names this join added\n * to the layer's features. Applying joins strips these first, which makes\n * re-application idempotent without keeping a duplicate copy of the base\n * data (base columns always win a name collision, so a joined column never\n * shadows one). Not user-editable.\n */\n addedFields?: string[];\n /** Last-run match statistics; see {@link LayerJoinStats}. */\n stats?: LayerJoinStats;\n}\n\n/** Edit-widget kinds the Attribute Form designer can assign to a field. */\nexport type AttributeFormWidget = \"text\" | \"number\" | \"range\" | \"checkbox\" | \"date\" | \"valueMap\";\n\n/** One selectable entry of a `valueMap` widget (stored value + display label). */\nexport interface AttributeFormValueMapEntry {\n /** The value written to the feature property (compared as a string). */\n value: string;\n /** Human-readable label shown in the dropdown; defaults to {@link value}. */\n label?: string;\n}\n\n/**\n * Per-field configuration authored in the Attribute Form designer (layer\n * properties → Attributes Form, QGIS-style): which edit widget the attribute\n * editing surfaces render for the field, plus optional expression-based\n * constraints and conditional visibility. Consumed by the attribute table's\n * inline editor and the Field Collection capture form; helpers live in\n * `attribute-form.ts`.\n */\nexport interface AttributeFormFieldConfig {\n /** Feature property key this configuration applies to. */\n field: string;\n widget: AttributeFormWidget;\n /** Display label override shown in forms instead of the raw field name. */\n alias?: string;\n /**\n * When true, a null/empty value fails validation. Checkbox widgets are\n * exempt: unchecked is a valid state, not a missing value.\n */\n required?: boolean;\n /** Dropdown entries for the `valueMap` widget. */\n valueMap?: AttributeFormValueMapEntry[];\n /**\n * Declared storage type of the field. When set, coercion follows it instead\n * of guessing from the value map (a text field whose codes are \"1\" and \"2\"\n * still stores strings), and validation rejects values of another type or,\n * for `integer`, fractional numbers. Service-derived configs (ArcGIS domains)\n * set it from the field's declared type; designer-authored configs leave it\n * unset.\n */\n valueType?: \"string\" | \"number\" | \"integer\";\n /** Lower bound for `number`/`range` widgets (inclusive). */\n min?: number;\n /** Upper bound for `number`/`range` widgets (inclusive). */\n max?: number;\n /** Step for the `range` widget's input. */\n step?: number;\n /**\n * Boolean MapLibre expression that must evaluate to `true` against the\n * feature's (candidate) properties for the value to be accepted, e.g.\n * `[\">\", [\"get\", \"population\"], 0]`. Stored as the expression source string\n * the Expression Builder edits.\n */\n constraintExpression?: string;\n /** Human-readable message shown when the constraint fails. */\n constraintDescription?: string;\n /**\n * Boolean MapLibre expression controlling whether the field is shown in\n * attribute forms; `false` hides the field (and skips its validation).\n * Empty/invalid expressions fail open so a typo cannot hide data entry.\n */\n visibilityExpression?: string;\n}\n\n/** The Attribute Form designer's whole per-layer configuration. */\nexport interface AttributeFormConfig {\n fields: AttributeFormFieldConfig[];\n}\n\n/** Point of contact recorded in a layer's {@link LayerDescriptiveMetadata}. */\nexport interface LayerMetadataContact {\n name?: string;\n email?: string;\n organization?: string;\n}\n\n/** A related resource recorded in a layer's {@link LayerDescriptiveMetadata}. */\nexport interface LayerMetadataLink {\n /** Absolute URL of the resource. */\n href: string;\n /** Relation type (STAC/IANA link relation such as `\"license\"` or `\"related\"`). */\n rel?: string;\n /** Human-readable label. */\n title?: string;\n}\n\n/**\n * Time span the layer's data covers. Each bound is an ISO 8601 date\n * (`YYYY-MM-DD`) or date-time; either may be omitted for an open interval.\n */\nexport interface LayerMetadataTemporalExtent {\n start?: string;\n end?: string;\n}\n\n/**\n * User-authored, catalog-style description of a layer (issue #2858), edited in\n * the layer's Metadata dialog and exported as a STAC Item. Distinct from the\n * internal {@link GeoLibreLayer.metadata} record the renderers and plugins key\n * off: every field here is free-form documentation that changes nothing about\n * how the layer renders. Helpers live in `layer-descriptive-metadata.ts`.\n */\nexport interface LayerDescriptiveMetadata {\n title?: string;\n /** Free-text summary of the data (STAC `description`, ISO abstract). */\n abstract?: string;\n keywords?: string[];\n /** SPDX license identifier (e.g. `\"CC-BY-4.0\"`) or free text. */\n license?: string;\n /** Credit line for the data's producers. */\n attribution?: string;\n contact?: LayerMetadataContact;\n /** How the data was produced: sources and processing steps. */\n lineage?: string;\n temporalExtent?: LayerMetadataTemporalExtent;\n links?: LayerMetadataLink[];\n}\n\n/**\n * How a popup renders one field's value (issue #2113). `\"auto\"` reproduces the\n * untyped rendering the Identify popup has always done — sanitized KML\n * `description` markup, an inline `data:image/*;base64` value as a thumbnail,\n * anything else stringified. The remaining kinds are explicit author choices\n * so a popup never has to guess from the value.\n */\nexport type PopupFieldKind = \"auto\" | \"text\" | \"number\" | \"date\" | \"link\" | \"image\";\n\n/** How a `\"date\"` field's value is written out. */\nexport type PopupDateFormat = \"date\" | \"datetime\" | \"time\" | \"iso\" | \"year\";\n\n/** Value formatting for one popup field. Every part is optional. */\nexport interface PopupFieldFormat {\n /** Fixed number of decimals for a `\"number\"` field. */\n decimals?: number;\n /** Group thousands with the locale's separator (`\"number\"` fields). */\n thousands?: boolean;\n /** Date rendering for a `\"date\"` field; defaults to `\"date\"`. */\n dateFormat?: PopupDateFormat;\n /** Text placed before the formatted value. */\n prefix?: string;\n /** Text placed after the formatted value, e.g. a unit suffix. */\n suffix?: string;\n /** Link text for a `\"link\"` field; the value itself is used when unset. */\n linkLabel?: string;\n}\n\n/** One field's entry in a layer's popup configuration. */\nexport interface PopupFieldConfig {\n /** Feature property key. */\n field: string;\n /** Display label shown instead of the raw field name. */\n label?: string;\n /** How the value renders; defaults to `\"auto\"`. */\n kind?: PopupFieldKind;\n format?: PopupFieldFormat;\n /** Include this field in the hover tooltip's short subset. */\n hover?: boolean;\n}\n\n/**\n * Per-layer configuration for what a viewer sees when they interact with a\n * feature (issue #2113): which fields the Identify popup shows, in what order,\n * under what labels and formatting, plus an optional hover tooltip.\n *\n * A layer with no `popup` block behaves exactly as it did before this existed:\n * the layer name as the heading, then every property as a key/value row.\n * {@link GeoLibreLayer.fieldVisibility} stays authoritative — a `\"hidden\"` or\n * `\"excluded\"` field is dropped even when a popup config names it.\n */\nexport interface LayerPopupConfig {\n /** `false` suppresses the Identify popup for this layer. Defaults to `true`. */\n click?: boolean;\n /** `true` shows a hover tooltip built from the `hover` fields. Defaults to `false`. */\n hover?: boolean;\n /** Field whose value titles the popup instead of the layer name. */\n titleField?: string;\n /**\n * MapLibre expression source producing the popup title. Wins over\n * {@link titleField}; when it fails or produces nothing the title falls\n * through to `titleField`, and only then to the layer name.\n */\n titleExpression?: string;\n /**\n * MapLibre expression source producing the whole popup body as text, for\n * authors who want a sentence rather than a table. When it evaluates, it\n * replaces the field rows.\n */\n bodyExpression?: string;\n /** `false` drops the synthetic `id` row. Defaults to `true`. */\n showFeatureId?: boolean;\n /**\n * Widest the click popup may grow, in CSS pixels. Unset keeps the default\n * (520px, or 420px for a popup carrying an image). Clamped to the range\n * `resolvePopupMaxWidth` enforces and always capped by the viewport, so a\n * value wider than the window still leaves the map usable.\n */\n maxWidth?: number;\n /**\n * Tallest an `\"image\"` field's thumbnail may draw inside the popup, in CSS\n * pixels. Unset keeps the default (`min(50vh, 420px)`). Clamped by\n * `resolvePopupImageHeight`. Pair it with {@link maxWidth} for a\n * bigger picture: the thumbnail keeps its aspect ratio, so widening the\n * popup is what lets a landscape photo use the extra height.\n */\n imageHeight?: number;\n /**\n * The fields to show and their order. An empty or absent list keeps today's\n * behavior: every visible property, in the feature's own key order.\n */\n fields?: PopupFieldConfig[];\n}\n\n/**\n * A virtual field attached to a vector layer (QGIS Field Calculator → \"Create\n * virtual field\", issue #1321): a column defined by a MapLibre expression that\n * recomputes live instead of being written once as static values. The engine\n * (`virtual-fields.ts`) materializes the computed values into the layer's\n * feature properties — so the attribute table, Expression Builder,\n * data-driven styling, labels, and selection all see the column with no\n * further wiring — and re-derives them whenever the layer's data (or its\n * joins) change. Definitions persist in `.geolibre.json` and re-resolve on\n * project load; the expression is a declarative MapLibre expression (never\n * arbitrary code), so re-evaluating it from a shared project file is safe.\n */\nexport interface LayerVirtualField {\n /** Stable id for list edits. */\n id: string;\n /** The output column name. A name already taken by a base column is skipped. */\n name: string;\n /**\n * MapLibre expression source (JSON text, e.g. `[\"/\", [\"get\", \"pop\"],\n * [\"get\", \"area_km2\"]]`) evaluated against each feature.\n */\n expression: string;\n /** `false` detaches the computed column without deleting the definition. */\n enabled?: boolean;\n /**\n * Bookkeeping written by the engine: the column name actually materialized\n * on the last apply, absent when the field was disabled, failed to compile,\n * or was skipped because the name collided with an existing column.\n * Applying virtual fields strips these first, which makes re-application\n * idempotent (an existing column is never shadowed, so stripping exactly\n * restores the pre-apply properties). Not user-editable.\n */\n addedField?: string;\n /** Compile error from the last apply, when the expression failed to parse. */\n error?: string;\n /**\n * Features whose evaluation threw at runtime on the last apply (their cell\n * is null). Surfaced so the UI can warn without one bad feature aborting\n * the whole column.\n */\n errorCount?: number;\n}\n\n/**\n * Quick filters (issue #2114): the data-driven filter controls a layer offers\n * in its Quick Filters section. What persists is the *control state* — field,\n * kind, chosen values — not the compiled output; `quick-filters.ts` compiles it\n * to a MapLibre filter at sync time so a saved filter can always be reopened\n * and edited.\n */\n\n/** Which control a quick filter renders, and how it compiles. */\nexport type QuickFilterKind = \"categorical\" | \"range\" | \"date\" | \"text\";\n\n/** The comparison a `text` quick filter applies (always case-insensitive). */\nexport type QuickFilterTextOperator = \"contains\" | \"startsWith\" | \"equals\";\n\n/**\n * How a date field stores its values, deciding whether a `date` quick filter\n * compares ISO text or epoch numbers. `iso` covers both `YYYY-MM-DD` and full\n * `YYYY-MM-DDTHH:MM:SSZ` timestamps: only the leading `YYYY-MM-DD` slice is\n * compared, so a trailing time, a milliseconds fraction, or a `Z` cannot break\n * a boundary. Mirrors (deliberately, in a smaller form) the value kinds the\n * Time Slider's `detectValueKind` reports.\n */\nexport type QuickFilterDateKind = \"iso\" | \"epochMs\" | \"epochS\";\n\n/** A single filter control persisted on a layer. */\nexport interface LayerQuickFilter {\n /** Stable id, used as the React key and for edits/removal. */\n id: string;\n /** The feature property this control narrows. */\n field: string;\n kind: QuickFilterKind;\n /**\n * `false` keeps the control configured but inert, so a filter can be muted\n * without losing the values chosen for it. Defaults to enabled.\n */\n enabled?: boolean;\n /**\n * `categorical`: the chosen values. An empty (or omitted) selection means\n * \"every value\" and compiles to nothing — unchecking the last box clears the\n * filter rather than emptying the map.\n */\n values?: (string | number | boolean)[];\n /** `range`: inclusive bounds. `null`/omitted leaves that side open. */\n min?: number | null;\n max?: number | null;\n /**\n * `date`: inclusive `YYYY-MM-DD` bounds. The end day is included in full, so\n * a timestamp field filtered to a single day keeps that whole day.\n */\n start?: string | null;\n end?: string | null;\n /** `date`: how the field stores its values. Defaults to `iso`. */\n dateKind?: QuickFilterDateKind;\n /** `text`: the comparison. Defaults to `contains`. */\n operator?: QuickFilterTextOperator;\n /** `text`: the needle. Blank means no constraint. */\n text?: string;\n}\n\n/** Persisted refresh policy and most recent synchronization result for a layer. */\nexport interface LayerConnection {\n /** Owning layer id. Repeated here so records remain self-describing when exported. */\n layerId: string;\n /** Automatic refresh cadence in seconds, or null for manual synchronization only. */\n interval: number | null;\n /** ISO timestamp of the most recent successful synchronization. */\n lastSyncedAt: string | null;\n /** Most recent synchronization error. Cleared by a successful synchronization. */\n lastError: string | null;\n /** Whether a failed synchronization retains the last good data or clears it. */\n onFailure: \"keep-last\" | \"clear\";\n}\n\n/**\n * Configuration for automatic feature editor tracking (Issue #1677).\n * Maintains `created_by`, `created_at`, `edited_by`, `edited_at` fields\n * automatically when features are created or updated.\n */\nexport interface EditorTrackingConfig {\n /** `true` enables automatic creation/edit timestamp and author stamping. */\n enabled: boolean;\n /** Field name for creation author (default `\"created_by\"`). */\n createdByField?: string;\n /** Field name for creation timestamp (default `\"created_at\"`). */\n createdAtField?: string;\n /** Field name for last-edit author (default `\"edited_by\"`). */\n editedByField?: string;\n /** Field name for last-edit timestamp (default `\"edited_at\"`). */\n editedAtField?: string;\n}\n\n/**\n * Visibility of a layer's attribute field.\n * - \"hidden\": Not shown in the attribute table, identify popup, tooltips, or field pickers, but remains in the data.\n * - \"excluded\": Removed entirely from the data when the project is shared or exported.\n */\nexport type FieldVisibility = \"hidden\" | \"excluded\";\n\n/**\n * Explicit capability flags defining what actions a user/session may perform on a layer.\n * Any omitted capability falls back to the inferred default behavior for that layer's source.\n */\nexport interface LayerCapabilities {\n /** Can the layer's features or attributes be queried / identified. */\n query?: boolean;\n /** Can new features be created/added to the layer. */\n create?: boolean;\n /** Can existing features/attributes be modified. */\n update?: boolean;\n /** Can features be deleted from the layer. */\n delete?: boolean;\n /** Can the layer data/symbology be exported/downloaded. */\n export?: boolean;\n}\n\n/**\n * Application privilege identifiers defining discrete capabilities in GeoLibre.\n */\nexport type AppPrivilege =\n | \"layers:edit\"\n | \"layers:add-remote\"\n | \"layers:add-local\"\n | \"processing:run\"\n | \"processing:sidecar\"\n | \"project:save\"\n | \"project:share\"\n | \"project:share-public\"\n | \"plugins:install\"\n | \"assistant:use\"\n | \"connections:manage\"\n | \"export:data\"\n | \"export:image\"\n | \"settings:manage\";\n\n/**\n * Standard named roles bundling application privileges.\n */\nexport type AppRole = \"viewer\" | \"editor\" | \"publisher\" | \"administrator\" | \"custom\";\n\n/**\n * Ephemeral application capabilities state defining the active role, effective privileges,\n * and optional restriction reason for the current session/deployment.\n */\nexport interface AppCapabilities {\n /**\n * The assigned application role. `custom` once an ad-hoc `grantAppPrivilege` /\n * `revokeAppPrivilege` has moved the set away from the bundle a named role\n * defines, so the role never claims a shape the privileges do not have.\n */\n role: AppRole;\n /** List of granted privileges for the active role or custom configuration. */\n privileges: AppPrivilege[];\n /**\n * Optional human-readable reason covering the whole set (e.g. \"Action disabled\n * by deployment policy\"), used for any privilege without its own.\n */\n reason?: string;\n /**\n * Per-privilege reasons, and why they exist: two privileges can be withheld by\n * different causes — a role bundle plus a licence limit, say — and a single\n * `reason` would make the second revocation relabel the first, so every gate\n * would explain itself with whichever cause happened to be recorded last.\n * Takes precedence over `reason` for the privileges it names.\n */\n privilegeReasons?: Partial<Record<AppPrivilege, string>>;\n}\n\nexport interface GeoLibreLayer {\n id: string;\n name: string;\n type: LayerType;\n source: Record<string, unknown>;\n visible: boolean;\n opacity: number;\n style: LayerStyle;\n metadata: Record<string, unknown>;\n beforeId?: string;\n geojson?: FeatureCollection;\n /** A SQL Server write needs a successful table reread before it may be submitted again. */\n mssqlWritebackPending?: boolean;\n /**\n * Explicit capability set for the layer (query, create, update, delete, export).\n * Unset capabilities default to the inferred behavior for the layer's source kind.\n */\n capabilities?: LayerCapabilities;\n /**\n * Automatic editor tracking configuration for feature creation/updates.\n */\n editorTracking?: EditorTrackingConfig;\n /**\n * Field-level visibility overrides. Fields marked as \"excluded\" are physically\n * removed from the data during export and sharing.\n */\n fieldVisibility?: Record<string, FieldVisibility>;\n /**\n * Per-field edit-widget, constraint, and visibility configuration authored\n * in the Attribute Form designer. Applied by the attribute editing surfaces\n * (attribute table inline editor, Field Collection capture form); persists\n * with the project like {@link joins}.\n */\n attributeForm?: AttributeFormConfig;\n /**\n * Popup and hover-tooltip design for this layer, authored in the Style\n * panel's Popup section. Absent means the default full-property dump.\n */\n popup?: LayerPopupConfig;\n /**\n * User-authored catalog metadata (title, abstract, keywords, license,\n * contact, lineage, temporal extent, links), edited in the layer's Metadata\n * dialog. Absent when nothing has been entered; an empty record is never\n * written to a project file.\n */\n descriptiveMetadata?: LayerDescriptiveMetadata;\n /**\n * Persistent attribute joins applied to this layer's features, in order.\n * The joined columns are materialized into `geojson` feature properties (so\n * the attribute table, Expression Builder, styling, and labels all see them)\n * and re-derived whenever the layer's or a join table's data changes.\n */\n joins?: LayerJoin[];\n /**\n * Expression-backed virtual fields computed for this layer's features, in\n * order. Applied after joins (so an expression can read joined columns) and\n * materialized into `geojson` feature properties; re-derived whenever the\n * layer's data changes. See {@link LayerVirtualField}.\n */\n virtualFields?: LayerVirtualField[];\n /**\n * Transient MapLibre filter expression applied on top of every rendered\n * sub-layer's geometry filter. The Time Slider plugin sets this on a bound\n * vector layer so scrubbing the timeline narrows the visible features to the\n * current time window, while the layer's own styling and opacity stay\n * untouched. It is derived from the slider's current date, so it is NOT\n * persisted (stripped by `prepareLayerForSave`); the binding config lives in\n * `metadata.timeBinding` and the filter is recomputed live on the next\n * activation. `undefined` means no time filter is applied.\n */\n timeFilter?: unknown[];\n /** Transient MapLibre expression applied by the iframe embed API. */\n embedFilter?: unknown[];\n /**\n * Project-persisted boolean MapLibre expression that narrows the features\n * rendered for this layer. Unlike a selection, this leaves the source data\n * intact and keeps non-matching features hidden until the filter is cleared.\n * It is composed with transient filters, quick filters, and rule visibility\n * by the map renderers.\n */\n filterExpression?: unknown[];\n /**\n * Data-driven filter controls authored in the layer's Quick Filters section\n * (issue #2114). Unlike {@link timeFilter} and {@link embedFilter} this is\n * persisted control *state*, not a compiled expression: `@geolibre/map`\n * compiles it at sync time (see `compileQuickFilters`) and combines the\n * result with the transient filters and the rule-based visibility filter, so\n * a host page's filter and a user's filter narrow the layer together.\n */\n quickFilters?: LayerQuickFilter[];\n sourcePath?: string;\n /**\n * Id of the {@link LayerGroup} this layer belongs to, or `undefined` when the\n * layer sits at the top level of the layer panel. Layers sharing a `groupId`\n * are kept contiguous in the store's flat `layers` array so the group renders\n * as one block; see `@geolibre/core`'s `layer-groups` helpers.\n */\n groupId?: string;\n /**\n * Project-persisted connection policy for reloadable layers. Runtime timers\n * are reconstructed from this record when a project opens.\n */\n connection?: LayerConnection;\n}\n\n/**\n * Options for {@link AppState.addTileLayer}: a native raster tile layer (XYZ,\n * WMS, or WMTS) that appears in the Layers panel and persists with the project,\n * just like a layer added through the Add Data dialog. Mirrors the raster\n * `source` fields MapLibre understands so an external plugin can register tile\n * layers without touching the map directly.\n */\nexport interface AddTileLayerOptions {\n /**\n * One or more XYZ tile URL templates (with `{x}`/`{y}`/`{z}` placeholders).\n * At least one non-empty template is required, or the layer renders nothing.\n */\n tiles: string[];\n /**\n * Layer discriminator, controlling how the layer is labelled and (for WMS)\n * dev-server proxied. Defaults to `\"xyz\"`. The layer's `source.type` is\n * always `\"raster\"`, so any other value (such as `\"vector-tiles\"` from an\n * untyped JS caller) throws rather than persisting a mislabelled source.\n */\n type?: \"xyz\" | \"wms\" | \"wmts\" | \"raster\";\n /** Service or base URL recorded on the source for display and restore. */\n url?: string;\n /** Tile size in pixels (default 256). */\n tileSize?: number;\n /** Attribution string shown in the map's attribution control. */\n attribution?: string;\n /** Visible extent as `[west, south, east, north]` in WGS84 degrees. */\n bounds?: [number, number, number, number];\n /** Minimum zoom at which tiles are requested. */\n minzoom?: number;\n /** Maximum zoom at which tiles are requested. */\n maxzoom?: number;\n /** Tile y-axis scheme; `\"tms\"` flips the y origin. Defaults to `\"xyz\"`. */\n scheme?: \"xyz\" | \"tms\";\n /** Initial visibility (default true). */\n visible?: boolean;\n /** Initial opacity in [0, 1] (default 1). */\n opacity?: number;\n /**\n * Extra source fields merged onto the layer's `source` (e.g. the WMS\n * `layers`/`styles`/`format` recorded for restore). The required `type`,\n * `tiles`, and `tileSize` always win over keys supplied here.\n */\n source?: Record<string, unknown>;\n /** Extra metadata merged onto the layer record. */\n metadata?: Record<string, unknown>;\n}\n\n/**\n * A named, collapsible folder in the layer panel that organizes a contiguous\n * run of layers. Groups may be nested by referencing another group as their\n * parent; the root groups omit `parentId`.\n *\n * The group's `visible` flag and `opacity` multiplier are folded into each\n * child layer's effective render state by `applyGroupEffects` before the map\n * syncs, so children keep their own stored `visible`/`opacity` values.\n */\nexport interface LayerGroup {\n id: string;\n name: string;\n /** Parent folder id, or undefined when this folder is at the panel root. */\n parentId?: string;\n /** When true, the group's children are hidden in the panel (not on the map). */\n collapsed: boolean;\n /** Group-level visibility; ANDed with each child layer's own visibility. */\n visible: boolean;\n /** Group-level opacity in [0, 1]; multiplied into each child's opacity. */\n opacity: number;\n}\n\n/**\n * Metadata `sourceKind` marking a live SQL query layer: a GeoJSON-backed layer\n * created from a SQL Workspace result whose DuckDB statement is stored on the\n * layer metadata and re-executed on refresh. Defined here so the desktop app\n * (which owns the query/refresh logic) and `@geolibre/plugins` (which excludes\n * these layers from in-place geometry editing) share one value.\n */\nexport const SQL_QUERY_SOURCE_KIND = \"sql-query\";\n\n/**\n * Metadata `sourceKind` marking a NetCDF/HDF grid baked into an `image` overlay,\n * as opposed to the KML ground overlays that otherwise use that layer type.\n *\n * Defined here so `@geolibre/map` (which must not run its feature-query identify\n * on these) and the desktop app (which owns the symbology panel, the pixel\n * readout, and the spectral profile) share one value.\n */\nexport const NETCDF_IMAGE_SOURCE_KIND = \"netcdf-image\";\n\n/**\n * Detect a DuckDB query layer rendered through the plugin's external deck.gl\n * overlay. Shared by `@geolibre/map`, `@geolibre/plugins`, and the desktop\n * app so the detection criteria cannot drift.\n */\nexport function isDuckDBQueryLayer(\n layer: Pick<GeoLibreLayer, \"metadata\" | \"type\"> | undefined,\n): boolean {\n return (\n layer?.type === \"duckdb-query\" &&\n layer.metadata.sourceKind === \"duckdb-query\" &&\n layer.metadata.externalDeckLayer === true\n );\n}\n\nexport interface MapViewState {\n center: [number, number];\n zoom: number;\n bearing: number;\n pitch: number;\n bbox?: [number, number, number, number];\n}\n\n/**\n * Multi-map (split/grid) support. The workspace can show several map panes in a\n * `rows x cols` grid. Pane 0 is always the *primary* map: it keeps the existing\n * single-map wiring (the global `mapView` + `basemap*` fields, the layer/style\n * panels, plugins, deck.gl). Panes 1..N are *secondary* maps described by\n * `SecondaryMapView` records: every pane shares the same basemap and the same\n * `layers`, but each secondary pane may override which layers are visible so\n * different panes can show different layers. When `syncView` is on,\n * panning/zooming any pane mirrors the camera to every other pane.\n */\nexport interface MapGridLayout {\n /** Grid rows (>= 1). */\n rows: number;\n /** Grid columns (>= 1). `rows * cols` is the total number of panes. */\n cols: number;\n /** When true, all panes share a synchronized camera. */\n syncView: boolean;\n}\n\n/**\n * Which engine draws a map pane.\n *\n * `\"maplibre\"` is the 2D MapLibre GL map that owns the app's plugin, styling,\n * and deck.gl integrations. `\"cesium\"` is the 3D globe (see `CesiumCanvas`),\n * which renders the same shared store state — camera, basemap, layers, group\n * effects — through CesiumJS. `\"mapbox\"` is Mapbox GL JS and `\"arcgis\"` the\n * ArcGIS Maps SDK for JavaScript, loaded from Esri's CDN at runtime (see\n * `ArcgisCanvas`); both draw the same store state through their own engines.\n *\n * Used both for secondary panes ({@link SecondaryMapView.viewKind}) and for the\n * primary workspace ({@link GeoLibreProject.primaryRenderer}), so the two never\n * drift apart.\n */\nexport type MapRendererKind = \"maplibre\" | \"cesium\" | \"mapbox\" | \"arcgis\";\n\n/**\n * The engine that draws the primary map area when a project says nothing. The\n * 2D map: it is the renderer every tool, plugin, and panel is wired to, so an\n * existing project (and a new one) opens exactly as it always did.\n */\nexport const DEFAULT_PRIMARY_RENDERER: MapRendererKind = \"maplibre\";\n\n/**\n * A non-primary map pane: shares the primary map's basemap and layers, with its\n * own camera and per-layer visibility overrides.\n */\nexport interface SecondaryMapView {\n /** Stable id, used as the React key and the sync-group registration id. */\n id: string;\n view: MapViewState;\n /** Optional user-entered label shown on the pane (e.g. a date or scenario). */\n label?: string;\n /**\n * Which rendering engine draws this pane. Defaults to `\"maplibre\"` (the 2D\n * map) when absent, so existing projects and panes are unchanged. `\"cesium\"`\n * renders a 3D globe (see {@link CesiumCanvas}) over the same shared layers.\n */\n viewKind?: MapRendererKind;\n /**\n * Per-layer visibility overrides keyed by layer id. A layer absent from this\n * map inherits the primary map's visibility (`layer.visible`); an entry forces\n * the layer visible (`true`) or hidden (`false`) in this pane only.\n */\n layerVisibility: Record<string, boolean>;\n}\n\n/** The default single-map grid (one pane, sync enabled so it turns on cleanly). */\nexport const DEFAULT_MAP_GRID_LAYOUT: MapGridLayout = {\n rows: 1,\n cols: 1,\n syncView: true,\n};\n\n/** Maximum rows or columns in the map grid (so at most a 4x4 = 16-pane grid). */\nexport const MAX_MAP_GRID_DIM = 4;\n\n/**\n * Live multi-user collaboration (issue #307). These types describe the\n * *ephemeral* session state the store holds while a live session is active. It\n * is intentionally never written to the `.geolibre.json` project file (the\n * `project.ts` serializers never read it) and never tracked in undo history (the\n * store's `partialize` never lists it), so it resets cleanly on reload.\n */\nexport type CollaborationRole = \"host\" | \"guest\";\n\n/** Whether guests may edit (`co-edit`) or only watch (`view-only`). */\nexport type CollaborationMode = \"view-only\" | \"co-edit\";\n\nexport interface ParticipantIdentity {\n provider: string;\n userId: string;\n username: string;\n}\n\nexport interface CollabInvite {\n token: string;\n role: CollaborationMode;\n createdAt: number;\n maxUses?: number;\n useCount: number;\n revoked: boolean;\n}\n\nexport interface CollaborationParticipant {\n clientId: string;\n displayName: string;\n color: string;\n role: CollaborationRole;\n /**\n * Host-set per-participant edit override (#754). `null` follows the session\n * `mode`; `true`/`false` pins this participant to can-edit / view-only. Always\n * `null` for the host (the host can always edit).\n */\n editOverride: boolean | null;\n /** Optional account identity when signed-in identity binding is enabled. */\n identity?: ParticipantIdentity | null;\n}\n\n/** A remote participant's live cursor + viewport, used to render presence. */\nexport interface CollaborationPresence {\n displayName: string;\n color: string;\n cursor?: { lng: number; lat: number } | null;\n view?: MapViewState | null;\n}\n\n/** One in-session chat message (#754). Ephemeral; never persisted to a project. */\nexport interface CollaborationChatMessage {\n /** Server-assigned id (stable React key / dedupe). */\n id: string;\n /** clientId of the author. */\n clientId: string;\n displayName: string;\n color: string;\n text: string;\n /** Optional map coordinate the author attached; clickable to recenter. */\n coordinate?: { lng: number; lat: number } | null;\n /** Server-assigned epoch-ms timestamp. */\n ts: number;\n}\n\nexport interface CollaborationState {\n /** True once connected and joined to a session. */\n isActive: boolean;\n /** True while connecting/reconnecting (UI shows a spinner). */\n connecting: boolean;\n sessionId: string | null;\n clientId: string | null;\n role: CollaborationRole | null;\n mode: CollaborationMode;\n selfName: string;\n selfColor: string;\n participants: CollaborationParticipant[];\n /** Remote presence keyed by participant clientId (never includes self). */\n presence: Record<string, CollaborationPresence>;\n /** When true, this participant's camera follows the host's viewport. */\n followHost: boolean;\n /** Recent session chat, oldest first, capped to a bounded window (#754). */\n chat: CollaborationChatMessage[];\n /** Session flag requiring participants to be signed in. */\n requireIdentity: boolean;\n /**\n * Whether the connected relay has an identity issuer configured. False (the\n * default) means it cannot verify a sign-in, so the host UI hides the\n * \"require a signed-in account\" toggle instead of offering a gate that would\n * lock every guest out.\n */\n identitySupported: boolean;\n /** Layer IDs marked locked by the host. */\n lockedLayerIds: string[];\n /** Active session invites minted by host. */\n invites: CollabInvite[];\n /** Last human-readable error, surfaced in the Collaborate dialog. */\n error: string | null;\n}\n\n/** Map projection the renderer uses. Mirrors the GlobeControl toggle. */\nexport type MapProjection = \"globe\" | \"mercator\";\n\n/**\n * Unit system the scale bar reports distances in. `\"metric\"` uses m/km,\n * `\"imperial\"` uses ft/mi, and `\"nautical\"` uses nautical miles.\n */\nexport type MapScaleUnit = \"metric\" | \"imperial\" | \"nautical\";\n\nexport interface MapPreferences {\n restrictBounds: boolean;\n bounds: [number, number, number, number];\n minZoom: number;\n maxZoom: number;\n maxPitch: number;\n renderWorldCopies: boolean;\n projection: MapProjection;\n /**\n * Celestial body / ellipsoid the project's coordinates describe (keys into the\n * ellipsoid registry in `@geolibre/core`). Drives measurement radii and pairs\n * with planetary basemaps. Defaults to `\"earth\"` (WGS 84).\n */\n ellipsoidId: string;\n /**\n * Unit system the scale bar displays. Defaults to `\"metric\"`; switch to\n * `\"imperial\"` for feet/miles or `\"nautical\"` for nautical miles.\n */\n scaleUnit: MapScaleUnit;\n /**\n * Whether the status bar resolves and shows the ground elevation under the\n * pointer (issue #1813). **Defaults to `false`**: with 3D terrain off the\n * lookup falls back to the public Open-Meteo service, so hovering would send\n * coordinates off the device for a readout the user never asked for. Toggled\n * from Controls -> Elevation.\n */\n showPointerElevation: boolean;\n /** Whether the built-in 3D terrain control and terrain surface are enabled. */\n terrainEnabled: boolean;\n /**\n * Optional Cesium Ion terrain asset used by the globe when terrain is enabled.\n * Ignored by 2D renderers; absent means the renderer's normal global source.\n */\n terrainIonAssetId?: number;\n /** Mapbox-only style. New projects use Streets; absent follows the shared basemap. */\n mapboxStyleUrl?: string;\n /**\n * ArcGIS-only basemap: an Esri basemap style id (`arcgis/streets`,\n * `arcgis/imagery`, `osm/standard`, ...). New projects use Streets. Absent\n * follows the shared basemap, translated to tiles the SDK can draw; the id\n * is also set aside when no ArcGIS API key is configured, since Esri's\n * basemap styles service requires one.\n */\n arcgisBasemap?: string;\n /**\n * ArcGIS-only projection of the flat map, as a WKID the ArcGIS Maps SDK\n * resolves (`54099` Spilhaus, `54030` Robinson, ...). Absent means Web\n * Mercator. Vector layers are reprojected on the client; tiled layers and\n * basemaps cannot be, so they do not draw in another projection. The globe\n * (a `SceneView`) ignores it. See `arcgis-projection.ts`.\n */\n arcgisWkid?: number;\n /** Cesium imagery override; absent follows the shared project basemap. */\n cesiumBasemap?: import(\"./cesium-imagery\").CesiumBasemapId;\n /**\n * Notation the status bar reports the pointer coordinate in: `\"dd\"` decimal\n * degrees (default), `\"dms\"` degrees/minutes/seconds, `\"ddm\"` degrees and\n * decimal minutes, `\"utm\"` zone easting/northing, `\"mgrs\"`/`\"usng\"` grid\n * references, or `\"epsg\"` x/y in the CRS named by `coordinateEpsgCode`.\n * Stored as a string rather than a union so `@geolibre/core` does not have to\n * depend on the formatter, which lives with the app's DMS helpers and the\n * Gridlines plugin's UTM projection; the app normalises unknown values to\n * `\"dd\"`.\n */\n coordinateFormat: string;\n /**\n * EPSG code the `\"epsg\"` coordinate format projects the pointer into, e.g.\n * `3857` or `32618`. Kept beside `coordinateFormat` rather than inside it so\n * the code survives cycling through the other notations. Absent means the\n * app default (EPSG:3857).\n */\n coordinateEpsgCode?: number;\n /**\n * Whether the map fits to a layer's extent after data is added (Add Data\n * menus, drag-and-drop, file import, tool outputs). Defaults to `true`; turn\n * it off to keep the current view. Explicit \"Zoom to layer\" actions always\n * fit regardless of this setting.\n */\n zoomToNewLayers: boolean;\n}\n\nexport interface RuntimeEnvironmentVariable {\n key: string;\n value: string;\n enabled: boolean;\n /**\n * Absent or `true`: the value is a secret (desktop keeps it in the OS\n * keychain; every egress removes it). `false`: an ordinary value saved in the\n * project and kept on share/export.\n */\n secret?: boolean;\n}\n\ndeclare global {\n interface Window {\n // Runtime environment variables published from project preferences. Shared\n // here so the desktop app and plugins type the global from one source.\n __GEOLIBRE_RUNTIME_ENV__?: Record<string, string>;\n }\n}\n\n/**\n * Geocoding backend selection persisted in the project. The provider id keys\n * into the geocoding registry in `@geolibre/core`; API keys are stored per\n * provider so switching backends does not discard the others' keys. Empty\n * endpoint overrides fall back to the provider's default endpoints.\n */\nexport interface GeocodingPreferences {\n providerId: string;\n /** Per-provider API key / access token, keyed by provider id. */\n apiKeys: Record<string, string>;\n /** Optional custom forward endpoint (else the provider default). */\n forwardEndpoint?: string;\n /** Optional custom reverse endpoint (else the provider default). */\n reverseEndpoint?: string;\n /** Contact email sent to identify the client (used by Nominatim). */\n email?: string;\n}\n\nexport interface ProjectPreferences {\n map: MapPreferences;\n environmentVariables: RuntimeEnvironmentVariable[];\n geocoding: GeocodingPreferences;\n}\n\nexport type ProjectPluginControlPosition =\n | \"top-left\"\n | \"top-right\"\n | \"bottom-left\"\n | \"bottom-right\";\n\nexport interface ProjectPluginState {\n manifestUrls: string[];\n activePluginIds: string[];\n mapControlPositions: Record<string, ProjectPluginControlPosition>;\n settings: Record<string, unknown>;\n}\n\nexport const DEFAULT_PROJECT_PREFERENCES: ProjectPreferences = {\n map: {\n restrictBounds: false,\n bounds: [-180, -85, 180, 85],\n minZoom: 0,\n maxZoom: 24,\n maxPitch: 85,\n renderWorldCopies: true,\n projection: \"globe\",\n ellipsoidId: \"earth\",\n scaleUnit: \"metric\",\n // Off by default: turning it on can send pointer coordinates to a public\n // elevation service, which should be an explicit choice.\n showPointerElevation: false,\n terrainEnabled: false,\n coordinateFormat: \"dd\",\n zoomToNewLayers: true,\n mapboxStyleUrl: \"mapbox://styles/mapbox/standard\",\n arcgisBasemap: \"arcgis/streets\",\n // With an Ion token this is the globe's photographic default. The\n // availability gate transparently falls back to the project basemap when\n // no token is configured.\n cesiumBasemap: \"bing-aerial\",\n },\n environmentVariables: [],\n geocoding: {\n providerId: \"nominatim\",\n apiKeys: {},\n },\n};\n\n/**\n * A single user override for one legend item, keyed in {@link LegendConfig.overrides}\n * by a stable item key (a layer id for a whole entry, or `${layerId}::${index}`\n * for an individual class within a graduated/categorized entry).\n */\nexport interface LegendItemOverride {\n /** User-supplied label that replaces the auto-generated one. */\n label?: string;\n /** When true, the item is omitted from the rendered legend. */\n hidden?: boolean;\n}\n\n/** Swatch shape for a hand-authored legend item. */\nexport type LegendCustomShape = \"square\" | \"circle\" | \"line\";\n\n/** One hand-authored legend item: a color swatch plus its label. */\nexport interface LegendCustomItem {\n /** Display label (e.g. an NLCD class name). */\n label: string;\n /** Swatch color as a CSS color (typically `#rrggbb`). */\n color: string;\n /** Swatch shape; defaults to `\"square\"`. */\n shape?: LegendCustomShape;\n /**\n * Symbol size in map pixels (circle radius / line width). Carried so that\n * customizing a proportional-symbol entry keeps the graduated sizes it was\n * seeded from instead of flattening every symbol to one size.\n */\n size?: number;\n}\n\n/**\n * A hand-authored legend entry. Keyed by layer id it REPLACES that layer's\n * auto-derived classes (the fallback when automatic derivation is wrong or\n * impossible, e.g. a paletted land-cover raster like NLCD); keyed by a\n * `custom:` id it renders as a standalone section not tied to any layer.\n */\nexport interface LegendCustomEntry {\n /** Optional heading; a layer-keyed entry falls back to the layer name. */\n title?: string;\n /** The items to render, top to bottom. */\n items: LegendCustomItem[];\n}\n\n/** Map corner the on-map legend panel docks to. */\nexport type LegendPanelPosition = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n\n/**\n * User customizations for the legend (the on-map Legend panel and the Print\n * Layout legend). The legend itself is always derived from the visible layers'\n * symbology; this record only stores the edits layered on top (title, ordering,\n * per-item rename/hide, hand-authored entries), so it survives layer additions\n * and removals and is persisted in the `.geolibre.json` project.\n */\nexport interface LegendConfig {\n /** Heading drawn above the legend entries. */\n title: string;\n /** When true, classes are grouped under a per-layer heading. */\n groupByLayer: boolean;\n /**\n * Custom top-level entry order by layer id, top-first. Layer ids not listed\n * keep their default order after the listed ones.\n */\n order: string[];\n /** Per-item overrides keyed by stable item key. */\n overrides: Record<string, LegendItemOverride>;\n /**\n * Hand-authored entries keyed by layer id (replacing that layer's\n * auto-derived classes) or by a standalone `custom:` id. See\n * {@link LegendCustomEntry}.\n */\n customEntries?: Record<string, LegendCustomEntry>;\n /** Whether the on-map Legend panel is open (persisted with the project). */\n panelVisible?: boolean;\n /** Whether the open panel is collapsed to just its header bar. */\n panelCollapsed?: boolean;\n /** Map corner the on-map Legend panel docks to; defaults to `\"top-left\"`. */\n panelPosition?: LegendPanelPosition;\n /**\n * User-resized panel width in px (via the corner drag handles). Absent means\n * the default width; height absent means auto-fit to content within the map.\n */\n panelWidth?: number;\n /** User-resized panel height in px. See {@link LegendConfig.panelWidth}. */\n panelHeight?: number;\n}\n\n// Frozen so the shared singleton can be safely spread (`{ ...DEFAULT_LEGEND_CONFIG }`)\n// at call sites without risk of a future in-place mutation corrupting the nested\n// `order`/`overrides` references that the spread keeps sharing.\nexport const DEFAULT_LEGEND_CONFIG: LegendConfig = Object.freeze({\n title: \"Legend\",\n groupByLayer: true,\n order: Object.freeze([] as string[]) as string[],\n overrides: Object.freeze({} as Record<string, LegendItemOverride>) as Record<\n string,\n LegendItemOverride\n >,\n});\n\n/**\n * Camera target captured for a story chapter. `pitch` and `bearing` are\n * optional in memory: a location built in code may omit them, and every engine\n * then keeps that part of the current camera (see `storyLocationView`). A\n * chapter read from a project file or a CSV never omits them: loading fills an\n * absent value with 0 (`normalizeStoryChapter`, `parseStoryMapCsv`), so a file\n * without them still plays north-up and flat, as it always has.\n */\nexport interface StoryChapterLocation {\n center: [number, number];\n zoom: number;\n pitch?: number;\n bearing?: number;\n}\n\n/** Where a chapter's text panel sits over the map. */\nexport type StoryChapterAlignment = \"left\" | \"center\" | \"right\" | \"full\";\n\n/** How the map transitions to a chapter's location. */\nexport type StoryChapterAnimation = \"flyTo\" | \"easeTo\" | \"jumpTo\";\n\n/** A layer opacity change triggered when a chapter is entered or exited. */\nexport interface StoryLayerOpacityChange {\n /** Stable identity for React list keys; optional for older project files. */\n id?: string;\n /** GeoLibre store layer id whose opacity should change. */\n layerId: string;\n opacity: number;\n /** Transition duration in milliseconds. */\n duration?: number;\n}\n\n/** A single scene in a scroll-driven story map. */\nexport interface StoryChapter {\n id: string;\n title: string;\n description: string;\n /** Optional image shown in the chapter panel (URL or data URI). */\n image?: string;\n alignment: StoryChapterAlignment;\n /** Hide the text panel while still transitioning the map. */\n hidden: boolean;\n location: StoryChapterLocation;\n mapAnimation: StoryChapterAnimation;\n /** Slowly rotate the camera once the transition settles. */\n rotateAnimation: boolean;\n onChapterEnter: StoryLayerOpacityChange[];\n onChapterExit: StoryLayerOpacityChange[];\n}\n\nexport type StoryInsetPosition = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n\n/**\n * A non-chapter intro/outro slide shown before the first or after the last\n * chapter (#998). `\"none\"` disables it. The remaining modes are:\n * - `\"blank\"` a solid screen in the panel theme color,\n * - `\"black\"` a solid black screen,\n * - `\"global\"` a zoomed-out global view of the map with no text,\n * - `\"adjacent\"` the camera of the neighboring chapter (the first chapter for\n * the start slide, the last chapter for the closing slide) with all text and\n * media hidden, so the presenter can zoom into the full content next.\n */\nexport type StorySlideMode = \"none\" | \"blank\" | \"black\" | \"global\" | \"adjacent\";\n\n/** A start/closing slide that is actually shown (every mode except `\"none\"`). */\nexport type StoryActiveSlideMode = Exclude<StorySlideMode, \"none\">;\n\n/** Scroll-driven story map authored on top of a GeoLibre project. */\nexport interface StoryMap {\n title: string;\n subtitle: string;\n byline: string;\n footer: string;\n theme: \"light\" | \"dark\";\n showMarkers: boolean;\n markerColor: string;\n inset: boolean;\n insetPosition: StoryInsetPosition;\n /**\n * Start the presentation with the chapter itinerary/navigation panel hidden,\n * revealing chapters one at a time instead of listing every upcoming location\n * up front (#995). The presenter still offers a toggle to open the list.\n */\n hideChapterNav: boolean;\n /** Optional intro slide shown before the first chapter (#998). */\n startSlide: StorySlideMode;\n /** Optional closing slide shown after the last chapter (#998). */\n endSlide: StorySlideMode;\n chapters: StoryChapter[];\n}\n\nexport const DEFAULT_STORY_MAP: StoryMap = {\n title: \"\",\n subtitle: \"\",\n byline: \"\",\n footer: \"\",\n theme: \"dark\",\n showMarkers: false,\n markerColor: \"#3fb1ce\",\n inset: false,\n insetPosition: \"bottom-left\",\n hideChapterNav: false,\n startSlide: \"none\",\n endSlide: \"none\",\n chapters: [],\n};\n\n/**\n * One step in a {@link ProcessingModel}: a processing tool invoked with a fixed\n * set of parameters. The runner chains steps by feeding each step's output layer\n * into the next step's input layer parameter (`inputParam`, default `\"layer\"`),\n * so a step's stored `parameters` for that input is ignored for every step after\n * the first.\n */\nexport interface ProcessingModelStep {\n /** Stable id, unique within the model (used as the React key and run label). */\n id: string;\n /** The processing tool's registry id (e.g. `\"buffer\"`). */\n toolId: string;\n /** Parameter values keyed by the tool's parameter ids. */\n parameters: Record<string, unknown>;\n /**\n * Which `type: \"layer\"` parameter receives the previous step's output. Defaults\n * to `\"layer\"`; set it for tools whose primary input is named differently.\n */\n inputParam?: string;\n}\n\n/**\n * What flows along a model edge. Vector nodes exchange FeatureCollections;\n * raster nodes exchange GeoTIFF bytes. A port declaring `\"any\"` accepts either\n * and is resolved to a concrete kind at run time by whatever is wired into it.\n */\nexport type ModelPortKind = \"vector\" | \"raster\" | \"any\";\n\n/** One connection point on a {@link ModelGraphNode}. */\nexport interface ModelGraphPort {\n /**\n * Port id, unique within its node and direction. For a tool node's inputs\n * this is the underlying tool parameter id, so wiring an edge and setting the\n * parameter by hand are the same operation.\n */\n id: string;\n label: string;\n kind: ModelPortKind;\n /** Inputs only: the run fails when nothing is wired in and no value is set. */\n required?: boolean;\n}\n\n/**\n * What a node does. `input` sources an existing project layer, `tool` runs a\n * processing algorithm, and `output` names a result to add back to the map.\n */\nexport type ModelGraphNodeKind = \"input\" | \"tool\" | \"output\";\n\n/** One node on the Model Builder canvas. */\nexport interface ModelGraphNode {\n /** Stable id, unique within the graph; referenced by {@link ModelGraphEdge}. */\n id: string;\n kind: ModelGraphNodeKind;\n /** Canvas position in graph coordinates (unscaled by zoom). */\n x: number;\n y: number;\n /** `input` nodes: the project layer id this node sources. */\n layerId?: string;\n /**\n * `tool` nodes: the tool's id within {@link provider}'s registry. Kept\n * separate from the provider so the same short id can exist in both.\n */\n toolId?: string;\n /** `tool` nodes: which registry resolves {@link toolId}. */\n provider?: ModelToolProvider;\n /** `tool` nodes: parameter values for everything not supplied by an edge. */\n parameters?: Record<string, unknown>;\n /** `output` nodes: the layer name given to the result added to the map. */\n name?: string;\n}\n\n/**\n * A directed connection from one node's output port to another node's input\n * port. Ports are named, so a tool with several inputs (Clip's target and\n * overlay, say) wires each one unambiguously.\n */\nexport interface ModelGraphEdge {\n id: string;\n from: string;\n fromPort: string;\n to: string;\n toPort: string;\n}\n\n/** The node-and-edge graph authored on the Model Builder canvas. */\nexport interface ProcessingModelGraph {\n nodes: ModelGraphNode[];\n edges: ModelGraphEdge[];\n}\n\n/** Which registry a {@link ModelGraphNode.toolId} is resolved against. */\nexport type ModelToolProvider = \"vector\" | \"whitebox\";\n\n/**\n * A reusable processing pipeline (\"model\" in QGIS Graphical Modeler / ArcGIS\n * ModelBuilder terms), saved in the project file so it can be reloaded and\n * re-run.\n *\n * Two shapes coexist. {@link steps} is the original strictly linear chain, and\n * remains the only thing older builds understand. {@link graph} is the\n * Model Builder's directed graph, which supports multi-input tools, branches\n * and merges. When both are present `graph` wins; a model saved by the canvas\n * also writes a `steps` projection whenever its graph happens to be a single\n * chain, so older builds can still run it.\n */\nexport interface ProcessingModel {\n id: string;\n name: string;\n steps: ProcessingModelStep[];\n graph?: ProcessingModelGraph;\n}\n\n/**\n * Which dialog family executed a {@link ProcessingRun}. Drives the History\n * panel's re-run routing: each kind maps to the dialog (and store open-flag)\n * that can be reopened pre-filled with the run's parameters.\n */\nexport type ProcessingRunKind =\n | \"vector\"\n | \"statistics\"\n | \"network\"\n | \"whitebox\"\n | \"raster\"\n | \"conversion\"\n | \"algorithm\";\n\nexport type ProcessingRunStatus = \"success\" | \"error\";\n\n/** Upper bound on persisted processing-history entries (oldest are dropped). */\nexport const MAX_PROCESSING_HISTORY = 100;\n\n/**\n * One recorded processing tool run (issue #1292). Appended by every processing\n * dialog when a run finishes and persisted in the project file, so a saved\n * project documents how its derived layers were produced. Parameter values are\n * stored exactly as the dialog dispatched them (layer parameters hold layer\n * ids), which is what re-run pre-fills and \"Copy as Python\" emit.\n */\nexport interface ProcessingRun {\n /** Stable id, unique within the project (React key and update key). */\n id: string;\n kind: ProcessingRunKind;\n /** The tool's registry id (e.g. `\"buffer\"`, `\"slope\"`). */\n toolId: string;\n /** Human-readable tool name captured at run time. */\n toolName: string;\n /** Engine that executed the run: `client`, `wasm`, `sidecar`, `pyodide`, `browser`. */\n engine: string;\n /** Parameter values keyed by the tool's parameter ids. */\n parameters: Record<string, unknown>;\n /** Names of input layers referenced by layer parameters, keyed by layer id. */\n inputLayerNames?: Record<string, string>;\n /** Names of layers the run added to the map. */\n outputLayerNames?: string[];\n /** Input file path/name, for file-based tools (raster/conversion). */\n inputPath?: string;\n /** Output file path/name, for file-based tools (raster/conversion). */\n outputPath?: string;\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Wall-clock duration of the run in milliseconds. */\n durationMs?: number;\n status: ProcessingRunStatus;\n /** Error message when `status` is `\"error\"`. */\n error?: string;\n}\n\n/**\n * A pending \"re-run from History\" request. The History panel writes it to the\n * store and opens the target dialog; the dialog consumes it (pre-filling its\n * parameter form) and clears it. `autoRun` asks the dialog to start the run\n * immediately after pre-filling (plain Re-run vs Edit & re-run).\n */\nexport interface ProcessingRerunRequest {\n kind: ProcessingRunKind;\n toolId: string;\n parameters: Record<string, unknown>;\n /** Engine to preselect, when the dialog offers a choice. */\n engine?: string;\n autoRun?: boolean;\n}\n\n/** Column-count bounds for the Dashboard panel's widget grid. */\nexport const MIN_DASHBOARD_COLUMNS = 1;\nexport const MAX_DASHBOARD_COLUMNS = 6;\nexport const DEFAULT_DASHBOARD_COLUMNS = 2;\n\n/** The chart a {@link DashboardWidget} draws. Mirrors the attribute Charts\n * panel's types so a widget reuses the same rendering. The `\"indicator\"` type\n * is a non-chart KPI tile (issue #1381). */\nexport type DashboardWidgetType =\n | \"histogram\"\n | \"scatter\"\n | \"bar\"\n | \"line\"\n | \"box\"\n | \"pie\"\n | \"indicator\"\n | \"selector\"\n | \"list\";\n\n/** How a bar widget reduces its category groups. */\nexport type DashboardWidgetAggregation = \"count\" | \"sum\" | \"mean\";\n\n/**\n * One chart in the Dashboard panel: a chart type bound to a layer and the\n * field(s) it plots. Which `field*`/`category`/`aggregation` keys apply depends\n * on `type` (histogram/line/box use `field`; scatter uses `xField`/`yField`;\n * bar uses `category` + `aggregation` and, for sum/mean, `valueField`). Unused\n * keys are simply ignored, so the record stays flat and easy to hand-edit.\n * Saved in the project file so a dashboard reopens with its widgets intact.\n */\n/**\n * A saved map view in the Bookmarks panel. Mirrors `MapBookmark` from\n * maplibre-gl-components so the panel can load it unchanged.\n */\nexport interface ProjectBookmark {\n /** Unique within the project. */\n id: string;\n name: string;\n /** Camera center longitude, -180..180. */\n lng: number;\n /** Camera center latitude, -90..90. */\n lat: number;\n zoom: number;\n /** Camera tilt in degrees, 0..85. */\n pitch: number;\n /** Camera rotation in degrees. */\n bearing: number;\n /** Creation time, milliseconds since the epoch. */\n createdAt: number;\n /** The {@link ProjectBookmarkGroup} it belongs to; absent when ungrouped. */\n groupId?: string;\n /**\n * Host state captured with the view. GeoLibre stores `visibleLayerIds`\n * (the layers to show when the bookmark is opened).\n */\n extra?: Record<string, unknown>;\n}\n\n/** A folder in the Bookmarks panel. Folders do not nest. */\nexport interface ProjectBookmarkGroup {\n id: string;\n name: string;\n /** Whether the folder is collapsed in the panel. */\n collapsed: boolean;\n}\n\nexport interface DashboardWidget {\n /** Stable id, unique within the project (React key and store key). */\n id: string;\n /** The layer whose features feed this widget. */\n layerId: string;\n /** The chart to draw. */\n type: DashboardWidgetType;\n /** Optional custom title; the panel derives a label from the fields if absent. */\n title?: string;\n /** Optional hex color (`#rgb`/`#rrggbb`) for the chart's marks. Single-series\n * charts use it as the series color; bar/pie use it as the base of a\n * monochromatic ramp. Defaults to the theme primary / multi-color palette. */\n color?: string;\n /** Value field for histogram/line/box. */\n field?: string;\n /** X-axis field for scatter. */\n xField?: string;\n /** Y-axis field for scatter. */\n yField?: string;\n /** Number of bins for a histogram. */\n bins?: number;\n /** Category field for a bar chart. */\n category?: string;\n /** Aggregation for a bar chart (default `count`). */\n aggregation?: DashboardWidgetAggregation;\n /** Value field a bar chart's sum/mean reduces (ignored for `count`). */\n valueField?: string;\n /** Indicator widget: aggregation function for the KPI value (issue #1381).\n * Extends bar aggregation with min, max, and median. */\n indicatorAggregation?: IndicatorAggregation;\n /** Indicator widget: optional prefix (e.g. \"€\", \"$\"). */\n prefix?: string;\n /** Indicator widget: optional suffix (e.g. \" kg\", \" ha\"). */\n suffix?: string;\n /** Selector widget: whether multiple values can be picked (default false). */\n multiple?: boolean;\n /** List widget: columns to display. */\n listFields?: string[];\n /** List widget: field to sort by. */\n sortBy?: string;\n /** List widget: sort direction (default \"desc\"). */\n sortDir?: \"asc\" | \"desc\";\n /** List widget: max rows to show (default 20). */\n limit?: number;\n}\n\n/** Aggregation functions for indicator widgets (issue #1381). Extends the bar\n * widget aggregation with min, max, and median. */\nexport type IndicatorAggregation = \"count\" | \"sum\" | \"mean\" | \"min\" | \"max\" | \"median\";\n\n/**\n * What slice of a layer's styling a Style Manager entry captures (issue #1294).\n *\n * - `\"style\"`: the layer's complete {@link LayerStyle} snapshot.\n * - `\"symbol\"`: fill/stroke/marker/pattern symbology only, so it can restyle a\n * layer without touching its labels or renderer configuration.\n * - `\"labels\"`: the {@link LabelStyle} block only.\n * - `\"ramp\"`: the color ramp + classification settings only (ramp name, class\n * count, classification scheme), independent of the attribute the target\n * layer classifies on.\n */\nexport type StyleLibraryEntryKind = \"style\" | \"symbol\" | \"labels\" | \"ramp\";\n\n/**\n * One saved, reusable style in the Style Manager library (issue #1294). The\n * payload is a {@link LayerStyle} subset chosen by {@link kind}; applying an\n * entry merges that subset onto the target layer's style. Entries live either\n * in the app-level library (persisted across projects) or embedded in a\n * project file's `styleLibrary` array.\n */\nexport interface StyleLibraryEntry {\n /** Stable id, used as the IndexedDB/store key; upserts overwrite by id. */\n id: string;\n /** Display name shown in the Style Manager. */\n name: string;\n /** Which style subset {@link style} carries. */\n kind: StyleLibraryEntryKind;\n /** Free-form tags for filtering the library. */\n tags: string[];\n /** The saved {@link LayerStyle} subset (see {@link kind}). */\n style: Partial<LayerStyle>;\n /** ISO timestamp of the last save; empty for built-in presets. */\n updatedAt: string;\n}\n\n/**\n * One saved, re-addable layer in the Layer Library (issue #1520) — the \"My\n * Data\" section of the Browser panel. Stores the layer's **source\n * specification** plus its full presentation state (style, labels, filters,\n * joins, virtual fields, attribute form), deliberately *not* the data, so the\n * library stays small and an entry always reflects the current contents of its\n * source.\n *\n * The exception is a layer whose features exist only in memory (drawn features,\n * processing output) or in a local file: those have no re-fetchable source, so\n * their features are embedded in {@link geojson} behind a size cap. See\n * `layer-library.ts` for how an entry is captured and re-added.\n */\nexport interface LayerLibraryEntry {\n /** Stable id, used as the IndexedDB/store key; upserts overwrite by id. */\n id: string;\n /** Display name shown in the Browser panel's My Data section. */\n name: string;\n /** ISO timestamp of the last save. */\n addedAt: string;\n /** The layer type to recreate ({@link GeoLibreLayer.type}). */\n layerType: LayerType;\n /** The MapLibre/plugin source spec to recreate the layer from. */\n source: Record<string, unknown>;\n /** The saved layer's complete {@link LayerStyle} (labels included). */\n style: LayerStyle;\n /** Layer opacity in [0, 1]. */\n opacity: number;\n /** The layer metadata the renderers and plugin sync modules key off. */\n metadata: Record<string, unknown>;\n /** Absolute local path, for a layer read from a file on disk. */\n sourcePath?: string;\n /** Persistent attribute joins to reapply. */\n joins?: LayerJoin[];\n /** Expression-backed virtual fields to reapply. */\n virtualFields?: LayerVirtualField[];\n /** Per-field edit-widget/constraint configuration to reapply. */\n attributeForm?: AttributeFormConfig;\n /** Popup/tooltip design to reapply. */\n popup?: LayerPopupConfig;\n /** User-authored catalog metadata to reapply. */\n descriptiveMetadata?: LayerDescriptiveMetadata;\n /**\n * Embedded features, present only for a layer whose source cannot be\n * re-read (in-memory features) or whose local file may be unavailable.\n */\n geojson?: FeatureCollection;\n /**\n * True when the entry can only be re-added by a host that can read\n * {@link sourcePath} — i.e. its features were too large to embed, so the\n * desktop app must re-read the file and the browser build cannot.\n */\n needsLocalFile?: boolean;\n}\n\n/** One saved template in the Template Library. */\nexport interface ProjectTemplateEntry {\n id: string;\n name: string;\n description?: string;\n project: GeoLibreProject;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface GeoLibreProject {\n id?: string;\n version: string;\n name: string;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n /** Custom colour for the Blank background. Null uses the current theme default. */\n blankBackgroundColor?: string | null;\n layers: GeoLibreLayer[];\n /**\n * Layer selected in the Layers panel when the project was saved. Omitted by\n * legacy projects; `null` deliberately restores no active layer.\n */\n selectedLayerId?: string | null;\n /** Named folders that organize the flat `layers` list in the layer panel. */\n layerGroups?: LayerGroup[];\n styles: Record<string, LayerStyle>;\n preferences: ProjectPreferences;\n plugins?: ProjectPluginState;\n /** User customizations for the Print Layout legend. */\n legend?: LegendConfig;\n /**\n * Print Layout composer settings (title, page size, orientation, blocks,\n * atlas). Omitted while the composer is untouched, so projects that never\n * opened it are unaffected (GeoLibre discussion #1992).\n */\n printLayout?: PrintLayoutConfig;\n storymap?: StoryMap;\n /** Saved processing pipelines (batch/model chaining; issue #344). */\n models?: ProcessingModel[];\n /** Recorded processing tool runs (Processing History; issue #1292). */\n processingHistory?: ProcessingRun[];\n /**\n * Object-Based Analysis workbench state and run provenance (issue #3053):\n * settings, classes, and the tool calls, parameters, seeds and engine\n * versions behind the last segmentation, measurement and classification.\n * A versioned JSON object owned by the desktop app; omitted until the\n * workbench is used.\n */\n obia?: Record<string, unknown>;\n /** Saved Dashboard panel chart widgets (issue #401). */\n widgets?: DashboardWidget[];\n /**\n * Saved map views (the Bookmarks panel), in panel order. Omitted when there\n * are none. Before #2869 these lived only in the browser's localStorage.\n */\n bookmarks?: ProjectBookmark[];\n /** Folders the {@link bookmarks} are organized into. Omitted when none. */\n bookmarkGroups?: ProjectBookmarkGroup[];\n /** Number of columns in the Dashboard widget grid; omitted when default. */\n dashboardColumns?: number;\n /**\n * Multi-map grid layout; omitted (single 1x1 pane) for default projects so\n * legacy readers and single-map files are unaffected.\n */\n mapLayout?: MapGridLayout;\n /**\n * Secondary map panes (everything past the primary pane). Omitted when the\n * grid is a single pane. The primary pane uses the top-level `mapView` /\n * `basemap*` fields.\n */\n secondaryMapViews?: SecondaryMapView[];\n /** User-entered label for the primary pane; omitted when empty. */\n primaryMapLabel?: string;\n /**\n * Which engine draws the primary map area (issue #2217). Omitted for the\n * default 2D map, so every project written before this existed — and every\n * project that never leaves MapLibre — is byte-identical to before. A project\n * saved as `\"cesium\"` reopens directly on the 3D globe.\n *\n * This is independent of {@link mapLayout}: a 1x1 workspace can be either\n * renderer, and a multi-pane grid can still mix the two through each pane's\n * {@link SecondaryMapView.viewKind}.\n */\n primaryRenderer?: MapRendererKind;\n /**\n * Project-scoped Style Manager entries (issue #1294), so a project can carry\n * its reusable styles to teammates. Omitted when empty; the app-level\n * library is persisted outside the project file and never serialized here.\n */\n styleLibrary?: StyleLibraryEntry[];\n /** Anchored review comments on map points or features (issue #1518). */\n comments?: ProjectComment[];\n /**\n * Interaction state applied when the project opens: the Identify target and\n * which map controls and toolbar panels start shown (issue #2688). Written by\n * the Python `geolibre` package from `set_identify` / `show_control`, so a\n * saved or HTML-exported notebook map opens the way it was set up. Omitted\n * by default; the app keeps what it loaded and writes it back unchanged.\n */\n interaction?: ProjectInteraction;\n metadata: Record<string, unknown>;\n}\n\n/**\n * Startup interaction state carried by a project (see\n * {@link GeoLibreProject.interaction}).\n */\nexport interface ProjectInteraction {\n /**\n * Identify target on open: a layer id, `\"all\"` for every visible queryable\n * layer, a list of layer ids to identify only those, or `null` for off.\n */\n identify?: string | string[] | null;\n /**\n * Map control or toolbar panel name (`\"search\"`, `\"bookmark\"`, `\"globe\"`,\n * ...) to whether it starts shown. Unknown names are ignored on open.\n */\n controls?: Record<string, boolean>;\n}\n\nexport type CommentAnchor =\n | { type: \"point\"; lngLat: [number, number] }\n | { type: \"feature\"; layerId: string; featureId: string | number; lngLat?: [number, number] };\n\nexport interface CommentAuthor {\n name: string;\n color: string;\n}\n\nexport interface CommentReply {\n id: string;\n author: CommentAuthor;\n body: string;\n createdAt: string;\n}\n\nexport interface ProjectComment {\n id: string;\n anchor: CommentAnchor;\n author: CommentAuthor;\n body: string;\n createdAt: string;\n resolved: boolean;\n replies: CommentReply[];\n}\n\nexport interface RecentProjectEntry {\n path: string;\n name: string;\n openedAt: string;\n}\n\n/**\n * Size at or above which a local vector file is read through DuckDB instead of\n * the in-memory JavaScript readers, and the feature count above which the user\n * is asked before every feature is materialized as GeoJSON.\n *\n * These live in core so the desktop loaders (`duckdb-vector-guard.ts`) and the\n * Add Vector Layer panel (`@geolibre/plugins`, which configures the\n * `maplibre-gl-vector` control's `autoThreshold`) switch strategy at the *same*\n * numbers. Before they were unified the two entry points disagreed — the panel\n * tiled at 25 MB / 50k while drag-and-drop stayed in memory to 100 MB — so the\n * same file behaved differently depending on how it was added, and no single\n * number could be documented.\n */\nexport const DUCKDB_VECTOR_ROUTE_BYTES = 100 * 1024 * 1024; // 100 MB\nexport const DUCKDB_VECTOR_FEATURE_WARN_COUNT = 100_000;\n\n/** A collection whose coordinates cannot be WGS84 longitude/latitude. */\nexport interface NonGeographicCoordinates {\n /** How many coordinates were inspected. */\n sampled: number;\n /** The largest |x| seen — a longitude may not exceed 180. */\n maxAbsX: number;\n /** The largest |y| seen — a latitude may not exceed 90. */\n maxAbsY: number;\n}\n\nconst MAX_WGS84_LON = 180;\nconst MAX_WGS84_LAT = 90;\n\n/**\n * Detect a collection that declares (or is assumed to be) WGS84 but carries\n * projected coordinates — the failure mode where a layer loads cleanly, appears\n * in the Layers panel, and renders nowhere because its \"longitude\" is a easting\n * in metres or feet.\n *\n * GeoLibre honours whatever CRS a file declares, so a file that declares\n * `CRS84`/`GCS_WGS_1984` while holding State Plane or Albers coordinates is\n * passed through untouched and lands off the map with no error. Callers use\n * this to warn instead of failing silently; it does not guess the true CRS,\n * which only the user knows.\n *\n * Sampling stops at `sampleLimit` coordinates: out-of-range values are a\n * property of the whole file, so a prefix is enough and a 3-million-coordinate\n * collection is not walked twice.\n *\n * @returns Details when a coordinate is out of geographic range, else null.\n */\nexport function detectNonGeographicCoordinates(\n geojson: GeoJSON.FeatureCollection | undefined,\n sampleLimit = 1000,\n): NonGeographicCoordinates | null {\n if (!geojson?.features?.length) return null;\n let sampled = 0;\n let maxAbsX = 0;\n let maxAbsY = 0;\n let offending = false;\n\n const visit = (coords: unknown): void => {\n if (sampled >= sampleLimit || !Array.isArray(coords)) return;\n if (typeof coords[0] === \"number\" && typeof coords[1] === \"number\") {\n const x = Math.abs(coords[0]);\n const y = Math.abs(coords[1]);\n sampled += 1;\n // Gated on finiteness for the same reason as `offending` below: an\n // Infinity would otherwise be reported as the offending magnitude in the\n // warning, hiding the real out-of-range value.\n if (Number.isFinite(x) && x > maxAbsX) maxAbsX = x;\n if (Number.isFinite(y) && y > maxAbsY) maxAbsY = y;\n // NaN/Infinity are a different defect (a broken file, not a CRS mismatch),\n // so only finite out-of-range values count.\n if (Number.isFinite(x) && Number.isFinite(y) && (x > MAX_WGS84_LON || y > MAX_WGS84_LAT)) {\n offending = true;\n }\n return;\n }\n for (const part of coords) {\n if (sampled >= sampleLimit) return;\n visit(part);\n }\n };\n\n for (const feature of geojson.features) {\n if (sampled >= sampleLimit) break;\n const geometry = feature?.geometry as {\n coordinates?: unknown;\n geometries?: { coordinates?: unknown }[];\n } | null;\n // A GeometryCollection holds its coordinates one level down, under\n // `geometries[]`, so it has no `coordinates` of its own to visit.\n if (geometry?.coordinates !== undefined) visit(geometry.coordinates);\n else if (Array.isArray(geometry?.geometries)) {\n for (const member of geometry.geometries) {\n if (sampled >= sampleLimit) break;\n if (member?.coordinates !== undefined) visit(member.coordinates);\n }\n }\n }\n\n return offending ? { sampled, maxAbsX, maxAbsY } : null;\n}\n","/**\n * 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 = '&copy; <a href=\"https://www.amap.com\">高德地图 Amap</a>';\nconst TENCENT_ATTRIBUTION = '&copy; <a href=\"https://map.qq.com\">腾讯地图 Tencent Maps</a>';\n\nconst sentinel = (id: string) => `${REGIONAL_BASEMAP_SENTINEL_PREFIX}${id}`;\n\n// Amap serves each product from four numbered hosts; MapLibre takes a single\n// template per source, so pin host 01. Browsers multiplex over HTTP/2, which\n// makes the sharding a non-issue. `style` selects the product: 7 street, 6\n// imagery, 8 roads-and-labels.\nconst AMAP_STREET_URL =\n \"https://wprd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scl=1&style=7&x={x}&y={y}&z={z}\";\nconst AMAP_SATELLITE_URL = \"https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}\";\nconst AMAP_LABELS_URL = \"https://webst01.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}\";\n\nconst tencentUrl = (styleId: number) =>\n `https://rt0.map.gtimg.com/tile?z={z}&x={x}&y={y}&styleid=${styleId}&scene=0`;\n\n/**\n * Basemaps served from inside mainland China.\n *\n * Zoom limits are each source's probed maximum, so MapLibre overzooms (blurs)\n * rather than painting a placeholder: Amap's imagery returns a \"no imagery\"\n * tile rather than a 404 past zoom 18, and Tencent 400s past 19 with zoom 19\n * already blank.\n */\nexport const CHINA_BASEMAPS: readonly RegionalBasemap[] = [\n {\n id: \"amap-street\",\n region: \"china\",\n name: \"高德地图\",\n styleUrl: sentinel(\"amap-street\"),\n tileUrl: AMAP_STREET_URL,\n maxZoom: 19,\n attribution: AMAP_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"amap-satellite\",\n region: \"china\",\n name: \"高德卫星\",\n styleUrl: sentinel(\"amap-satellite\"),\n tileUrl: AMAP_SATELLITE_URL,\n maxZoom: 18,\n attribution: AMAP_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"amap-hybrid\",\n region: \"china\",\n name: \"高德混合\",\n styleUrl: sentinel(\"amap-hybrid\"),\n tileUrl: AMAP_SATELLITE_URL,\n overlayTileUrl: AMAP_LABELS_URL,\n maxZoom: 18,\n attribution: AMAP_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"tencent-street\",\n region: \"china\",\n name: \"腾讯地图\",\n styleUrl: sentinel(\"tencent-street\"),\n tileUrl: tencentUrl(1),\n scheme: \"tms\",\n maxZoom: 18,\n attribution: TENCENT_ATTRIBUTION,\n gcj02: true,\n },\n {\n id: \"tencent-dark\",\n region: \"china\",\n name: \"腾讯深色\",\n styleUrl: sentinel(\"tencent-dark\"),\n tileUrl: tencentUrl(4),\n scheme: \"tms\",\n maxZoom: 18,\n attribution: TENCENT_ATTRIBUTION,\n gcj02: true,\n },\n];\n\n/** Every regional basemap, across all regions. */\nexport const REGIONAL_BASEMAPS: readonly RegionalBasemap[] = CHINA_BASEMAPS;\n\n/**\n * The regional basemaps grouped for display, one entry per region. Both the New\n * Project and Change Basemap panels render this inside a single collapsible\n * \"Regional\" section, so adding a region is a change here rather than in two\n * dialogs.\n */\nexport const REGIONAL_BASEMAP_GROUPS: readonly {\n id: RegionalBasemapRegionId;\n basemaps: readonly RegionalBasemap[];\n}[] = [{ id: \"china\", basemaps: CHINA_BASEMAPS }];\n\n/** Look up a regional basemap by its `geolibre://regional-basemap/<id>` sentinel. */\nexport function getRegionalBasemapByStyleUrl(\n styleUrl: string | undefined,\n): RegionalBasemap | undefined {\n if (!styleUrl) return undefined;\n return REGIONAL_BASEMAPS.find((basemap) => basemap.styleUrl === styleUrl);\n}\n\n/** Look up a regional basemap by id. */\nexport function getRegionalBasemapById(id: string | undefined): RegionalBasemap | undefined {\n if (!id) return undefined;\n return REGIONAL_BASEMAPS.find((basemap) => basemap.id === id);\n}\n\n/** Whether a style URL is a regional-basemap sentinel, resolvable or not. */\nexport function isRegionalBasemapSentinel(styleUrl: string | undefined): boolean {\n return Boolean(styleUrl?.startsWith(REGIONAL_BASEMAP_SENTINEL_PREFIX));\n}\n","/**\n * Maps the project's basemap onto imagery the Cesium globe can render.\n *\n * The 2D panes render `basemapStyleUrl` through MapLibre, which understands\n * vector GL styles and GeoLibre's `geolibre://` sentinels. Cesium understands\n * neither: it draws imagery from tile providers only. So a globe pane that\n * simply mounted the store basemap would show nothing, which is why the pane\n * used to hardcode Ion World Imagery (with a token) or OpenStreetMap (without)\n * and ignore the user's choice entirely — leaving a bright globe beside dark 2D\n * panes and breaking the \"same map, different projection\" promise the multi-map\n * grid makes.\n *\n * This module is the translation. It is deliberately **engine-free** — it\n * returns a plain descriptor and never imports Cesium — so it lives in\n * `@geolibre/core` beside the basemap catalogs it reads, the 2D and 3D\n * renderers share one source of truth, and the Cesium chunk boundary in\n * `vite.config.ts` stays intact.\n *\n * Three outcomes are possible, and the caller has to handle all three (see\n * {@link CesiumBasemapImagery}): tiles to draw, nothing to draw, or \"no raster\n * equivalent exists — pick your own default\".\n */\n\nimport { getPlanetaryBasemapByStyleUrl } from \"./ellipsoids\";\nimport { getRegionalBasemapByStyleUrl } from \"./regional-basemaps\";\nimport { BLANK_BASEMAP, DEFAULT_BASEMAP } from \"./types\";\n\n/** Stable project IDs and the corresponding Cesium bundled thumbnails / ion assets. */\nexport const CESIUM_BASEMAPS = [\n { id: \"project\", name: \"Project basemap\", icon: \"naturalEarthII.png\" },\n { id: \"natural-earth\", name: \"Natural Earth II\", icon: \"naturalEarthII.png\" },\n { id: \"bing-aerial\", name: \"Bing Maps Aerial\", icon: \"bingAerial.png\", assetId: 2 },\n {\n id: \"bing-labels\",\n name: \"Bing Maps Aerial with Labels\",\n icon: \"bingAerialLabels.png\",\n assetId: 3,\n },\n { id: \"bing-roads\", name: \"Bing Maps Roads\", icon: \"bingRoads.png\", assetId: 4 },\n { id: \"sentinel-2\", name: \"Sentinel-2\", icon: \"sentinel-2.png\", assetId: 3954 },\n { id: \"blue-marble\", name: \"Blue Marble\", icon: \"blueMarble.png\", assetId: 3845 },\n { id: \"earth-at-night\", name: \"Earth at night\", icon: \"earthAtNight.png\", assetId: 3812 },\n {\n id: \"google-satellite\",\n name: \"Google Maps Satellite\",\n icon: \"googleSatellite.png\",\n assetId: 3830182,\n },\n {\n id: \"google-labels\",\n name: \"Google Maps Satellite with Labels\",\n icon: \"googleSatelliteLabels.png\",\n assetId: 3830183,\n },\n { id: \"google-roads\", name: \"Google Maps Roadmap\", icon: \"googleRoadmap.png\", assetId: 3830184 },\n {\n id: \"google-contour\",\n name: \"Google Maps Contour\",\n icon: \"googleContour.png\",\n assetId: 3830186,\n },\n { id: \"azure-aerial\", name: \"Azure Maps Aerial\", icon: \"azureAerial.png\", assetId: 3891168 },\n { id: \"azure-roads\", name: \"Azure Maps Roads\", icon: \"azureRoads.png\", assetId: 3891169 },\n {\n id: \"esri-imagery\",\n name: \"ArcGIS World Imagery\",\n icon: \"ArcGisMapServiceWorldImagery.png\",\n category: \"Other\",\n service: \"World_Imagery\",\n },\n {\n id: \"esri-hillshade\",\n name: \"ArcGIS World Hillshade\",\n icon: \"ArcGisMapServiceWorldHillshade.png\",\n category: \"Other\",\n service: \"Elevation/World_Hillshade\",\n },\n {\n id: \"esri-ocean\",\n name: \"Esri World Ocean\",\n icon: \"ArcGisMapServiceWorldOcean.png\",\n category: \"Other\",\n service: \"Ocean/World_Ocean_Base\",\n },\n { id: \"osm\", name: \"OpenStreetMap\", icon: \"openStreetMap.png\", category: \"Other\" },\n] as const;\n\nexport type CesiumBasemapId = (typeof CESIUM_BASEMAPS)[number][\"id\"];\n\nexport function normalizeCesiumBasemap(value: unknown): CesiumBasemapId {\n return CESIUM_BASEMAPS.find((entry) => entry.id === value)?.id ?? \"project\";\n}\n\n/**\n * Missing credentials fall back to keyless imagery without changing the saved\n * choice.\n *\n * The fallback is ArcGIS World Imagery rather than the project background: a\n * globe wants to look like the Earth, and the project's own basemap is usually\n * a vector street style whose raster analogue is mostly empty ocean from\n * orbit. It needs no key, so the globe looks the same with or without an Ion\n * token — only terrain and the Ion-only basemaps are lost.\n */\nexport function availableCesiumBasemap(value: unknown, hasIonToken: boolean): CesiumBasemapId {\n const keyless = \"esri-imagery\";\n const requested = value ?? (hasIonToken ? \"bing-aerial\" : keyless);\n const entry = CESIUM_BASEMAPS.find((entry) => entry.id === requested);\n return entry && (!(\"assetId\" in entry) || hasIonToken) ? entry.id : keyless;\n}\n\n/**\n * What the globe should draw underneath the project's data layers.\n *\n * - `xyz` — a tile template to render, with the credit and zoom bounds that go\n * with it.\n * - `none` — the user chose the blank basemap; draw a bare ellipsoid.\n * - `default` — this basemap has no raster equivalent (a custom style URL, a\n * provider style, an offline archive). The renderer falls back to whatever it\n * can offer, which is Ion World Imagery when a token is configured and\n * OpenStreetMap otherwise.\n */\nexport type CesiumBasemapImagery =\n | { kind: \"none\" }\n | { kind: \"default\" }\n | { kind: \"ion\"; assetId: number }\n | { kind: \"natural-earth\" }\n | { kind: \"arcgis\"; url: string }\n | {\n kind: \"xyz\";\n /** Tile template with `{z}`/`{x}`/`{y}` placeholders. */\n template: string;\n /** Credit to show on the globe, as the HTML the 2D map already uses. */\n attribution: string;\n /** Max native zoom of the source, so the globe overzooms rather than 404s. */\n maximumLevel?: number;\n /**\n * Tile row ordering. `\"tms\"` numbers rows from the bottom; absent means\n * standard XYZ. Cesium has no `scheme` option, so a TMS template needs\n * `{y}` swapped for its `{reverseY}` placeholder at render time.\n */\n scheme?: \"tms\";\n /**\n * A transparent overlay drawn directly above {@link template} — the\n * roads-and-labels tiles a hybrid basemap (Amap Hybrid) bakes in, so one\n * basemap selection still yields a labelled satellite globe.\n */\n overlayTemplate?: string;\n };\n\n/**\n * A keyless raster basemap standing in for a vector style of the same tone.\n *\n * \"Keyless\" is the binding constraint, and it rules out most of the obvious\n * candidates: CARTO's raster endpoints (which the 2D basemap control still\n * lists) now stamp every tile with an \"API KEY REQUIRED\" watermark, and\n * Stadia/Stamen and Thunderforest have required keys for years. What is left\n * and still permissively licensed is OpenStreetMap's own tiles and EOX Maps'\n * WMTS layers (CC BY-SA), which between them cover the three tones GeoLibre's\n * vector basemaps span.\n */\ninterface RasterAnalogue {\n template: string;\n attribution: string;\n /** Max native zoom of the source; the globe overzooms past it rather than 404s. */\n maximumLevel: number;\n}\n\nconst OSM_CREDIT =\n '© <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors';\n/** EOX Maps serves these layers keyless under CC BY-SA 4.0. */\nconst EOX_CREDIT = '<a href=\"https://maps.eox.at\">EOX Maps</a> (CC BY-SA 4.0)';\n\nconst eoxLayer = (layer: string, extension: string): string =>\n `https://tiles.maps.eox.at/wmts/1.0.0/${layer}/default/g/{z}/{y}/{x}.${extension}`;\n\n/** Colourful streets — the tone of Liberty and Bright. */\nconst STREETS: RasterAnalogue = {\n template: \"https://tile.openstreetmap.org/{z}/{x}/{y}.png\",\n attribution: OSM_CREDIT,\n maximumLevel: 19,\n};\n\n/** Light and muted — the tone of Positron and the pale Protomaps flavors. */\nconst LIGHT: RasterAnalogue = {\n template: eoxLayer(\"terrain-light_3857\", \"jpg\"),\n attribution: `Terrain Light · ${OSM_CREDIT} · ${EOX_CREDIT}`,\n maximumLevel: 16,\n};\n\n/**\n * Dark — the tone of Dark, Fiord, and the dark Protomaps flavors. NASA's Black\n * Marble night lights is the one keyless dark raster basemap left, and it suits\n * a globe: the point of matching here is that a dark project does not end up\n * with a glaring bright globe in the corner.\n */\nconst DARK: RasterAnalogue = {\n template: eoxLayer(\"blackmarble_3857\", \"jpg\"),\n attribution: `Black Marble © NASA Earth Observatory · ${EOX_CREDIT}`,\n // A low-resolution global product; past this the globe blurs rather than 404s.\n maximumLevel: 8,\n};\n\n/** Raster analogue per OpenFreeMap style name (the last path segment). */\nconst OPENFREEMAP_ANALOGUES: Record<string, RasterAnalogue> = {\n liberty: STREETS,\n bright: STREETS,\n positron: LIGHT,\n dark: DARK,\n fiord: DARK,\n};\n\n/** Raster analogue per Protomaps v5 flavor. */\nconst PROTOMAPS_ANALOGUES: Record<string, RasterAnalogue> = {\n light: LIGHT,\n white: LIGHT,\n grayscale: LIGHT,\n dark: DARK,\n black: DARK,\n};\n\nfunction toImagery(analogue: RasterAnalogue): CesiumBasemapImagery {\n return {\n kind: \"xyz\",\n template: analogue.template,\n attribution: analogue.attribution,\n maximumLevel: analogue.maximumLevel,\n };\n}\n\n/**\n * Whether two descriptors would draw the same thing.\n *\n * Descriptors are built fresh on every call, so two basemaps resolving to the\n * same analogue — OpenFreeMap `liberty` and `bright` are both the streets\n * tone — yield equal but distinct objects. A renderer that remembers the\n * descriptor it drew (to skip redundant work) must compare by value: a\n * reference check would miss that case and tear down and re-add an identical\n * tile provider, costing a flash and a round of re-requested tiles for no\n * visible change.\n */\nexport function sameCesiumImagery(a: CesiumBasemapImagery, b: CesiumBasemapImagery): boolean {\n if (a.kind !== b.kind) return false;\n if (a.kind === \"arcgis\" && b.kind === \"arcgis\") return a.url === b.url;\n if (a.kind === \"ion\" && b.kind === \"ion\") return a.assetId === b.assetId;\n // `none` and `default` carry no fields, so matching kinds is the whole test.\n if (a.kind !== \"xyz\" || b.kind !== \"xyz\") return true;\n return (\n a.template === b.template &&\n a.attribution === b.attribution &&\n a.maximumLevel === b.maximumLevel &&\n a.scheme === b.scheme &&\n a.overlayTemplate === b.overlayTemplate\n );\n}\n\n/**\n * `decodeURIComponent` that reports failure instead of throwing. A URL keeps a\n * malformed escape (`%E0%A4`) in its pathname quite happily, but decoding one\n * throws `URIError` — and this runs inside a React render, so an unhandled throw\n * would take the globe pane down instead of falling through to the default.\n */\nfunction safeDecode(value: string): string | undefined {\n try {\n return decodeURIComponent(value);\n } catch {\n return undefined;\n }\n}\n\n/**\n * The raster analogue for a vector GL style URL, or undefined when the style is\n * not one GeoLibre ships (a provider style, a user's own URL, a Mapbox style).\n * Matching on host + path rather than searching the URL for \"dark\"/\"light\"\n * keeps an unrelated style whose *key* happens to contain one of those words\n * from being mapped to the wrong tone.\n */\nfunction vectorStyleAnalogue(styleUrl: string): RasterAnalogue | undefined {\n let parsed: URL;\n try {\n parsed = new URL(styleUrl);\n } catch {\n return undefined;\n }\n if (parsed.hostname === \"tiles.openfreemap.org\") {\n // https://tiles.openfreemap.org/styles/<name>\n const name = parsed.pathname.split(\"/\").filter(Boolean).at(-1);\n return name ? OPENFREEMAP_ANALOGUES[name] : undefined;\n }\n if (parsed.hostname === \"api.protomaps.com\") {\n // https://api.protomaps.com/styles/v5/<flavor>/<lang>.json?key=…\n const segments = parsed.pathname.split(\"/\").filter(Boolean);\n const flavor = segments[0] === \"styles\" ? segments[2] : undefined;\n const decoded = flavor ? safeDecode(flavor) : undefined;\n return decoded ? PROTOMAPS_ANALOGUES[decoded] : undefined;\n }\n return undefined;\n}\n\n/**\n * Translate a basemap style URL into imagery for the Cesium globe.\n *\n * Planetary and regional basemaps are already raster tile sets, so they carry\n * straight over — the globe shows the same Mars/Moon/Amap tiles the 2D panes\n * do. GeoLibre's own vector styles map to a keyless raster basemap of matching\n * tone, so switching the project to a dark basemap darkens the globe too.\n * Everything else reports `default`, leaving the choice of fallback imagery to\n * the renderer (which knows whether an Ion token is configured).\n *\n * A `geolibre://` sentinel that no longer resolves is treated as the default\n * basemap, mirroring what `resolveMapStyle` does for the 2D map rather than\n * handing the sentinel onward as if it were fetchable.\n *\n * @param styleUrl - The project's `basemapStyleUrl`.\n * @returns What the globe should draw beneath the data layers.\n */\nexport function basemapToCesiumImagery(\n styleUrl: string | undefined,\n cesiumBasemap: CesiumBasemapId = \"project\",\n): CesiumBasemapImagery {\n const entry = CESIUM_BASEMAPS.find((entry) => entry.id === cesiumBasemap);\n if (entry && \"service\" in entry) {\n return {\n kind: \"arcgis\",\n url: `https://services.arcgisonline.com/ArcGIS/rest/services/${entry.service}/MapServer`,\n };\n }\n if (entry && \"assetId\" in entry) return { kind: \"ion\", assetId: entry.assetId };\n if (cesiumBasemap === \"natural-earth\") return { kind: \"natural-earth\" };\n if (cesiumBasemap === \"osm\") return toImagery(STREETS);\n if (styleUrl === BLANK_BASEMAP) return { kind: \"none\" };\n const url = styleUrl ?? DEFAULT_BASEMAP;\n\n const planetary = getPlanetaryBasemapByStyleUrl(url);\n if (planetary) {\n return {\n kind: \"xyz\",\n template: planetary.tileUrl,\n attribution: planetary.attribution,\n maximumLevel: planetary.maxZoom,\n ...(planetary.scheme ? { scheme: planetary.scheme } : {}),\n };\n }\n\n const regional = getRegionalBasemapByStyleUrl(url);\n if (regional) {\n return {\n kind: \"xyz\",\n template: regional.tileUrl,\n attribution: regional.attribution,\n maximumLevel: regional.maxZoom,\n ...(regional.scheme ? { scheme: regional.scheme } : {}),\n ...(regional.overlayTileUrl ? { overlayTemplate: regional.overlayTileUrl } : {}),\n };\n }\n\n // Every remaining sentinel kind — an offline PMTiles archive, a planetary or\n // regional id that has since been renamed — has no raster form to show, so\n // fall back the way the 2D map does instead of leaving the globe blank.\n // Reading the fallback back out of DEFAULT_BASEMAP keeps the two in step if\n // the default ever changes.\n const analogue = vectorStyleAnalogue(url.startsWith(\"geolibre://\") ? DEFAULT_BASEMAP : url);\n return analogue ? toImagery(analogue) : { kind: \"default\" };\n}\n","/**\n * Projections the ArcGIS renderer can draw its flat map in (issue #2708).\n *\n * MapLibre and Mapbox only draw Web Mercator and a globe, but the ArcGIS Maps\n * SDK reprojects vector layers on the client into any projected coordinate\n * system it knows by WKID. `preferences.map.arcgisWkid` names that system;\n * absent means the default Web Mercator map. The SDK cannot reproject tiled\n * layers (XYZ, Esri basemaps), so in another projection those do not draw.\n */\n\n/** A projection offered in the settings picker. */\nexport interface ArcgisProjectionPreset {\n /** The Esri or EPSG well-known ID the SDK resolves. */\n wkid: number;\n /** English name; the UI translates it by `id`. */\n name: string;\n /** Stable key for the translated name (`settings.map.arcgisProjections.<id>`). */\n id: string;\n}\n\n/** WKIDs that are Web Mercator, i.e. the default map. */\nconst WEB_MERCATOR_WKIDS: ReadonlySet<number> = new Set([3857, 102100, 102113, 900913]);\n\n/** Presets, verified to load and frame in the SDK pinned by `ARCGIS_SDK_VERSION`. */\nexport const ARCGIS_PROJECTION_PRESETS: readonly ArcgisProjectionPreset[] = [\n { id: \"spilhaus\", wkid: 54099, name: \"Spilhaus (World Ocean)\" },\n { id: \"equalEarth\", wkid: 8857, name: \"Equal Earth\" },\n { id: \"robinson\", wkid: 54030, name: \"Robinson\" },\n { id: \"winkelTripel\", wkid: 54042, name: \"Winkel Tripel\" },\n { id: \"mollweide\", wkid: 54009, name: \"Mollweide\" },\n { id: \"plateCarree\", wkid: 4326, name: \"Plate Carrée (WGS 84)\" },\n { id: \"northPolar\", wkid: 3995, name: \"Arctic Polar Stereographic\" },\n { id: \"southPolar\", wkid: 3031, name: \"Antarctic Polar Stereographic\" },\n];\n\n/**\n * Normalize a stored ArcGIS projection WKID.\n *\n * Args:\n * value: The raw `preferences.map.arcgisWkid` value.\n *\n * Returns:\n * A positive integer WKID other than Web Mercator, or `undefined` for the\n * default Web Mercator map (absent, malformed or a Web Mercator code).\n */\nexport function normalizeArcgisWkid(value: unknown): number | undefined {\n // Digits only: `Number` would also read \"1e3\" or \"0x10\" as a code.\n const wkid =\n typeof value === \"string\" ? (/^\\s*\\d+\\s*$/.test(value) ? Number(value) : undefined) : value;\n if (typeof wkid !== \"number\" || !Number.isSafeInteger(wkid) || wkid <= 0) return undefined;\n return WEB_MERCATOR_WKIDS.has(wkid) ? undefined : wkid;\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. The same data can carry its elevation\n * in a `bbox` member too, which `horizontalBbox` trims back to two dimensions.\n */\n\n/**\n * Reduces a bounding box to its horizontal extent, `[west, south, east, north]`.\n *\n * RFC 7946 §5 lets a `bbox` member carry elevation, in which case it holds six\n * values — `[west, south, minAltitude, east, north, maxAltitude]` — and\n * `@turf/bbox` returns a collection's own member verbatim rather than\n * recomputing it. A consumer reading such a box as four values takes the\n * altitude for a longitude and the east edge for a latitude; that is how a 3D\n * feed reached MapLibre as an out-of-range latitude (#2358).\n *\n * Returns null when there is no usable horizontal extent: a box of any other\n * length, or one carrying a non-finite value anywhere — an empty collection, or\n * one whose features all have a null geometry, bboxes to ±Infinity, and a box\n * whose altitudes are not finite is not one to trust the rest of.\n *\n * @param box - A GeoJSON or Turf bounding box, of four or six values.\n */\nexport function horizontalBbox(\n box: readonly number[] | null | undefined,\n): [number, number, number, number] | null {\n if (!box) return null;\n if (box.length !== 4 && box.length !== 6) return null;\n if (!box.every((value) => Number.isFinite(value))) return null;\n return box.length === 6 ? [box[0], box[1], box[3], box[4]] : [box[0], box[1], box[2], box[3]];\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>();\nconst hasFiniteZCache = 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 * @param includeZero - Accept zero elevations when explicit absolute-height placement matters.\n */\nexport function geojsonHasZCoordinates(\n geojson: GeoJSON | null | undefined,\n includeZero = false,\n): boolean {\n if (!geojson) return false;\n const cache = includeZero ? hasFiniteZCache : hasZCache;\n const cached = cache.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) && (includeZero || z !== 0);\n }),\n );\n cache.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","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","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 fn(\n \"number-format\",\n '[\"number-format\", [\"get\", \"field\"], {\"locale\": \"en-US\", \"max-fraction-digits\": 2}]',\n \"numberFormat\",\n ),\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 /**\n * Runs the expression against one feature. `zoom` overrides the zoom the\n * expression was compiled with, for callers that re-evaluate a\n * zoom-dependent expression as the camera moves.\n */\n evaluate?: (feature: Feature, zoom?: number) => 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 compiledZoom = options.zoom ?? 0;\n return {\n ok: true,\n errors: [],\n evaluate: (feature, zoom = compiledZoom) =>\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","/**\n * Helpers for a layer's user-authored catalog metadata\n * ({@link LayerDescriptiveMetadata}, issue #2858): normalization for the\n * project file, field validation for the Metadata dialog, and the STAC 1.0\n * Item export.\n */\nimport type {\n GeoLibreLayer,\n LayerDescriptiveMetadata,\n LayerMetadataContact,\n LayerMetadataLink,\n LayerMetadataTemporalExtent,\n} from \"./types\";\n\n/** STAC specification version the Item export targets. */\nexport const STAC_VERSION = \"1.0.0\";\n\n/** Schema URL of the STAC processing extension, declared for `processing:lineage`. */\nexport const STAC_PROCESSING_EXTENSION =\n \"https://stac-extensions.github.io/processing/v1.1.0/schema.json\";\n\n/** URL schemes a metadata link (or asset) may use. */\nconst ALLOWED_LINK_PROTOCOLS = new Set([\"http:\", \"https:\", \"ftp:\", \"s3:\", \"gs:\", \"mailto:\"]);\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst DATE_ONLY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst DATE_TIME_PATTERN =\n /^(\\d{4})-(\\d{2})-(\\d{2})[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d+)?)?(Z|[+-]\\d{2}:?\\d{2})?$/i;\n/** The STAC 1.0 `license` pattern (an SPDX identifier, `various` or `proprietary`). */\nconst STAC_LICENSE_PATTERN = /^[\\w\\-.+]+$/;\n\n/**\n * Trim a value to a non-empty string, or `undefined`.\n *\n * @param value - Any value.\n * @returns The trimmed string, or `undefined` for a non-string or blank value.\n */\nfunction cleanString(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed === \"\" ? undefined : trimmed;\n}\n\n/**\n * Split comma-separated keyword text into a clean keyword list.\n *\n * @param text - Text such as `\"roads, transport, osm\"`.\n * @returns The trimmed, non-empty keywords with case-insensitive duplicates removed.\n */\nexport function parseMetadataKeywords(text: string): string[] {\n return normalizeKeywords(text.split(\",\"));\n}\n\nfunction normalizeKeywords(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n const seen = new Set<string>();\n const keywords: string[] = [];\n for (const entry of value) {\n const keyword = cleanString(entry);\n if (!keyword) continue;\n const key = keyword.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n keywords.push(keyword);\n }\n return keywords;\n}\n\nfunction normalizeContact(value: unknown): LayerMetadataContact | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const contact: LayerMetadataContact = {};\n const name = cleanString(raw.name);\n const email = cleanString(raw.email);\n const organization = cleanString(raw.organization);\n if (name) contact.name = name;\n if (email) contact.email = email;\n if (organization) contact.organization = organization;\n return Object.keys(contact).length > 0 ? contact : undefined;\n}\n\nfunction normalizeTemporalExtent(value: unknown): LayerMetadataTemporalExtent | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const extent: LayerMetadataTemporalExtent = {};\n const start = cleanString(raw.start);\n const end = cleanString(raw.end);\n if (start) extent.start = start;\n if (end) extent.end = end;\n return Object.keys(extent).length > 0 ? extent : undefined;\n}\n\nfunction normalizeLinks(value: unknown): LayerMetadataLink[] {\n if (!Array.isArray(value)) return [];\n const links: LayerMetadataLink[] = [];\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) continue;\n const raw = entry as Record<string, unknown>;\n const href = cleanString(raw.href);\n // A link is its address: a row with only a label is an unfinished edit.\n if (!href) continue;\n const link: LayerMetadataLink = { href };\n const rel = cleanString(raw.rel);\n const title = cleanString(raw.title);\n if (rel) link.rel = rel;\n if (title) link.title = title;\n links.push(link);\n }\n return links;\n}\n\n/**\n * Normalize a raw descriptive-metadata value (from a project file, a plugin or\n * the Metadata dialog): keep only string fields, trim them, and drop empty\n * fields, empty keywords and links without an address.\n *\n * Values are not validated here — a malformed email or date in a hand-edited\n * file is kept so the dialog can show and fix it rather than silently lose it.\n *\n * @param value - Any value.\n * @returns The cleaned record, or `undefined` when nothing is left, so an\n * empty record is never written to a project.\n */\nexport function normalizeLayerDescriptiveMetadata(\n value: unknown,\n): LayerDescriptiveMetadata | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const raw = value as Record<string, unknown>;\n const metadata: LayerDescriptiveMetadata = {};\n for (const key of [\"title\", \"abstract\", \"license\", \"attribution\", \"lineage\"] as const) {\n const text = cleanString(raw[key]);\n if (text) metadata[key] = text;\n }\n const keywords = normalizeKeywords(raw.keywords);\n if (keywords.length > 0) metadata.keywords = keywords;\n const contact = normalizeContact(raw.contact);\n if (contact) metadata.contact = contact;\n const temporalExtent = normalizeTemporalExtent(raw.temporalExtent);\n if (temporalExtent) metadata.temporalExtent = temporalExtent;\n const links = normalizeLinks(raw.links);\n if (links.length > 0) metadata.links = links;\n return Object.keys(metadata).length > 0 ? metadata : undefined;\n}\n\n/**\n * Whether a string is a plausible email address (`local@domain.tld`).\n *\n * @param value - The candidate address.\n * @returns `true` when it has the shape of an email address.\n */\nexport function isValidMetadataEmail(value: string): boolean {\n return EMAIL_PATTERN.test(value.trim());\n}\n\n/**\n * Whether a string is an absolute URL with a scheme a metadata link may use\n * (http, https, ftp, s3, gs, mailto). Script and data URLs are rejected.\n *\n * @param value - The candidate URL.\n * @returns `true` when it parses as an allowed absolute URL.\n */\nexport function isValidMetadataUrl(value: string): boolean {\n try {\n const url = new URL(value.trim());\n return ALLOWED_LINK_PROTOCOLS.has(url.protocol);\n } catch {\n return false;\n }\n}\n\n/**\n * Whether the calendar fields name a real day (rejecting e.g. February 30).\n *\n * @param year - Four-digit year.\n * @param month - Month, 1-12.\n * @param day - Day of month.\n * @returns `true` when the date exists.\n */\nfunction isRealDate(year: number, month: number, day: number): boolean {\n // setUTCFullYear, not Date.UTC: the latter maps years 0-99 to 1900-1999.\n const date = new Date(0);\n date.setUTCFullYear(year, month - 1, day);\n return (\n date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day\n );\n}\n\n/**\n * Whether a string is an ISO 8601 date (`YYYY-MM-DD`) or date-time\n * (`YYYY-MM-DDTHH:MM[:SS[.fff]][Z|±HH:MM]`) naming a real instant.\n *\n * @param value - The candidate date.\n * @returns `true` when it is a valid ISO 8601 date or date-time.\n */\nexport function isValidMetadataDate(value: string): boolean {\n const text = value.trim();\n const dateOnly = DATE_ONLY_PATTERN.exec(text);\n if (dateOnly) return isRealDate(Number(dateOnly[1]), Number(dateOnly[2]), Number(dateOnly[3]));\n const dateTime = DATE_TIME_PATTERN.exec(text);\n if (!dateTime) return false;\n if (!isRealDate(Number(dateTime[1]), Number(dateTime[2]), Number(dateTime[3]))) return false;\n const hours = Number(dateTime[4]);\n const minutes = Number(dateTime[5]);\n const seconds = dateTime[6] === undefined ? 0 : Number(dateTime[6]);\n return hours < 24 && minutes < 60 && seconds < 60;\n}\n\n/**\n * Convert a validated metadata date to an RFC 3339 UTC date-time, as STAC\n * requires. A date-only bound covers its whole day: a start becomes the first\n * second of the day, an end the last. A date-time without an offset is read\n * as UTC so the export does not depend on the exporting machine's time zone.\n *\n * @param value - A date accepted by {@link isValidMetadataDate}.\n * @param bound - Whether the value starts or ends an interval.\n * @returns The RFC 3339 date-time, or `null` for an invalid date.\n */\nexport function metadataDateToRfc3339(value: string, bound: \"start\" | \"end\"): string | null {\n const text = value.trim();\n if (!isValidMetadataDate(text)) return null;\n if (DATE_ONLY_PATTERN.test(text)) {\n return `${text}T${bound === \"start\" ? \"00:00:00\" : \"23:59:59\"}Z`;\n }\n // A compact `+HHMM` offset passes validation but is not an ECMAScript\n // date-time format, so spell it `+HH:MM` before parsing. The pattern is\n // case-insensitive, but ECMAScript only promises to read an upper-case `T`\n // and `Z`, so upper-case the text first.\n const normalized = text\n .toUpperCase()\n .replace(\" \", \"T\")\n .replace(/([+-]\\d{2})(\\d{2})$/, \"$1:$2\");\n const hasOffset = /(Z|[+-]\\d{2}:\\d{2})$/.test(normalized);\n const parsed = new Date(hasOffset ? normalized : `${normalized}Z`);\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();\n}\n\n/** Why a metadata field failed {@link validateLayerDescriptiveMetadata}. */\nexport type LayerMetadataIssueCode = \"email\" | \"url\" | \"date\" | \"dateOrder\";\n\n/** One validation failure, addressed by a dotted field path. */\nexport interface LayerMetadataIssue {\n /**\n * The failing field: `contact.email`, `temporalExtent.start`,\n * `temporalExtent.end`, or `links.<index>.href`.\n */\n field: string;\n code: LayerMetadataIssueCode;\n}\n\n/**\n * Validate the fields of a descriptive-metadata record that have a format:\n * the contact email, the temporal-extent dates (and their order), and the\n * link URLs. Free-text fields are not checked.\n *\n * @param metadata - The record to check (normalized or a dialog draft).\n * @returns Every failure found; empty when the record is valid.\n */\nexport function validateLayerDescriptiveMetadata(\n metadata: LayerDescriptiveMetadata | undefined,\n): LayerMetadataIssue[] {\n const issues: LayerMetadataIssue[] = [];\n if (!metadata) return issues;\n const email = cleanString(metadata.contact?.email);\n if (email && !isValidMetadataEmail(email)) issues.push({ field: \"contact.email\", code: \"email\" });\n const start = cleanString(metadata.temporalExtent?.start);\n const end = cleanString(metadata.temporalExtent?.end);\n const startValid = start ? isValidMetadataDate(start) : true;\n const endValid = end ? isValidMetadataDate(end) : true;\n if (!startValid) issues.push({ field: \"temporalExtent.start\", code: \"date\" });\n if (!endValid) issues.push({ field: \"temporalExtent.end\", code: \"date\" });\n if (start && end && startValid && endValid) {\n const startTime = Date.parse(metadataDateToRfc3339(start, \"start\") ?? \"\");\n const endTime = Date.parse(metadataDateToRfc3339(end, \"end\") ?? \"\");\n if (startTime > endTime) issues.push({ field: \"temporalExtent.end\", code: \"dateOrder\" });\n }\n (metadata.links ?? []).forEach((link, index) => {\n const href = cleanString(link?.href);\n if (href && !isValidMetadataUrl(href)) {\n issues.push({ field: `links.${index}.href`, code: \"url\" });\n }\n });\n return issues;\n}\n\n/** A STAC 1.0 link object. */\nexport interface StacLink {\n href: string;\n rel: string;\n type?: string;\n title?: string;\n}\n\n/** A STAC 1.0 asset object. */\nexport interface StacAsset {\n href: string;\n type?: string;\n title?: string;\n roles?: string[];\n}\n\n/** A STAC 1.0 provider object. */\nexport interface StacProvider {\n name: string;\n description?: string;\n roles?: string[];\n url?: string;\n}\n\n/** The STAC 1.0 Item {@link buildLayerStacItem} produces. */\nexport interface StacItem {\n type: \"Feature\";\n stac_version: string;\n stac_extensions: string[];\n id: string;\n geometry: Record<string, unknown> | null;\n bbox?: [number, number, number, number];\n properties: Record<string, unknown> & { datetime: string | null };\n links: StacLink[];\n assets: Record<string, StacAsset>;\n}\n\n/** Options for {@link buildLayerStacItem}. */\nexport interface LayerStacItemOptions {\n /** The layer's extent in EPSG:4326 as `[west, south, east, north]`. */\n bbox?: readonly number[] | null;\n /** Where the layer's data can be fetched from, when it has a source URL. */\n assetHref?: string | null;\n /** Clock used for `datetime` when no temporal extent is recorded. */\n now?: Date;\n}\n\n/**\n * Validate a WGS84 bounding box.\n *\n * @param bbox - Candidate `[west, south, east, north]`.\n * @returns The box, or `null` when it is not four finite in-range numbers.\n */\nfunction validBbox(\n bbox: readonly number[] | null | undefined,\n): [number, number, number, number] | null {\n if (!bbox || bbox.length !== 4) return null;\n if (!bbox.every((value) => typeof value === \"number\" && Number.isFinite(value))) return null;\n const [west, south, east, north] = bbox;\n if (south < -90 || north > 90 || south > north) return null;\n if (west < -180 || west > 180 || east < -180 || east > 180) return null;\n return [west, south, east, north];\n}\n\n/**\n * The GeoJSON geometry covering a bounding box: a Point for a degenerate box,\n * a MultiPolygon split at the antimeridian when `west > east` (the STAC\n * convention for a box crossing it), otherwise a Polygon.\n *\n * @param bbox - A box accepted by {@link validBbox}.\n * @returns The footprint geometry.\n */\nfunction bboxGeometry([west, south, east, north]: [number, number, number, number]): Record<\n string,\n unknown\n> {\n if (west === east && south === north) return { type: \"Point\", coordinates: [west, south] };\n // Degenerate on one axis only (e.g. points sharing one longitude): a line,\n // not a zero-area polygon.\n if (west === east || south === north) {\n return {\n type: \"LineString\",\n coordinates: [\n [west, south],\n [east, north],\n ],\n };\n }\n const ring = (w: number, e: number) => [\n [w, south],\n [e, south],\n [e, north],\n [w, north],\n [w, south],\n ];\n if (west > east) {\n return { type: \"MultiPolygon\", coordinates: [[ring(west, 180)], [ring(-180, east)]] };\n }\n return { type: \"Polygon\", coordinates: [ring(west, east)] };\n}\n\n/** Media types by data-file extension, for the Item's data asset. */\nconst ASSET_MEDIA_TYPES: Record<string, string> = {\n geojson: \"application/geo+json\",\n json: \"application/json\",\n tif: \"image/tiff; application=geotiff\",\n tiff: \"image/tiff; application=geotiff\",\n parquet: \"application/vnd.apache.parquet\",\n geoparquet: \"application/vnd.apache.parquet\",\n fgb: \"application/vnd.flatgeobuf\",\n pmtiles: \"application/vnd.pmtiles\",\n gpkg: \"application/geopackage+sqlite3\",\n kml: \"application/vnd.google-earth.kml+xml\",\n kmz: \"application/vnd.google-earth.kmz\",\n csv: \"text/csv\",\n zip: \"application/zip\",\n las: \"application/vnd.las\",\n laz: \"application/vnd.laszip\",\n zarr: \"application/vnd+zarr\",\n};\n\n/**\n * Guess the media type of a layer's data asset from the URL's extension, or\n * the layer type when the URL has none (a COG is flagged cloud-optimized).\n *\n * @param href - The asset URL.\n * @param layerType - The layer's type.\n * @returns The media type, or `undefined` when it cannot be told.\n */\nexport function stacAssetMediaType(\n href: string,\n layerType: GeoLibreLayer[\"type\"],\n): string | undefined {\n let path = href;\n try {\n path = new URL(href).pathname;\n } catch {\n // Not an absolute URL: read the extension off the raw text.\n }\n const extension = /\\.([a-z0-9]+)$/i.exec(path.replace(/\\/+$/, \"\"))?.[1]?.toLowerCase();\n if (layerType === \"cog\" && (extension === \"tif\" || extension === \"tiff\" || !extension)) {\n return \"image/tiff; application=geotiff; profile=cloud-optimized\";\n }\n if (extension && ASSET_MEDIA_TYPES[extension]) return ASSET_MEDIA_TYPES[extension];\n if (layerType === \"geojson\" && !extension) return \"application/geo+json\";\n return undefined;\n}\n\n/**\n * Build a STAC 1.0 Item describing a layer from its descriptive metadata.\n *\n * Mapping: `id` is the layer id; `geometry`/`bbox` come from the WGS84 extent\n * (a `null` geometry, as STAC allows, when the extent is unknown); `title`\n * falls back to the layer name; `abstract` becomes `description`; the contact\n * becomes a `producer` provider; `lineage` becomes `processing:lineage` (with\n * the processing extension declared); the temporal extent becomes `datetime`\n * or `start_datetime`/`end_datetime` (with `datetime` set to the export time\n * when none is recorded, since STAC requires one); links keep their `rel`\n * (default `related`); and a source URL becomes the `data` asset.\n *\n * A license that is not an SPDX-style identifier (STAC 1.0's pattern) is\n * exported as `proprietary`, with the original text kept in\n * `geolibre:license`; the attribution is kept in `geolibre:attribution`.\n *\n * @param layer - The layer to describe.\n * @param options - Extent, asset URL and clock; see {@link LayerStacItemOptions}.\n * @returns The STAC Item, ready to `JSON.stringify`.\n */\nexport function buildLayerStacItem(\n layer: Pick<GeoLibreLayer, \"id\" | \"name\" | \"type\" | \"descriptiveMetadata\">,\n options: LayerStacItemOptions = {},\n): StacItem {\n const metadata = normalizeLayerDescriptiveMetadata(layer.descriptiveMetadata) ?? {};\n const now = options.now ?? new Date();\n const properties: StacItem[\"properties\"] = { datetime: null };\n properties.title = metadata.title ?? layer.name;\n if (metadata.abstract) properties.description = metadata.abstract;\n if (metadata.keywords?.length) properties.keywords = [...metadata.keywords];\n\n const start = metadata.temporalExtent?.start\n ? metadataDateToRfc3339(metadata.temporalExtent.start, \"start\")\n : null;\n const end = metadata.temporalExtent?.end\n ? metadataDateToRfc3339(metadata.temporalExtent.end, \"end\")\n : null;\n // Compared as instants: the two bounds may be spelled differently\n // (`…T00:00:00Z` from a date, `…T00:00:00.000Z` from a date-time).\n if (start && end && Date.parse(start) !== Date.parse(end)) {\n properties.start_datetime = start;\n properties.end_datetime = end;\n } else {\n // STAC needs a datetime unless both interval bounds are known: an instant\n // or a one-sided interval exports its known bound, and a layer with no\n // temporal extent is stamped with the export time.\n properties.datetime = start ?? end ?? now.toISOString();\n }\n\n if (metadata.license) {\n if (STAC_LICENSE_PATTERN.test(metadata.license)) {\n properties.license = metadata.license;\n } else {\n properties.license = \"proprietary\";\n properties[\"geolibre:license\"] = metadata.license;\n }\n }\n const contact = metadata.contact;\n const providerName = contact?.organization ?? contact?.name ?? contact?.email;\n if (contact && providerName) {\n const person = [\n contact.organization && contact.name ? contact.name : undefined,\n contact.email,\n ].filter(Boolean);\n const provider: StacProvider = { name: providerName, roles: [\"producer\"] };\n if (person.length > 0) provider.description = `Contact: ${person.join(\", \")}`;\n properties.providers = [provider];\n }\n if (metadata.attribution) properties[\"geolibre:attribution\"] = metadata.attribution;\n\n const extensions: string[] = [];\n if (metadata.lineage) {\n properties[\"processing:lineage\"] = metadata.lineage;\n extensions.push(STAC_PROCESSING_EXTENSION);\n }\n\n const links: StacLink[] = (metadata.links ?? [])\n .filter((link) => isValidMetadataUrl(link.href))\n .map((link) => ({\n href: link.href,\n rel: link.rel ?? \"related\",\n ...(link.title ? { title: link.title } : {}),\n }));\n\n const assets: Record<string, StacAsset> = {};\n const assetHref = cleanString(options.assetHref);\n if (assetHref) {\n // A tile template (`…/{z}/{x}/{y}.png`) cannot be fetched as-is, so it\n // is published as a `tiles` asset rather than a dereferenceable `data` one.\n const templated = /\\{[a-z]+\\}/i.test(assetHref);\n const type = templated ? undefined : stacAssetMediaType(assetHref, layer.type);\n assets[templated ? \"tiles\" : \"data\"] = {\n href: assetHref,\n ...(type ? { type } : {}),\n title: properties.title as string,\n roles: [templated ? \"tiles\" : \"data\"],\n };\n }\n\n const bbox = validBbox(options.bbox);\n return {\n type: \"Feature\",\n stac_version: STAC_VERSION,\n stac_extensions: extensions,\n id: layer.id,\n geometry: bbox ? bboxGeometry(bbox) : null,\n ...(bbox ? { bbox } : {}),\n properties,\n links,\n assets,\n };\n}\n","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/** GeoJSON columns are own properties; inherited object members are not data. */\nfunction fieldValue(properties: GeoJsonProperties, field: string): unknown {\n return properties && Object.hasOwn(properties, field) ? properties[field] : undefined;\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(fieldValue(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(fieldValue(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] = fieldValue(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(fieldValue(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 {\n AppCapabilities,\n AppPrivilege,\n AppRole,\n GeoLibreLayer,\n LayerCapabilities,\n} from \"./types\";\n\n/**\n * Default inferred capabilities for a layer based on its type and metadata.\n *\n * Inferred rules:\n * - `query`: true for all valid layers.\n * - `create`, `update`, `delete`: true for in-memory or editable GeoJSON vector layers,\n * false for derived queries (DuckDB/SQL), read-only vector tiles, and raster layers.\n * - `export`: true by default.\n */\nexport function inferLayerCapabilities(layer: GeoLibreLayer): Required<LayerCapabilities> {\n const isVector = layer.type === \"geojson\";\n const isDuckDB =\n layer.metadata?.sourceKind === \"duckdb-query\" ||\n (layer.type === \"geojson\" && typeof layer.metadata?.query === \"string\");\n const isExternalNative = layer.metadata?.externalNativeLayer === true;\n const isSketches = layer.metadata?.sourceKind === \"geoeditor-sketches\";\n const isSqlQuery = layer.metadata?.sourceKind === \"sql-query\";\n\n const isReadOnlyVector = isExternalNative && layer.metadata?.sourceKind !== \"maplibre-gl-vector\";\n const isEditable = isVector && !isDuckDB && !isReadOnlyVector && !isSketches && !isSqlQuery;\n\n return {\n query: true,\n create: isEditable,\n update: isEditable,\n delete: isEditable,\n export: true,\n };\n}\n\n/**\n * Resolves the effective capabilities for a layer by overlaying any explicit\n * capability overrides on top of inferred defaults.\n */\nexport function resolveLayerCapabilities(\n layer: GeoLibreLayer | undefined,\n): Required<LayerCapabilities> {\n if (!layer) {\n return {\n query: false,\n create: false,\n update: false,\n delete: false,\n export: false,\n };\n }\n\n const defaults = inferLayerCapabilities(layer);\n if (!layer.capabilities) {\n return defaults;\n }\n\n return {\n query: layer.capabilities.query ?? defaults.query,\n create: layer.capabilities.create ?? defaults.create,\n update: layer.capabilities.update ?? defaults.update,\n delete: layer.capabilities.delete ?? defaults.delete,\n export: layer.capabilities.export ?? defaults.export,\n };\n}\n\n/**\n * Normalizes an untrusted capabilities value from JSON/project data.\n */\nexport function normalizeLayerCapabilities(raw: unknown): LayerCapabilities | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return undefined;\n const obj = raw as Record<string, unknown>;\n const caps: LayerCapabilities = {};\n let hasAny = false;\n\n if (typeof obj.query === \"boolean\") {\n caps.query = obj.query;\n hasAny = true;\n }\n if (typeof obj.create === \"boolean\") {\n caps.create = obj.create;\n hasAny = true;\n }\n if (typeof obj.update === \"boolean\") {\n caps.update = obj.update;\n hasAny = true;\n }\n if (typeof obj.delete === \"boolean\") {\n caps.delete = obj.delete;\n hasAny = true;\n }\n if (typeof obj.export === \"boolean\") {\n caps.export = obj.export;\n hasAny = true;\n }\n\n return hasAny ? caps : undefined;\n}\n\n/**\n * All supported application privilege identifiers in GeoLibre.\n */\nexport const ALL_APP_PRIVILEGES: readonly AppPrivilege[] = [\n \"layers:edit\",\n \"layers:add-remote\",\n \"layers:add-local\",\n \"processing:run\",\n \"processing:sidecar\",\n \"project:save\",\n \"project:share\",\n \"project:share-public\",\n \"plugins:install\",\n \"assistant:use\",\n \"connections:manage\",\n \"export:data\",\n \"export:image\",\n \"settings:manage\",\n] as const;\n\n/**\n * Standard privilege bundles for named application roles.\n */\nexport const ROLE_PRIVILEGES: Record<Exclude<AppRole, \"custom\">, readonly AppPrivilege[]> = {\n viewer: [\"export:image\", \"export:data\"],\n editor: [\n \"export:image\",\n \"export:data\",\n \"layers:edit\",\n \"layers:add-local\",\n \"layers:add-remote\",\n \"processing:run\",\n \"project:save\",\n ],\n publisher: [\n \"export:image\",\n \"export:data\",\n \"layers:edit\",\n \"layers:add-local\",\n \"layers:add-remote\",\n \"processing:run\",\n \"project:save\",\n \"project:share\",\n \"project:share-public\",\n \"processing:sidecar\",\n \"assistant:use\",\n ],\n administrator: ALL_APP_PRIVILEGES,\n};\n\n/**\n * Resolves the effective privilege list for a given role, applying custom overrides if specified.\n */\nexport function resolveRolePrivileges(\n role: AppRole,\n customPrivileges?: readonly AppPrivilege[],\n): AppPrivilege[] {\n if (role === \"custom\") {\n if (!customPrivileges || customPrivileges.length === 0) return [];\n const validSet = new Set<AppPrivilege>(ALL_APP_PRIVILEGES);\n return [...new Set(customPrivileges.filter((p) => validSet.has(p)))];\n }\n return [...ROLE_PRIVILEGES[role]];\n}\n\n/**\n * Intersects multiple sets of privileges to derive the effective permissions when multiple\n * policies (e.g. deployment, organization, and share link) apply simultaneously.\n */\nexport function intersectPrivileges(...privilegeSets: (readonly AppPrivilege[])[]): AppPrivilege[] {\n if (privilegeSets.length === 0) return [];\n if (privilegeSets.length === 1) return [...new Set(privilegeSets[0])];\n let current = new Set<AppPrivilege>(privilegeSets[0]);\n for (let i = 1; i < privilegeSets.length; i++) {\n const nextSet = new Set<AppPrivilege>(privilegeSets[i]);\n current = new Set([...current].filter((p) => nextSet.has(p)));\n }\n return [...current];\n}\n\n/**\n * Evaluates whether an application capability set grants a specific privilege.\n */\nexport function hasAppPrivilege(\n capabilities: AppCapabilities | undefined,\n privilege: AppPrivilege,\n): boolean {\n if (!capabilities) return true;\n return capabilities.privileges.includes(privilege);\n}\n\n/**\n * Resolves why a privilege is withheld: its own reason if it has one, otherwise\n * the reason recorded for the capability set as a whole.\n *\n * @param capabilities - The active capability set, if any.\n * @param privilege - The privilege being explained.\n * @returns The reason, or undefined when none was recorded.\n */\nexport function appPrivilegeReason(\n capabilities: AppCapabilities | undefined,\n privilege: AppPrivilege,\n): string | undefined {\n if (!capabilities) return undefined;\n return capabilities.privilegeReasons?.[privilege] ?? capabilities.reason;\n}\n\n/**\n * Creates the default unconstrained application capabilities (Administrator role).\n */\nexport function createDefaultAppCapabilities(): AppCapabilities {\n return {\n role: \"administrator\",\n privileges: [...ALL_APP_PRIVILEGES],\n };\n}\n\n/**\n * Normalizes an untrusted array of privilege strings.\n */\nexport function normalizeAppPrivileges(raw: unknown): AppPrivilege[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const validSet = new Set<string>(ALL_APP_PRIVILEGES);\n const result: AppPrivilege[] = [];\n for (const item of raw) {\n if (typeof item === \"string\" && validSet.has(item)) {\n result.push(item as AppPrivilege);\n }\n }\n return result.length > 0 ? [...new Set(result)] : [];\n}\n","import type { GeoLibreLayer, LayerGroup } from \"./types\";\n\n/** Opacity a freshly created {@link LayerGroup} starts at (fully opaque). */\nexport const DEFAULT_LAYER_GROUP_OPACITY = 1;\n\n/**\n * One row in the layer panel: either a top-level (ungrouped) layer or a group\n * header together with its child layers.\n */\nexport type LayerTreeItem =\n | { kind: \"layer\"; layer: GeoLibreLayer }\n | { kind: \"group\"; group: LayerGroup; children: GeoLibreLayer[] };\n\n/**\n * One top-level block of the layer panel, in top-of-panel-first order: either a\n * single ungrouped layer or the contiguous run of layers a group owns.\n *\n * A group that owns no layer anywhere beneath it still gets a unit, with an\n * empty `layers` array, so that empty folders have a position in the panel and\n * can be reordered like any other row.\n */\nexport type LayerPanelUnit = {\n /** Group the block belongs to, or `null` for a lone ungrouped layer. */\n groupId: string | null;\n /** Member layers, top-first. Empty for a group with no layers under it. */\n layers: GeoLibreLayer[];\n};\n\n/**\n * The group a group is really nested under: its `parentId`, or `null` when it\n * is top-level or its `parentId` points at a group that no longer exists. Every\n * placement rule reads the parent through here so a dangling reference behaves\n * like a top-level folder rather than falling into a branch of the tree that is\n * not there.\n */\nfunction effectiveParentId(\n group: LayerGroup | undefined,\n groupById: ReadonlyMap<string, LayerGroup>,\n): string | null {\n const parentId = group?.parentId;\n return parentId && groupById.has(parentId) ? parentId : null;\n}\n\n/**\n * Panel index range `[first, last]` each group spans, keyed by group id. A\n * group with no unit of its own (an organizer whose layers all live in child\n * groups) spans the union of its descendants' units, so it is absent only when\n * nothing under it has a position yet.\n */\nfunction groupUnitRanges(\n units: LayerPanelUnit[],\n groupById: ReadonlyMap<string, LayerGroup>,\n): Map<string, [number, number]> {\n const ranges = new Map<string, [number, number]>();\n units.forEach((unit, index) => {\n let id: string | null = unit.groupId;\n const visited = new Set<string>();\n while (id && !visited.has(id)) {\n visited.add(id);\n const range = ranges.get(id);\n if (range) ranges.set(id, [Math.min(range[0], index), Math.max(range[1], index)]);\n else ranges.set(id, [index, index]);\n // A dangling `parentId` ends the walk rather than recording a range for a\n // group that does not exist.\n id = effectiveParentId(groupById.get(id), groupById);\n }\n });\n return ranges;\n}\n\n/**\n * Group indices ordered so that a group's parent always comes first, keeping\n * the `groups` order otherwise. `moveLayerGroupToGroup` reparents in place\n * without touching the array, so a child can sit ahead of its own parent; a\n * parent has to be placed first for the child to have a block to sit under.\n * A `parentId` cycle never resolves, so what is left is emitted in array order\n * rather than dropped.\n */\nfunction parentsFirstOrder(\n groups: LayerGroup[],\n groupById: ReadonlyMap<string, LayerGroup>,\n): number[] {\n const order: number[] = [];\n const placed = new Set<string>();\n let pending = groups.map((_, index) => index);\n while (pending.length > 0) {\n const deferred: number[] = [];\n for (const index of pending) {\n const parentId = groups[index].parentId;\n if (parentId && groupById.has(parentId) && !placed.has(parentId)) deferred.push(index);\n else {\n order.push(index);\n placed.add(groups[index].id);\n }\n }\n if (deferred.length === pending.length) {\n order.push(...deferred);\n break;\n }\n pending = deferred;\n }\n return order;\n}\n\n/**\n * Split the panel into its top-level blocks, **top-of-panel first** (the\n * reverse of the store's render order, where the last array element draws on\n * top).\n *\n * Layers give every populated group a position of its own. A group with no\n * layers under it has none to derive, so it is slotted in next to its\n * **siblings** — the groups sharing its parent — in `groups` order: directly\n * below the nearest sibling above it in that array, else directly above the\n * nearest one below it. Because the neighbours are read from `groups` rather\n * than from the layer array, a folder keeps its place relative to its siblings\n * when one of them gains its first layer (GeoLibre#1739).\n *\n * Only a sibling can place it. An ancestor's range already contains the slot\n * being chosen, and an unrelated group's range covers a different branch of the\n * tree, so anchoring on either drops the folder into someone else's block —\n * which is how a nested folder used to land outside its own parent once a\n * cousin gained a layer (GeoLibre#1739 follow-up). With no sibling positioned\n * yet, a nested folder falls to the end of its parent's block and a top-level\n * one to the top of the panel.\n *\n * An *organizer* — a group with no layers of its own but with some beneath it —\n * gets no block here: the panel draws its header against its top-most\n * descendant layer instead. Only a group whose whole subtree is empty needs one.\n *\n * A `groupId` that does not match any group is treated as ungrouped, so a\n * dangling reference degrades gracefully instead of dropping the layer.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns Panel blocks in top-to-bottom display order.\n */\nexport function buildLayerPanelUnits(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n): LayerPanelUnit[] {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const units: LayerPanelUnit[] = [];\n for (let i = layers.length - 1; i >= 0; i--) {\n const layer = layers[i];\n const groupId = layer.groupId && groupById.has(layer.groupId) ? layer.groupId : null;\n const last = units[units.length - 1];\n if (groupId && last && last.groupId === groupId) last.layers.push(layer);\n else units.push({ groupId, layers: [layer] });\n }\n\n let ranges = groupUnitRanges(units, groupById);\n // Every group a layer reaches — the ones that own a block and the organizers\n // spanning them — already has a position. `ranges` alone cannot stand in for\n // that set once the loop starts: splicing a folder in gives its ancestors an\n // inherited range too, and an ancestor whose subtree holds no layer still\n // needs a block of its own or it renders nowhere at all.\n const positioned = new Set(ranges.keys());\n if (positioned.size === groups.length) return units;\n for (const at of parentsFirstOrder(groups, groupById)) {\n const group = groups[at];\n if (positioned.has(group.id)) continue;\n const parentId = effectiveParentId(group, groupById);\n const isSibling = (other: LayerGroup) => effectiveParentId(other, groupById) === parentId;\n let insertAt: number | undefined;\n for (let i = at - 1; insertAt === undefined && i >= 0; i--) {\n if (!isSibling(groups[i])) continue;\n const range = ranges.get(groups[i].id);\n if (range) insertAt = range[1] + 1;\n }\n for (let i = at + 1; insertAt === undefined && i < groups.length; i++) {\n if (!isSibling(groups[i])) continue;\n const range = ranges.get(groups[i].id);\n if (range) insertAt = range[0];\n }\n // No sibling to sit beside: stay inside the parent's block rather than\n // drifting to the top of the panel past groups this one is nested in.\n if (insertAt === undefined && parentId) insertAt = (ranges.get(parentId)?.[1] ?? -1) + 1;\n units.splice(insertAt ?? 0, 0, { groupId: group.id, layers: [] });\n positioned.add(group.id);\n // Placing this folder shifts the units below it, and makes the folder\n // itself an anchor for the ones still to be placed.\n ranges = groupUnitRanges(units, groupById);\n }\n return units;\n}\n\n/**\n * Where the layer panel should draw every group header, expressed against the\n * layer rows the panel already renders.\n */\nexport type LayerPanelGroupHeaders = {\n /** Headers drawn immediately above the keyed layer's row, in panel order. */\n aboveLayer: Map<string, LayerGroup[]>;\n /** Headers drawn below every layer row, in panel order. */\n bottom: LayerGroup[];\n};\n\n/**\n * Resolve {@link buildLayerPanelUnits} into a draw position for each group\n * header, so a panel that renders row by row from the flat layer list can place\n * them without rebuilding its whole render as a tree.\n *\n * Every header comes from the one walk: the group a layer belongs to, the\n * *organizers* above it whose own layers all live in child groups, and the\n * folders holding no layer at all. Deriving them together is what keeps them in\n * order — each is emitted the first time the walk reaches its subtree, parents\n * ahead of children, so a nested folder can never be drawn above the parent it\n * sits inside (GeoLibre#1739 follow-up).\n *\n * A group is emitted once, at its top-most block, so a group whose members are\n * scattered still gets a single header.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns The anchor layer (or the panel bottom) each header sits above.\n */\nexport function layerPanelGroupHeaders(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n): LayerPanelGroupHeaders {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const units = buildLayerPanelUnits(layers, groups);\n const aboveLayer = new Map<string, LayerGroup[]>();\n const bottom: LayerGroup[] = [];\n const drawn = new Set<string>();\n for (let i = 0; i < units.length; i++) {\n const unit = units[i];\n if (!unit.groupId) continue;\n // Walk up to the root and reverse, so an ancestor's header precedes the\n // headers of everything nested in it. A `parentId` cycle stops the walk\n // instead of hanging.\n const chain: LayerGroup[] = [];\n let id: string | null = unit.groupId;\n const visited = new Set<string>();\n while (id && !visited.has(id) && !drawn.has(id)) {\n visited.add(id);\n const group = groupById.get(id);\n if (!group) break;\n chain.unshift(group);\n id = effectiveParentId(group, groupById);\n }\n if (chain.length === 0) continue;\n // The block's own first layer anchors it; a folder with none borrows the\n // first layer below it, and falls to the panel bottom when there is none.\n let anchorId = unit.layers[0]?.id;\n for (let j = i + 1; anchorId === undefined && j < units.length; j++) {\n anchorId = units[j].layers[0]?.id;\n }\n for (const group of chain) drawn.add(group.id);\n if (anchorId === undefined) {\n bottom.push(...chain);\n continue;\n }\n const at = aboveLayer.get(anchorId);\n if (at) at.push(...chain);\n else aboveLayer.set(anchorId, chain);\n }\n return { aboveLayer, bottom };\n}\n\n/**\n * Derive the layer-panel tree from the flat `layers` array and the group\n * definitions.\n *\n * Items are returned **top-of-panel first** (the reverse of the store's\n * render order, where the last array element draws on top). Within a group the\n * children are likewise ordered top-first. Each group is emitted once, at the\n * panel position of its top-most member; a group's members are normally\n * contiguous in the array, but if they are not, every member is still gathered\n * under a single header. Groups with no members (empty folders) sit where\n * {@link buildLayerPanelUnits} places them.\n *\n * A `groupId` that does not match any group is treated as ungrouped, so a\n * dangling reference degrades gracefully instead of dropping the layer.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns Panel rows in top-to-bottom display order.\n */\nexport function buildLayerTree(layers: GeoLibreLayer[], groups: LayerGroup[]): LayerTreeItem[] {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const items: LayerTreeItem[] = [];\n const emittedAt = new Map<string, LayerTreeItem & { kind: \"group\" }>();\n\n for (const unit of buildLayerPanelUnits(layers, groups)) {\n const group = unit.groupId ? groupById.get(unit.groupId) : undefined;\n if (!group) {\n for (const layer of unit.layers) items.push({ kind: \"layer\", layer });\n continue;\n }\n // A group's members are normally one contiguous run, but a scattered group\n // still gets a single header, at its top-most member.\n const existing = emittedAt.get(group.id);\n if (existing) {\n existing.children.push(...unit.layers);\n continue;\n }\n const item = { kind: \"group\" as const, group, children: [...unit.layers] };\n emittedAt.set(group.id, item);\n items.push(item);\n }\n return items;\n}\n\n/**\n * Fold each group's visibility and opacity into its child layers so the map\n * sync can keep treating every layer independently.\n *\n * A child's effective visibility is its own `visible` ANDed with the group's,\n * and its effective opacity is its own multiplied by the group's. Layers\n * without a group (or with a dangling `groupId`) are returned unchanged, and\n * the original object reference is preserved whenever nothing changes so\n * downstream memoization stays cheap.\n *\n * @param layers Flat layer list.\n * @param groups Group definitions.\n * @returns Layers with group effects applied.\n */\nexport function applyGroupEffects(layers: GeoLibreLayer[], groups: LayerGroup[]): GeoLibreLayer[] {\n if (groups.length === 0) return layers;\n const groupById = new Map(groups.map((g) => [g.id, g]));\n return layers.map((layer) => {\n if (!layer.groupId || !groupById.has(layer.groupId)) return layer;\n const { visible, opacity } = foldGroupChain(layer, groupById);\n if (visible === layer.visible && opacity === layer.opacity) return layer;\n return { ...layer, visible, opacity };\n });\n}\n\n/**\n * Walk a layer's group chain upward, ANDing each group's visibility into the\n * layer's own and multiplying each group's opacity into it. The `visited` set\n * makes a corrupted `parentId` cycle terminate instead of hanging.\n */\nfunction foldGroupChain(\n layer: GeoLibreLayer,\n groupById: ReadonlyMap<string, LayerGroup>,\n): { visible: boolean; opacity: number } {\n let visible = layer.visible;\n let opacity = layer.opacity;\n let group = layer.groupId ? groupById.get(layer.groupId) : undefined;\n const visited = new Set<string>();\n while (group && !visited.has(group.id)) {\n visited.add(group.id);\n visible = visible && group.visible;\n opacity *= group.opacity;\n group = group.parentId ? groupById.get(group.parentId) : undefined;\n }\n return { visible, opacity };\n}\n\n/**\n * The single layer form of {@link applyGroupEffects}: the visibility and\n * opacity a layer should actually render at once its (possibly nested) parent\n * groups are folded in.\n *\n * Group state never mutates a child's own `visible`/`opacity` fields, so any\n * renderer that cannot read the folded array — a plugin control that owns its\n * own paint and is driven layer by layer — asks for a single layer here\n * instead.\n *\n * @param layer The layer to fold.\n * @param groups Group definitions, or an already-built id → group map. Callers\n * in a render path that fold many layers against the same groups (the layer\n * panel) should pass a memoized map, since the array form rebuilds one per\n * call.\n * @returns The effective render state (the layer's own values when it is\n * ungrouped or its `groupId` is dangling).\n */\nexport function effectiveLayerRenderState(\n layer: GeoLibreLayer,\n groups: LayerGroup[] | ReadonlyMap<string, LayerGroup>,\n): { visible: boolean; opacity: number } {\n const size = Array.isArray(groups) ? groups.length : groups.size;\n if (size === 0 || !layer.groupId) {\n return { visible: layer.visible, opacity: layer.opacity };\n }\n return foldGroupChain(\n layer,\n Array.isArray(groups) ? new Map(groups.map((g) => [g.id, g])) : groups,\n );\n}\n\n/**\n * Indices, in store order, of every layer that belongs to `groupId`.\n *\n * @param layers Flat layer list.\n * @param groupId Group whose members to locate.\n * @returns Ascending list of member indices (empty when the group has none).\n */\nexport function groupMemberIndices(layers: GeoLibreLayer[], groupId: string): number[] {\n const indices: number[] = [];\n for (let i = 0; i < layers.length; i++) {\n if (layers[i]?.groupId === groupId) indices.push(i);\n }\n return indices;\n}\n\n/**\n * Reorder `layers` so that every group's members form a single contiguous block,\n * preserving the relative order of layers within each group and of the\n * top-level items. The block for a group is anchored at the position of its\n * current bottom-most (first) member — the first member encountered iterating\n * from index 0, which renders at the bottom of the layer panel.\n *\n * Mutating store actions call this after assigning `groupId`s to restore the\n * contiguity invariant the rest of the system relies on.\n *\n * @param layers Flat layer list, possibly with interleaved group members.\n * @returns A new array with grouped layers made contiguous.\n */\nexport function normalizeGroupContiguity(layers: GeoLibreLayer[]): GeoLibreLayer[] {\n const result: GeoLibreLayer[] = [];\n const placed = new Set<string>();\n for (let i = 0; i < layers.length; i++) {\n const layer = layers[i];\n if (placed.has(layer.id)) continue;\n if (!layer.groupId) {\n result.push(layer);\n placed.add(layer.id);\n continue;\n }\n // Pull in every remaining member of this group, in array order, so the\n // block is anchored at the first member encountered.\n for (let j = i; j < layers.length; j++) {\n const candidate = layers[j];\n if (candidate.groupId === layer.groupId && !placed.has(candidate.id)) {\n result.push(candidate);\n placed.add(candidate.id);\n }\n }\n }\n return result;\n}\n\n/**\n * Which of `parentId`'s own blocks a panel unit belongs to, used to move a group\n * only among the blocks it shares a parent with:\n *\n * - the id of the child of `parentId` the unit is nested in, however deep — the\n * sibling block a move steps over, or the moving group's own block;\n * - `parentId` itself when the unit holds that group's own layers: one more\n * block inside the parent, which a child steps over like any other rather\n * than a wall;\n * - `null` for an ungrouped layer at the root of the panel (`parentId` `null`);\n * - `undefined` when the unit sits outside `parentId` altogether — a wall,\n * since a nested group has no position outside the parent holding it.\n *\n * A `parentId` cycle reads as outside rather than looping.\n */\nfunction branchUnder(\n parentId: string | null,\n groupId: string | null,\n groupById: ReadonlyMap<string, LayerGroup>,\n): string | null | undefined {\n if (groupId === null) return parentId === null ? null : undefined;\n if (groupId === parentId) return groupId;\n let id = groupId;\n const visited = new Set([id]);\n for (;;) {\n const next = effectiveParentId(groupById.get(id), groupById);\n if (next === parentId) return id;\n if (next === null || visited.has(next)) return undefined;\n visited.add(next);\n id = next;\n }\n}\n\n/**\n * How deeply a group is nested: 0 for a top-level folder, 1 for a child of one,\n * and so on. Drives the layer panel's indentation and keeps a parent ahead of\n * its children when a reorder re-sorts the group array. A corrupted `parentId`\n * cycle stops the walk instead of hanging.\n *\n * @param group Group to measure.\n * @param groupById Group lookup, so a caller folding many groups against the\n * same set can pass a memoized map rather than rebuild one per call.\n * @returns The nesting depth.\n */\nexport function layerGroupDepth(\n group: LayerGroup,\n groupById: ReadonlyMap<string, LayerGroup>,\n): number {\n let depth = 0;\n let parentId = group.parentId;\n const visited = new Set([group.id]);\n while (parentId && !visited.has(parentId)) {\n visited.add(parentId);\n const parent = groupById.get(parentId);\n if (!parent) break;\n depth += 1;\n parentId = parent.parentId;\n }\n return depth;\n}\n\n/**\n * Move a group — with everything nested inside it — one block up or down among\n * its siblings, the groups sharing its parent.\n *\n * A nested group moves inside its parent only: it steps over a whole sibling\n * block rather than into it, and the parent's boundary is the end of its\n * travel, so `null` comes back rather than a move that would take it out of the\n * folder holding it. Inside that boundary it steps over its parent's *own*\n * layers as readily as over a sibling folder, since those rows are one more\n * block of the parent and ordering a child against them is a real reorder. A\n * folder holding no layer is the exception: it has no way to record a position\n * between two layer rows, so a move that would only cross them changes nothing\n * and reports `null` — the same limit empty folders already have at the top\n * level. A top-level group moves among the other top-level groups and the\n * ungrouped layers.\n *\n * Both arrays carry part of the panel order, so both come back: `layers` holds\n * the order of every populated block, and `groups` holds the order the empty\n * folders are placed against (see {@link buildLayerPanelUnits}). Swapping two\n * folders that own no layers moves nothing in `layers`, and moving a populated\n * group past an empty one moves nothing but the group order, so a caller must\n * apply both.\n *\n * `normalizeGroupContiguity` keeps each *group's* own members together, but not\n * a whole *subtree*: nesting only rewrites `parentId`, so an unrelated block can\n * sit between two sibling child groups. The subtree still travels as one block —\n * \"with everything nested inside it\" — which means such an in-between block is\n * left behind on the other side of it, and the move can shift more than one row.\n * Compacting the subtree is the intended reading of the operation; the in-between\n * block landing on the far side is the accepted cost.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @param id Group to move.\n * @param direction `\"up\"` toward the top of the panel, `\"down\"` toward the\n * bottom.\n * @returns The new arrays, or `null` when the group is already at that end of\n * the panel and nothing would change.\n */\nexport function reorderLayerGroupInPanel(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n direction: \"up\" | \"down\",\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n return moveGroupThroughUnits(buildLayerPanelUnits(layers, groups), layers, groups, id, direction);\n}\n\n/**\n * The body of {@link reorderLayerGroupInPanel}, against panel blocks the caller\n * already has, so asking about every group in turn splits the panel once rather\n * than once per question.\n */\nfunction moveGroupThroughUnits(\n units: LayerPanelUnit[],\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n direction: \"up\" | \"down\",\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n const group = groupById.get(id);\n if (!group) return null;\n // Label every unit with the block it occupies among the moving group's own\n // siblings, so the move steps over a whole neighbouring block and stops at the\n // edge of the parent instead of stepping one unit at a time into it.\n const parentId = effectiveParentId(group, groupById);\n const branches = units.map((unit) => branchUnder(parentId, unit.groupId, groupById));\n const matching = new Set<number>();\n branches.forEach((branch, index) => {\n if (branch === id) matching.add(index);\n });\n if (matching.size === 0) return null;\n const indices = [...matching].sort((a, b) => a - b);\n // Units run top-first, so \"up\" steps over the block above the group's first.\n const neighbor = direction === \"up\" ? indices[0] - 1 : indices[indices.length - 1] + 1;\n if (neighbor < 0 || neighbor >= units.length) return null;\n const neighborBranch = branches[neighbor];\n // Outside the parent: the group is already at that end of its own block.\n if (neighborBranch === undefined) return null;\n // A neighbouring group travels whole, so the move clears everything nested in\n // it; an ungrouped layer is a block of one.\n const neighborUnits =\n neighborBranch === null\n ? [units[neighbor]]\n : units.filter((_, index) => branches[index] === neighborBranch);\n // The moving group's own indices need not be contiguous — sibling child groups\n // can straddle an unrelated block. Gathering them compacts the subtree, which\n // is the point of moving it as a whole; see `reorderLayerGroupInPanel`.\n const block = units.filter((_, index) => matching.has(index));\n const remaining = units.filter((_, index) => !matching.has(index));\n const edge =\n direction === \"up\"\n ? remaining.indexOf(neighborUnits[0])\n : remaining.indexOf(neighborUnits[neighborUnits.length - 1]) + 1;\n const reordered = [...remaining];\n reordered.splice(edge, 0, ...block);\n\n return arraysFromUnits(reordered, layers, groups, groupById);\n}\n\n/**\n * Turn reordered panel blocks back into the two store arrays, or `null` when\n * neither array changed. Shared by every panel reorder so they all agree on how\n * the empty folders follow a move.\n */\nfunction arraysFromUnits(\n reordered: LayerPanelUnit[],\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n groupById: ReadonlyMap<string, LayerGroup>,\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n // Units are top-first and so are the layers inside them; the store keeps the\n // reverse, with the last element drawing on top.\n const nextLayers = reordered.flatMap((unit) => unit.layers).reverse();\n // Re-derive the group order from the new panel so the empty folders, which\n // are placed against it, follow the move. Ties (an organizer and the child\n // block it spans) keep parents first.\n const ranges = groupUnitRanges(reordered, groupById);\n const nextGroups = [...groups].sort((a, b) => {\n const byPosition = (ranges.get(a.id)?.[0] ?? 0) - (ranges.get(b.id)?.[0] ?? 0);\n return byPosition !== 0\n ? byPosition\n : layerGroupDepth(a, groupById) - layerGroupDepth(b, groupById);\n });\n const layersMoved = nextLayers.some((layer, index) => layer.id !== layers[index]?.id);\n const groupsMoved = nextGroups.some((group, index) => group.id !== groups[index]?.id);\n if (!layersMoved && !groupsMoved) return null;\n return { layers: nextLayers, groups: nextGroups };\n}\n\n/** Direction {@link sortLayerGroupInPanel} orders names in, top of panel first. */\nexport type LayerGroupSortOrder = \"asc\" | \"desc\";\n\n/**\n * A numeric-aware, case/accent-insensitive collator, so \"Parcel 2\" sorts before\n * \"Parcel 10\" and \"owner\" sits with \"Owner\", the way a file browser orders.\n * Core has no i18n of its own, so the panel passes the app's display language;\n * without one (or with a tag `Intl` rejects) the runtime default applies.\n */\nfunction nameCollator(locale: string | undefined): Intl.Collator {\n const options: Intl.CollatorOptions = { numeric: true, sensitivity: \"base\" };\n try {\n return new Intl.Collator(locale, options);\n } catch {\n return new Intl.Collator(undefined, options);\n }\n}\n\n/**\n * Sort what sits directly inside a group by name, A to Z (`\"asc\"`) or Z to A\n * (`\"desc\"`) from the top of the panel down (GeoLibre#2599).\n *\n * The group's own layers are sorted among themselves, and its child groups are\n * sorted among themselves by group name, each carrying everything nested inside\n * it. The two are not interleaved: a group's own layers are one contiguous\n * block (see {@link normalizeGroupContiguity}), so that block keeps its slot\n * among the child groups rather than being split between them. Only direct\n * children move; the contents of a child group keep their order.\n *\n * Like {@link reorderLayerGroupInPanel}, both arrays come back, so empty child\n * folders follow the sort too.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @param id Group whose children to sort.\n * @param order `\"asc\"` for A to Z, `\"desc\"` for Z to A.\n * @param locale BCP 47 tag whose collation rules to compare names by, normally\n * the app's display language. Defaults to the runtime locale.\n * @returns The new arrays, or `null` when the children are already in that\n * order and nothing would change.\n */\nexport function sortLayerGroupInPanel(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n order: LayerGroupSortOrder,\n locale?: string,\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n const units = buildLayerPanelUnits(layers, groups);\n return sortGroupThroughUnits(units, layers, groups, id, order, nameCollator(locale));\n}\n\n/**\n * The body of {@link sortLayerGroupInPanel}, against panel blocks the caller\n * already has.\n */\nfunction sortGroupThroughUnits(\n units: LayerPanelUnit[],\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n id: string,\n order: LayerGroupSortOrder,\n collator: Intl.Collator,\n): { layers: GeoLibreLayer[]; groups: LayerGroup[] } | null {\n const groupById = new Map(groups.map((g) => [g.id, g]));\n if (!groupById.has(id)) return null;\n const sign = order === \"asc\" ? 1 : -1;\n const compare = (a: string, b: string) => sign * collator.compare(a, b);\n // Label each unit with the direct child of `id` it sits in (`id` itself for\n // the group's own layers), or `undefined` when it lies outside the group.\n const branches = units.map((unit) => branchUnder(id, unit.groupId, groupById));\n const inside = branches.flatMap((branch, index) => (branch ? [index] : []));\n if (inside.length === 0) return null;\n\n // Blocks in their current top-first order: the group's own layers as one\n // block, each child group as one block carrying its whole subtree.\n const blockUnits = new Map<string, LayerPanelUnit[]>();\n for (const index of inside) {\n const branch = branches[index] as string;\n const list = blockUnits.get(branch);\n if (list) list.push(units[index]);\n else blockUnits.set(branch, [units[index]]);\n }\n const blockIds = [...blockUnits.keys()];\n const childIds = blockIds.filter((branch) => branch !== id);\n const sortedChildIds = [...childIds].sort((a, b) =>\n compare(groupById.get(a)?.name ?? \"\", groupById.get(b)?.name ?? \"\"),\n );\n let nextChild = 0;\n const sortedBlocks = blockIds.map((branch): LayerPanelUnit[] => {\n if (branch !== id) return blockUnits.get(sortedChildIds[nextChild++]) ?? [];\n const own = (blockUnits.get(id) ?? []).flatMap((unit) => unit.layers);\n return [{ groupId: id, layers: [...own].sort((a, b) => compare(a.name, b.name)) }];\n });\n\n // The group's units need not be contiguous (see `reorderLayerGroupInPanel`);\n // gathering them at the first one compacts the subtree, as a move does.\n const insideSet = new Set(inside);\n const reordered = units.filter((_, index) => !insideSet.has(index));\n reordered.splice(inside[0], 0, ...sortedBlocks.flat());\n // Judge \"already sorted\" by what the panel shows, not by `groups` order:\n // a reparent can leave that array out of panel order, and re-deriving it\n // alone would enable a sort that changes nothing visible. This also skips\n // the whole-array rebuild for the common no-op asked by the sort menu.\n const unchanged =\n reordered.length === units.length &&\n reordered.every(\n (unit, index) =>\n unit.groupId === units[index].groupId &&\n unit.layers.length === units[index].layers.length &&\n unit.layers.every((layer, at) => layer.id === units[index].layers[at].id),\n );\n if (unchanged) return null;\n return arraysFromUnits(reordered, layers, groups, groupById);\n}\n\n/**\n * Which directions {@link reorderLayerGroupInPanel} can actually move each\n * group, so the layer panel can disable a menu item that would do nothing.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @returns Per-group flags, keyed by group id.\n */\nexport function layerGroupMoveability(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n): Map<string, { up: boolean; down: boolean }> {\n const units = buildLayerPanelUnits(layers, groups);\n const result = new Map<string, { up: boolean; down: boolean }>();\n for (const group of groups) {\n result.set(group.id, {\n up: moveGroupThroughUnits(units, layers, groups, group.id, \"up\") !== null,\n down: moveGroupThroughUnits(units, layers, groups, group.id, \"down\") !== null,\n });\n }\n return result;\n}\n\n/**\n * Which orders {@link sortLayerGroupInPanel} would actually change for each\n * group, so the layer panel can disable a sort that would do nothing.\n *\n * @param layers Flat layer list in store (render) order.\n * @param groups Group definitions.\n * @param locale Collation locale, as for {@link sortLayerGroupInPanel}.\n * @returns Per-group flags, keyed by group id.\n */\nexport function layerGroupSortability(\n layers: GeoLibreLayer[],\n groups: LayerGroup[],\n locale?: string,\n): Map<string, Record<LayerGroupSortOrder, boolean>> {\n const units = buildLayerPanelUnits(layers, groups);\n const collator = nameCollator(locale);\n const result = new Map<string, Record<LayerGroupSortOrder, boolean>>();\n for (const group of groups) {\n result.set(group.id, {\n asc: sortGroupThroughUnits(units, layers, groups, group.id, \"asc\", collator) !== null,\n desc: sortGroupThroughUnits(units, layers, groups, group.id, \"desc\", collator) !== null,\n });\n }\n return result;\n}\n","import type { DashboardWidget, LegendConfig, ProjectComment } from \"./types\";\n\n/**\n * Normalize the removed-layer-ids argument into a Set for uniform lookup.\n */\nexport function removedLayerIdSet(layerIds: string | Iterable<string>): Set<string> {\n if (typeof layerIds === \"string\") return new Set([layerIds]);\n if (layerIds instanceof Set) return layerIds as Set<string>;\n return new Set(layerIds);\n}\n\n/**\n * Filter out dashboard widgets whose `layerId` references a removed layer.\n * Returns the same array reference when nothing changes.\n */\nexport function scrubWidgetsForRemovedLayers(\n widgets: DashboardWidget[],\n layerIds: string | Iterable<string>,\n): DashboardWidget[] {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0 || widgets.length === 0) return widgets;\n const filtered = widgets.filter((w) => !removed.has(w.layerId));\n return filtered.length === widgets.length ? widgets : filtered;\n}\n\n/**\n * Drop comments whose anchor is a feature on a removed layer.\n * Point-anchored comments are always kept.\n * Returns the same array reference when nothing changes.\n */\nexport function scrubCommentsForRemovedLayers(\n comments: ProjectComment[],\n layerIds: string | Iterable<string>,\n): ProjectComment[] {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0 || comments.length === 0) return comments;\n const filtered = comments.filter(\n (c) => !(c.anchor.type === \"feature\" && removed.has(c.anchor.layerId)),\n );\n return filtered.length === comments.length ? comments : filtered;\n}\n\n/**\n * Scrub legend config of references to removed layers:\n * - `order`: filter out removed layer ids\n * - `overrides`: drop keys that equal a removed id OR start with `${id}::`\n * - `customEntries`: drop keys that equal a removed layer id but KEEP keys\n * starting with `custom:`\n *\n * Returns the same reference when nothing changes.\n */\nexport function scrubLegendForRemovedLayers(\n legend: LegendConfig,\n layerIds: string | Iterable<string>,\n): LegendConfig {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return legend;\n\n let changed = false;\n\n // --- order ---\n const order = legend.order.filter((id) => !removed.has(id));\n if (order.length !== legend.order.length) changed = true;\n\n // --- overrides ---\n const overrides: Record<string, (typeof legend.overrides)[string]> = {};\n for (const [key, value] of Object.entries(legend.overrides)) {\n const base = key.includes(\"::\") ? key.slice(0, key.indexOf(\"::\")) : key;\n if (removed.has(base)) {\n changed = true;\n } else {\n overrides[key] = value;\n }\n }\n\n // --- customEntries ---\n let customEntries = legend.customEntries;\n if (customEntries) {\n let customEntriesChanged = false;\n const nextCustom: Record<string, (typeof customEntries)[string]> = {};\n for (const [key, value] of Object.entries(customEntries)) {\n if (key.startsWith(\"custom:\")) {\n nextCustom[key] = value;\n } else if (removed.has(key)) {\n customEntriesChanged = true;\n } else {\n nextCustom[key] = value;\n }\n }\n if (customEntriesChanged) {\n customEntries = Object.keys(nextCustom).length > 0 ? nextCustom : undefined;\n changed = true;\n }\n }\n\n if (!changed) return legend;\n return { ...legend, order, overrides, customEntries };\n}\n","/**\n * Print Layout composer settings that belong to a project.\n *\n * The Print Layout dialog composes a printable page out of the current map:\n * a title block, page size and orientation, the cartographic furniture\n * (legend, scale bar, north arrow), optional data blocks, and an atlas /\n * map-series definition. All of that describes the *project's* map document,\n * so it round-trips through `.geolibre.json` (GeoLibre discussion #1992 —\n * before this, reopening a project lost the title, page format and\n * orientation, and the dialog kept showing the previous project's settings).\n *\n * Deliberately **not** in here: everything that is per-session rather than\n * part of the document — the captured map image, export progress, error and\n * clipboard notices, the current atlas page, and the dialog's own chrome\n * (panel widths, dialog size).\n *\n * The unions below are the storage contract. Their app-side counterparts\n * (`PaperSizeId`, `Orientation`, `BodyCorner`, ... in\n * `apps/geolibre-desktop/src/lib/`) are checked against these by ordinary\n * assignment in the dialog's snapshot/restore paths, so adding a paper size or\n * a corner on one side without the other fails the build.\n */\n\nimport { removedLayerIdSet } from \"./layer-ref-scrub\";\n\n/** A page corner an overlay block can be pinned to. */\nexport type PrintLayoutCorner = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\";\n\n/** Which features a data block covers when the atlas paginates the map. */\nexport type PrintLayoutPageFilter = \"all\" | \"contained\" | \"intersecting\";\n\n/** One hand-authored swatch in the composer's user-defined legend. */\nexport interface PrintLayoutLegendEntry {\n id: string;\n label: string;\n color: string;\n}\n\nexport interface PrintLayoutConfig {\n // Title block\n title: string;\n subtitle: string;\n titlePlacement: \"outside\" | \"inside\";\n titleAlign: \"left\" | \"center\" | \"right\";\n\n // Page\n paperSize:\n | \"a4\"\n | \"a3\"\n | \"letter\"\n | \"legal\"\n | \"tabloid\"\n | \"fullhd\"\n | \"hd\"\n | \"uhd4k\"\n | \"square\"\n | \"custom\";\n orientation: \"portrait\" | \"landscape\";\n customWidth: number;\n customHeight: number;\n customUnit: \"mm\" | \"px\";\n pageMargin: \"normal\" | \"narrow\" | \"none\";\n showPageBorder: boolean;\n pageBorderColor: string;\n pageBorderWidth: number;\n\n // Map frame\n mapBorderColor: string;\n mapBorderWidth: number;\n mapBackground: string;\n\n // Map elements\n showTitle: boolean;\n showSubtitle: boolean;\n showLegend: boolean;\n showScaleBar: boolean;\n showNorthArrow: boolean;\n navigationGrouped: boolean;\n showFooter: boolean;\n footerText: string;\n showDate: boolean;\n /** Blank means \"today\", resolved when the page is drawn. */\n dateText: string;\n showAttribution: boolean;\n\n // Colorbar\n showColorbar: boolean;\n colorbarRamp: string;\n /** Kept as typed text so a half-entered value survives a round trip. */\n colorbarMin: string;\n colorbarMax: string;\n colorbarLabel: string;\n colorbarOrientation: \"vertical\" | \"horizontal\";\n colorbarLength: number;\n colorbarPosition: PrintLayoutCorner;\n\n // User-defined legend\n showCustomLegend: boolean;\n customLegendTitle: string;\n customLegendEntries: PrintLayoutLegendEntry[];\n customLegendPosition: PrintLayoutCorner;\n\n // Attribute-table block\n showDataTable: boolean;\n tableLayerId: string;\n tableTitle: string;\n tableColumns: string[];\n tableSortField: string;\n tableSortDesc: boolean;\n tableMaxRows: number;\n tableFitRows: boolean;\n tablePosition: PrintLayoutCorner;\n tablePageFilter: PrintLayoutPageFilter;\n tableFilterToAtlasFeature: boolean;\n\n // Chart block\n showDataChart: boolean;\n chartLayerId: string;\n chartTitle: string;\n chartType: \"bar\" | \"pie\" | \"line\";\n chartCategoryField: string;\n chartAggregation: \"count\" | \"sum\" | \"mean\";\n chartValueField: string;\n chartPosition: PrintLayoutCorner;\n chartPageFilter: PrintLayoutPageFilter;\n\n // Info block (\"title block\" / cartouche)\n showInfoBlock: boolean;\n author: string;\n projectNumber: string;\n crs: string;\n revision: string;\n\n // What the page captures\n captureMode: \"viewport\" | \"extent\";\n /** Drawn print extent as `[west, south, east, north]`; null when unset. */\n extentBbox: [number, number, number, number] | null;\n\n // Atlas / map series\n atlasEnabled: boolean;\n atlasLayerId: string;\n atlasCoverage: \"features\" | \"line\";\n atlasSegmentKm: string;\n atlasNameField: string;\n atlasExtentMode: \"margin\" | \"scale\";\n atlasMarginPct: number;\n atlasMaskEnabled: boolean;\n atlasScale: string;\n atlasSortField: string;\n atlasSortDescending: boolean;\n atlasFilter: string;\n atlasFilenamePattern: string;\n}\n\nconst CORNERS = [\"top-left\", \"top-right\", \"bottom-left\", \"bottom-right\"] as const;\nconst PAGE_FILTERS = [\"all\", \"contained\", \"intersecting\"] as const;\n\n/**\n * Settings a project starts with, and the shape every partial or hand-edited\n * `printLayout` is filled out against. Also the dialog's initial state, so\n * \"new project\" and \"a project saved before this feature\" compose identically.\n */\nexport const DEFAULT_PRINT_LAYOUT: PrintLayoutConfig = {\n title: \"\",\n subtitle: \"\",\n titlePlacement: \"outside\",\n titleAlign: \"center\",\n\n paperSize: \"a4\",\n orientation: \"landscape\",\n customWidth: 1280,\n customHeight: 720,\n customUnit: \"px\",\n pageMargin: \"normal\",\n showPageBorder: false,\n pageBorderColor: \"#111827\",\n pageBorderWidth: 2,\n\n mapBorderColor: \"#9ca3af\",\n mapBorderWidth: 1,\n mapBackground: \"#e5e7eb\",\n\n showTitle: true,\n showSubtitle: true,\n showLegend: true,\n showScaleBar: true,\n showNorthArrow: true,\n navigationGrouped: true,\n showFooter: false,\n footerText: \"\",\n showDate: true,\n dateText: \"\",\n showAttribution: true,\n\n showColorbar: false,\n colorbarRamp: \"viridis\",\n colorbarMin: \"0\",\n colorbarMax: \"100\",\n colorbarLabel: \"\",\n colorbarOrientation: \"vertical\",\n colorbarLength: 34,\n colorbarPosition: \"top-right\",\n\n showCustomLegend: false,\n customLegendTitle: \"Legend\",\n customLegendEntries: [\n { id: \"cl-1\", label: \"Class 1\", color: \"#2563eb\" },\n { id: \"cl-2\", label: \"Class 2\", color: \"#16a34a\" },\n ],\n customLegendPosition: \"top-left\",\n\n showDataTable: false,\n tableLayerId: \"\",\n tableTitle: \"\",\n tableColumns: [],\n tableSortField: \"\",\n tableSortDesc: false,\n tableMaxRows: 10,\n tableFitRows: false,\n tablePosition: \"bottom-left\",\n tablePageFilter: \"contained\",\n tableFilterToAtlasFeature: false,\n\n showDataChart: false,\n chartLayerId: \"\",\n chartTitle: \"\",\n chartType: \"bar\",\n chartCategoryField: \"\",\n chartAggregation: \"count\",\n chartValueField: \"\",\n chartPosition: \"top-right\",\n chartPageFilter: \"contained\",\n\n showInfoBlock: false,\n author: \"\",\n projectNumber: \"\",\n crs: \"\",\n revision: \"\",\n\n captureMode: \"viewport\",\n extentBbox: null,\n\n atlasEnabled: false,\n atlasLayerId: \"\",\n atlasCoverage: \"features\",\n atlasSegmentKm: \"20\",\n atlasNameField: \"\",\n atlasExtentMode: \"margin\",\n atlasMarginPct: 10,\n atlasMaskEnabled: false,\n atlasScale: \"50000\",\n atlasSortField: \"\",\n atlasSortDescending: false,\n atlasFilter: \"\",\n atlasFilenamePattern: \"{atlas.pagenumber}-{atlas.name}\",\n};\n\n/** A fresh copy of the defaults, safe to hand to a store or a component. */\nexport function createDefaultPrintLayout(): PrintLayoutConfig {\n return {\n ...DEFAULT_PRINT_LAYOUT,\n tableColumns: [],\n customLegendEntries: DEFAULT_PRINT_LAYOUT.customLegendEntries.map((entry) => ({ ...entry })),\n };\n}\n\nfunction str(value: unknown, fallback: string): string {\n return typeof value === \"string\" ? value : fallback;\n}\n\nfunction bool(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nfunction num(value: unknown, fallback: number, min: number, max: number): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) return fallback;\n return Math.min(max, Math.max(min, value));\n}\n\nfunction oneOf<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {\n return typeof value === \"string\" && (allowed as readonly string[]).includes(value)\n ? (value as T)\n : fallback;\n}\n\nfunction stringList(value: unknown, fallback: string[]): string[] {\n if (!Array.isArray(value)) return fallback;\n return value.filter((item): item is string => typeof item === \"string\");\n}\n\nfunction legendEntries(\n value: unknown,\n fallback: PrintLayoutLegendEntry[],\n): PrintLayoutLegendEntry[] {\n if (!Array.isArray(value)) return fallback.map((entry) => ({ ...entry }));\n const entries: PrintLayoutLegendEntry[] = [];\n // The editor keys its swatch rows by id, so two entries sharing one would\n // make an edit to either apply to both. A hand-edited file can omit an id or\n // repeat one, so ids are made unique here rather than trusted.\n const used = new Set<string>();\n for (const [index, item] of value.entries()) {\n if (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n const entry = item as Partial<PrintLayoutLegendEntry>;\n const given = typeof entry.id === \"string\" ? entry.id.trim() : \"\";\n let id = given;\n if (!id || used.has(id)) {\n // Synthesize past whatever is already taken, including ids claimed later\n // in the array, so the fallback cannot collide with an explicit one.\n let candidate = index + 1;\n while (used.has(`cl-${candidate}`) || claimsId(value, index, `cl-${candidate}`)) {\n candidate += 1;\n }\n id = `cl-${candidate}`;\n }\n used.add(id);\n entries.push({\n id,\n label: str(entry.label, \"\"),\n color: str(entry.color, \"#2563eb\"),\n });\n }\n return entries;\n}\n\n/** Whether any entry after `index` explicitly carries `id`. */\nfunction claimsId(value: unknown[], index: number, id: string): boolean {\n for (let i = index + 1; i < value.length; i += 1) {\n const item = value[i];\n if (!item || typeof item !== \"object\" || Array.isArray(item)) continue;\n const candidate = (item as Partial<PrintLayoutLegendEntry>).id;\n if (typeof candidate === \"string\" && candidate.trim() === id) return true;\n }\n return false;\n}\n\nfunction extent(value: unknown): [number, number, number, number] | null {\n if (!Array.isArray(value) || value.length !== 4) return null;\n if (!value.every((n) => typeof n === \"number\" && Number.isFinite(n))) return null;\n const [west, south, east, north] = value as number[];\n // The draw tool orders its corners, so an inverted or zero-area box only\n // reaches here from a hand-edited file; capturing it would produce an empty\n // image, so treat it as unset (mirrors `normalizeBounds` in project.ts).\n if (west >= east || south >= north) return null;\n return [west, south, east, north];\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited, possibly partial) `printLayout`\n * into a complete {@link PrintLayoutConfig}, filling every missing or\n * malformed field from {@link DEFAULT_PRINT_LAYOUT}.\n *\n * @param value - The raw `printLayout` value read from a project file.\n * @returns The normalized config, or null when the project carries none, so\n * callers can tell \"absent\" from \"explicitly default\".\n */\nexport function normalizePrintLayoutConfig(value: unknown): PrintLayoutConfig | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const raw = value as Record<string, unknown>;\n const d = DEFAULT_PRINT_LAYOUT;\n return {\n title: str(raw.title, d.title),\n subtitle: str(raw.subtitle, d.subtitle),\n titlePlacement: oneOf(raw.titlePlacement, [\"outside\", \"inside\"] as const, d.titlePlacement),\n titleAlign: oneOf(raw.titleAlign, [\"left\", \"center\", \"right\"] as const, d.titleAlign),\n\n paperSize: oneOf(\n raw.paperSize,\n [\"a4\", \"a3\", \"letter\", \"legal\", \"tabloid\", \"fullhd\", \"hd\", \"uhd4k\", \"square\", \"custom\"],\n d.paperSize,\n ),\n orientation: oneOf(raw.orientation, [\"portrait\", \"landscape\"] as const, d.orientation),\n customWidth: num(raw.customWidth, d.customWidth, 1, 100000),\n customHeight: num(raw.customHeight, d.customHeight, 1, 100000),\n customUnit: oneOf(raw.customUnit, [\"mm\", \"px\"] as const, d.customUnit),\n pageMargin: oneOf(raw.pageMargin, [\"normal\", \"narrow\", \"none\"] as const, d.pageMargin),\n showPageBorder: bool(raw.showPageBorder, d.showPageBorder),\n pageBorderColor: str(raw.pageBorderColor, d.pageBorderColor),\n // A page border is drawn only when `showPageBorder` is on, so a zero width\n // would be an invisible \"visible\" border; the editor's own floor is 1.\n pageBorderWidth: num(raw.pageBorderWidth, d.pageBorderWidth, 1, 20),\n\n mapBorderColor: str(raw.mapBorderColor, d.mapBorderColor),\n mapBorderWidth: num(raw.mapBorderWidth, d.mapBorderWidth, 0, 10),\n mapBackground: str(raw.mapBackground, d.mapBackground),\n\n showTitle: bool(raw.showTitle, d.showTitle),\n showSubtitle: bool(raw.showSubtitle, d.showSubtitle),\n showLegend: bool(raw.showLegend, d.showLegend),\n showScaleBar: bool(raw.showScaleBar, d.showScaleBar),\n showNorthArrow: bool(raw.showNorthArrow, d.showNorthArrow),\n navigationGrouped: bool(raw.navigationGrouped, d.navigationGrouped),\n showFooter: bool(raw.showFooter, d.showFooter),\n footerText: str(raw.footerText, d.footerText),\n showDate: bool(raw.showDate, d.showDate),\n dateText: str(raw.dateText, d.dateText),\n showAttribution: bool(raw.showAttribution, d.showAttribution),\n\n showColorbar: bool(raw.showColorbar, d.showColorbar),\n colorbarRamp: str(raw.colorbarRamp, d.colorbarRamp),\n colorbarMin: str(raw.colorbarMin, d.colorbarMin),\n colorbarMax: str(raw.colorbarMax, d.colorbarMax),\n colorbarLabel: str(raw.colorbarLabel, d.colorbarLabel),\n colorbarOrientation: oneOf(\n raw.colorbarOrientation,\n [\"vertical\", \"horizontal\"] as const,\n d.colorbarOrientation,\n ),\n colorbarLength: num(raw.colorbarLength, d.colorbarLength, 1, 100),\n colorbarPosition: oneOf(raw.colorbarPosition, CORNERS, d.colorbarPosition),\n\n showCustomLegend: bool(raw.showCustomLegend, d.showCustomLegend),\n customLegendTitle: str(raw.customLegendTitle, d.customLegendTitle),\n customLegendEntries: legendEntries(raw.customLegendEntries, d.customLegendEntries),\n customLegendPosition: oneOf(raw.customLegendPosition, CORNERS, d.customLegendPosition),\n\n showDataTable: bool(raw.showDataTable, d.showDataTable),\n tableLayerId: str(raw.tableLayerId, d.tableLayerId),\n tableTitle: str(raw.tableTitle, d.tableTitle),\n tableColumns: stringList(raw.tableColumns, d.tableColumns),\n tableSortField: str(raw.tableSortField, d.tableSortField),\n tableSortDesc: bool(raw.tableSortDesc, d.tableSortDesc),\n tableMaxRows: num(raw.tableMaxRows, d.tableMaxRows, 1, 10000),\n tableFitRows: bool(raw.tableFitRows, d.tableFitRows),\n tablePosition: oneOf(raw.tablePosition, CORNERS, d.tablePosition),\n tablePageFilter: oneOf(raw.tablePageFilter, PAGE_FILTERS, d.tablePageFilter),\n tableFilterToAtlasFeature: bool(raw.tableFilterToAtlasFeature, d.tableFilterToAtlasFeature),\n\n showDataChart: bool(raw.showDataChart, d.showDataChart),\n chartLayerId: str(raw.chartLayerId, d.chartLayerId),\n chartTitle: str(raw.chartTitle, d.chartTitle),\n chartType: oneOf(raw.chartType, [\"bar\", \"pie\", \"line\"] as const, d.chartType),\n chartCategoryField: str(raw.chartCategoryField, d.chartCategoryField),\n chartAggregation: oneOf(\n raw.chartAggregation,\n [\"count\", \"sum\", \"mean\"] as const,\n d.chartAggregation,\n ),\n chartValueField: str(raw.chartValueField, d.chartValueField),\n chartPosition: oneOf(raw.chartPosition, CORNERS, d.chartPosition),\n chartPageFilter: oneOf(raw.chartPageFilter, PAGE_FILTERS, d.chartPageFilter),\n\n showInfoBlock: bool(raw.showInfoBlock, d.showInfoBlock),\n author: str(raw.author, d.author),\n projectNumber: str(raw.projectNumber, d.projectNumber),\n crs: str(raw.crs, d.crs),\n revision: str(raw.revision, d.revision),\n\n captureMode: oneOf(raw.captureMode, [\"viewport\", \"extent\"] as const, d.captureMode),\n extentBbox: extent(raw.extentBbox),\n\n atlasEnabled: bool(raw.atlasEnabled, d.atlasEnabled),\n atlasLayerId: str(raw.atlasLayerId, d.atlasLayerId),\n atlasCoverage: oneOf(raw.atlasCoverage, [\"features\", \"line\"] as const, d.atlasCoverage),\n atlasSegmentKm: str(raw.atlasSegmentKm, d.atlasSegmentKm),\n atlasNameField: str(raw.atlasNameField, d.atlasNameField),\n atlasExtentMode: oneOf(raw.atlasExtentMode, [\"margin\", \"scale\"] as const, d.atlasExtentMode),\n atlasMarginPct: num(raw.atlasMarginPct, d.atlasMarginPct, 0, 100),\n atlasMaskEnabled: bool(raw.atlasMaskEnabled, d.atlasMaskEnabled),\n atlasScale: str(raw.atlasScale, d.atlasScale),\n atlasSortField: str(raw.atlasSortField, d.atlasSortField),\n atlasSortDescending: bool(raw.atlasSortDescending, d.atlasSortDescending),\n atlasFilter: str(raw.atlasFilter, d.atlasFilter),\n atlasFilenamePattern: str(raw.atlasFilenamePattern, d.atlasFilenamePattern),\n };\n}\n\n/**\n * Whether a config is still the untouched default, so a project that never\n * opened the composer serializes without a `printLayout` key and stays\n * byte-identical to one saved before this feature.\n */\nexport function isDefaultPrintLayout(config: PrintLayoutConfig): boolean {\n return printLayoutConfigsEqual(config, DEFAULT_PRINT_LAYOUT);\n}\n\n/**\n * Structural equality for two configs. Used to skip no-op store writes, so\n * merely opening the Print Layout dialog never marks a project dirty.\n */\nexport function printLayoutConfigsEqual(a: PrintLayoutConfig, b: PrintLayoutConfig): boolean {\n if (a === b) return true;\n for (const key of Object.keys(DEFAULT_PRINT_LAYOUT) as (keyof PrintLayoutConfig)[]) {\n const left = a[key];\n const right = b[key];\n if (Array.isArray(left) || Array.isArray(right)) {\n if (JSON.stringify(left) !== JSON.stringify(right)) return false;\n } else if (left !== right) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Clear each data/atlas block whose layer `missing` rejects. Shared by the two\n * entry points below, which differ only in how they decide what is gone.\n *\n * @returns The same object when every block's layer survives.\n */\nfunction clearBlocksForMissingLayers(\n config: PrintLayoutConfig,\n missing: (layerId: string) => boolean,\n): PrintLayoutConfig {\n const gone = (id: string) => id !== \"\" && missing(id);\n if (!gone(config.tableLayerId) && !gone(config.chartLayerId) && !gone(config.atlasLayerId)) {\n return config;\n }\n return {\n ...config,\n ...(gone(config.tableLayerId) ? { tableLayerId: \"\", showDataTable: false } : {}),\n ...(gone(config.chartLayerId) ? { chartLayerId: \"\", showDataChart: false } : {}),\n ...(gone(config.atlasLayerId) ? { atlasLayerId: \"\", atlasEnabled: false } : {}),\n };\n}\n\n/**\n * Drop references to layers the project no longer carries, so a composer that\n * pointed at a since-deleted layer opens with the block cleared instead of\n * rendering nothing from a dangling id. Mirrors the widget/comment/legend\n * scrubbing in `applyProjectToStore`.\n *\n * @param config - The config to scrub.\n * @param existingLayerIds - Ids of the layers present in the project.\n * @returns The same object when nothing referenced a missing layer.\n */\nexport function scrubPrintLayoutForLayers(\n config: PrintLayoutConfig,\n existingLayerIds: ReadonlySet<string>,\n): PrintLayoutConfig {\n return clearBlocksForMissingLayers(config, (id) => !existingLayerIds.has(id));\n}\n\n/**\n * The delete-time counterpart, matching `scrubLegendForRemovedLayers` and its\n * siblings: called when layers are removed from the open project so the saved\n * file never carries a block pointing at a layer that is already gone.\n *\n * @param config - The config to scrub.\n * @param layerIds - The removed layer id, or ids.\n * @returns The same object when no block referenced a removed layer.\n */\nexport function scrubPrintLayoutForRemovedLayers(\n config: PrintLayoutConfig,\n layerIds: string | Iterable<string>,\n): PrintLayoutConfig {\n const removed = removedLayerIdSet(layerIds);\n if (removed.size === 0) return config;\n return clearBlocksForMissingLayers(config, (id) => removed.has(id));\n}\n","// 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 \"standard-deviation\",\n \"geometric-interval\",\n \"manual\",\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 { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from \"./types\";\n\n/**\n * CZML (Cesium Language) domain models and layer authoring (issue #2290).\n *\n * CZML is a JSON-based format for describing dynamic, time-varying 3D geospatial\n * scenes on the Cesium globe (trajectories, satellites, vehicles, sensor\n * networks, moving entities with orientations and paths). The globe loads CZML\n * natively through `CzmlDataSource`; the 2D map has no equivalent, so CZML\n * layers are badged \"3D only\" there.\n */\n\n/** `metadata.sourceKind` of a layer that carries or references CZML data. */\nexport const CZML_SOURCE_KIND = \"czml\";\n\n/** One CZML packet in a document stream. */\nexport type CzmlPacket = Record<string, unknown>;\n\n/** A CZML document represented as an array of packets. */\nexport type CzmlDocument = CzmlPacket[];\n\n/** Minimal Point sample in CZML. */\nexport const CZML_SAMPLE_POINT: CzmlPacket[] = [\n {\n id: \"document\",\n name: \"CZML Point Sample\",\n version: \"1.0\",\n },\n {\n id: \"point 1\",\n name: \"Extruded Point\",\n position: {\n cartographicDegrees: [-75.59777, 40.03883, 1000],\n },\n point: {\n color: {\n rgba: [255, 128, 0, 255],\n },\n pixelSize: 14,\n outlineColor: {\n rgba: [255, 255, 255, 255],\n },\n outlineWidth: 2,\n },\n },\n];\n\n/** Time-dynamic trajectory sample with an orbit track and clock. */\nexport const CZML_SAMPLE_DYNAMIC: CzmlPacket[] = [\n {\n id: \"document\",\n name: \"CZML Trajectory Sample\",\n version: \"1.0\",\n clock: {\n interval: \"2026-09-09T00:00:00Z/2026-09-09T02:00:00Z\",\n currentTime: \"2026-09-09T00:00:00Z\",\n multiplier: 60,\n range: \"LOOP_STOP\",\n },\n },\n {\n id: \"satellite\",\n name: \"Satellite Track\",\n availability: \"2026-09-09T00:00:00Z/2026-09-09T02:00:00Z\",\n path: {\n material: {\n solidColor: {\n color: {\n rgba: [0, 200, 255, 255],\n },\n },\n },\n width: 2,\n leadTime: 1800,\n trailTime: 1800,\n },\n position: {\n epoch: \"2026-09-09T00:00:00Z\",\n cartographicDegrees: [\n 0, -75, 40, 250000, 1800, -30, 20, 250000, 3600, 20, 0, 250000, 5400, 70, -20, 250000, 7200,\n 120, -40, 250000,\n ],\n },\n point: {\n color: {\n rgba: [255, 255, 255, 255],\n },\n pixelSize: 10,\n },\n },\n];\n\n/** One-click CZML samples offered in the Add Data dialog. */\nexport const CZML_QUICK_PICKS: ReadonlyArray<{\n name: string;\n data: CzmlPacket[];\n}> = [\n { name: \"CZML Point\", data: CZML_SAMPLE_POINT },\n { name: \"CZML Dynamic Trajectory\", data: CZML_SAMPLE_DYNAMIC },\n];\n\n/**\n * Parse and validate a CZML payload from a JSON string or raw object.\n * Returns an array of CZML packets or null when the input is malformed.\n */\nexport function parseCzml(value: unknown): CzmlPacket[] | null {\n if (!value) return null;\n let parsed = value;\n if (typeof value === \"string\") {\n const trimmed = value.trim();\n if (!trimmed.startsWith(\"[\") && !trimmed.startsWith(\"{\")) return null;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n return null;\n }\n }\n if (Array.isArray(parsed)) {\n return parsed.length > 0 && parsed.every((item) => item && typeof item === \"object\")\n ? (parsed as CzmlPacket[])\n : null;\n }\n if (parsed && typeof parsed === \"object\") {\n return [parsed as CzmlPacket];\n }\n return null;\n}\n\n/**\n * Whether `layer` represents a CZML dynamic 3D scene.\n */\nexport function isCzmlLayer(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): boolean {\n return layer.metadata?.sourceKind === CZML_SOURCE_KIND;\n}\n\n/** Extracted CZML source definition for a layer. */\nexport interface CzmlSource {\n url?: string;\n data?: CzmlPacket[] | string;\n}\n\n/**\n * Extract the CZML content and/or URL from a layer, or null if not a CZML layer.\n */\nexport function czmlSource(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): CzmlSource | null {\n if (!isCzmlLayer(layer)) return null;\n const raw = layer.source?.czmlData as CzmlPacket[] | CzmlPacket | string | undefined;\n // An empty packet array is a document with nothing in it, not a document; a\n // bare packet (the Python API accepts one) is wrapped the way `parseCzml`\n // does, since Cesium indexes a document as an array.\n const data = Array.isArray(raw)\n ? raw.length > 0\n ? raw\n : undefined\n : raw && typeof raw === \"object\"\n ? [raw]\n : raw || undefined;\n const rawUrl = layer.source?.url;\n const url = typeof rawUrl === \"string\" && rawUrl.trim() ? rawUrl.trim() : undefined;\n if (!data && !url) return null;\n return { url, data };\n}\n\n/** Generate a unique identifier for a newly created layer. */\nfunction newLayerId(): string {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\n/**\n * Options for constructing a CZML layer.\n */\nexport interface CzmlLayerOptions {\n /** Optional layer id; defaults to a new UUID. */\n id?: string;\n /** Display name of the layer in the Layers panel. */\n name: string;\n /** Inlined CZML packet array or serialized JSON string. */\n data?: CzmlPacket[] | string;\n /** Remote URL pointing to a .czml document. */\n url?: string;\n /** Local path when loaded from disk. */\n sourcePath?: string;\n /** Credit shown in the globe's attribution control while the layer is present. */\n attribution?: string;\n}\n\n/**\n * Build a typed GeoLibreLayer representing a CZML dynamic scene on the Cesium globe.\n */\nexport function createCzmlLayer(options: CzmlLayerOptions): GeoLibreLayer {\n const id = options.id ?? newLayerId();\n const data = options.data;\n const url = options.url?.trim() || undefined;\n const sourcePath = options.sourcePath?.trim() || undefined;\n\n return {\n id,\n name: options.name,\n type: \"3d-tiles\",\n ...(sourcePath ? { sourcePath } : {}),\n source: {\n type: \"3d-tiles\",\n sourceId: id,\n ...(data !== undefined ? { czmlData: data } : {}),\n ...(url ? { url } : {}),\n ...(sourcePath ? { sourcePath } : {}),\n ...(options.attribution?.trim() ? { attribution: options.attribution.trim() } : {}),\n },\n visible: true,\n opacity: 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: CZML_SOURCE_KIND,\n externalNativeLayer: true,\n // Cesium builds real entities from the document and\n // `CesiumLayerSync.resolveFeature` answers for them, so a click can read a\n // packet's name and custom `properties` (issue #2504). Positions are\n // time-dynamic properties rather than stored geometry, so the answer\n // carries no geometry.\n identifiable: true,\n sourceId: id,\n nativeLayerIds: [id],\n },\n };\n}\n","import { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from \"./types\";\n\n/** Native KML/KMZ documents retain their own styling on the globe. */\nexport const CESIUM_KML_SOURCE_KIND = \"cesium-kml\";\n\nexport function isCesiumKmlLayer(layer: Pick<GeoLibreLayer, \"metadata\">): boolean {\n return layer.metadata?.sourceKind === CESIUM_KML_SOURCE_KIND;\n}\n\nexport interface CesiumKmlLayerOptions {\n id?: string;\n name: string;\n /** HTTP(S) KML or KMZ URL. Relative resources resolve against this URL. */\n url?: string;\n /** Inline XML, or a KMZ data URL so local archives survive project saves. */\n data?: string;\n sourcePath?: string;\n}\n\nexport function cesiumKmlSource(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): string | null {\n if (!isCesiumKmlLayer(layer)) return null;\n for (const value of [layer.source.kmlData, layer.source.url]) {\n if (typeof value === \"string\" && value.trim()) return value.trim();\n }\n return null;\n}\n\nexport function createCesiumKmlLayer(options: CesiumKmlLayerOptions): GeoLibreLayer {\n const data = options.data?.trim();\n const url = options.url?.trim();\n if (!data && !url) throw new Error(\"Provide a KML/KMZ document or URL.\");\n const id = options.id ?? crypto.randomUUID();\n return {\n id,\n name: options.name,\n type: \"3d-tiles\",\n ...(options.sourcePath ? { sourcePath: options.sourcePath } : {}),\n source: { type: \"3d-tiles\", sourceId: id, ...(data ? { kmlData: data } : { url }) },\n visible: true,\n opacity: 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: CESIUM_KML_SOURCE_KIND,\n externalNativeLayer: true,\n identifiable: false,\n },\n };\n}\n","import { isCzmlLayer } from \"./czml\";\nimport { isCesiumKmlLayer } from \"./cesium-kml\";\nimport { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from \"./types\";\n\n// Cesium Ion assets (issue #2290): 3D Tiles and imagery referenced by Ion\n// asset id rather than by URL. The globe loads them natively through the Ion\n// token the app already holds for World Terrain; the 2D map has no equivalent,\n// so these layers are flagged \"3D only\" there and never painted.\n\n/** `metadata.sourceKind` of a layer that references a Cesium Ion asset. */\nexport const CESIUM_ION_SOURCE_KIND = \"cesium-ion\";\n\n/** What an Ion asset id resolves to on the globe. */\nexport type CesiumIonAssetKind = \"3d-tiles\" | \"imagery\";\n\n/** Cesium OSM Buildings, the one-click 3D Tiles asset every Ion account can use. */\nexport const CESIUM_OSM_BUILDINGS_ASSET_ID = 96188;\n\n/** Bing Maps Aerial through Ion, the imagery asset every Ion account can use. */\nexport const CESIUM_BING_AERIAL_ASSET_ID = 2;\n\n/**\n * Google Photorealistic 3D Tiles served through Ion. Unlike the two above it\n * is not free on every plan: Ion accounts without it get a 403 the globe\n * surfaces as a layer error.\n */\nexport const CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID = 2275207;\n\n/**\n * Which tier of the Ion catalog a quick pick comes from. `global` assets ship\n * with every Ion account; `depot` assets are Asset Depot samples the account\n * has to add once at ion.cesium.com before they stream (until then the\n * endpoint 404s and the globe reports a layer error).\n */\nexport type CesiumIonQuickPickGroup = \"global\" | \"depot\";\n\nexport interface CesiumIonQuickPick {\n assetId: number;\n kind: CesiumIonAssetKind;\n name: string;\n group: CesiumIonQuickPickGroup;\n}\n\n/**\n * Ion assets offered as one-click picks in the Add Data dialog. Asset ids are\n * the dropdown's option values, so they have to stay unique; the ids below are\n * the ones Cesium's own samples use.\n */\nexport const CESIUM_ION_QUICK_PICKS: ReadonlyArray<CesiumIonQuickPick> = [\n {\n assetId: CESIUM_OSM_BUILDINGS_ASSET_ID,\n kind: \"3d-tiles\",\n name: \"Cesium OSM Buildings\",\n group: \"global\",\n },\n {\n assetId: CESIUM_BING_AERIAL_ASSET_ID,\n kind: \"imagery\",\n name: \"Bing Maps Aerial (Ion)\",\n group: \"global\",\n },\n {\n assetId: CESIUM_GOOGLE_PHOTOREALISTIC_ASSET_ID,\n kind: \"3d-tiles\",\n name: \"Google Photorealistic 3D Tiles\",\n group: \"global\",\n },\n { assetId: 2602291, kind: \"3d-tiles\", name: \"Japan 3D Building Data\", group: \"depot\" },\n { assetId: 69380, kind: \"3d-tiles\", name: \"Melbourne Photogrammetry\", group: \"depot\" },\n { assetId: 43978, kind: \"3d-tiles\", name: \"Melbourne Point Cloud\", group: \"depot\" },\n { assetId: 28945, kind: \"3d-tiles\", name: \"Montreal Point Cloud\", group: \"depot\" },\n { assetId: 75343, kind: \"3d-tiles\", name: \"New York City 3D Buildings\", group: \"depot\" },\n { assetId: 3827, kind: \"imagery\", name: \"Washington DC 2017\", group: \"depot\" },\n];\n\n/** Parse an Ion asset id the way the Add Data form and the project file carry it. */\nexport function parseCesiumIonAssetId(value: unknown): number | null {\n const id = typeof value === \"string\" ? Number(value.trim()) : value;\n return typeof id === \"number\" && Number.isSafeInteger(id) && id > 0 ? id : null;\n}\n\n/**\n * The Ion asset id a layer references, or null when the layer is not an Ion\n * asset layer (or carries no usable id).\n */\nexport function cesiumIonAssetId(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): number | null {\n if (layer.metadata?.sourceKind !== CESIUM_ION_SOURCE_KIND) return null;\n return parseCesiumIonAssetId(layer.source?.ionAssetId);\n}\n\n/** Whether `layer` references a Cesium Ion asset. */\nexport function isCesiumIonLayer(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): boolean {\n return cesiumIonAssetId(layer) !== null;\n}\n\n/** What the Ion asset draws as: a tileset primitive or an imagery layer. */\nexport function cesiumIonAssetKind(layer: Pick<GeoLibreLayer, \"type\">): CesiumIonAssetKind {\n return layer.type === \"3d-tiles\" ? \"3d-tiles\" : \"imagery\";\n}\n\n/**\n * Whether only the 3D globe can render `layer`: the mirror of the globe's\n * `isCesiumSupportedLayerType`, for the Layers panel to badge on the 2D map.\n */\nexport function isCesiumOnlyLayer(layer: Pick<GeoLibreLayer, \"source\" | \"metadata\">): boolean {\n return isCesiumIonLayer(layer) || isCzmlLayer(layer) || isCesiumKmlLayer(layer);\n}\n\nfunction newLayerId(): string {\n return typeof crypto !== \"undefined\" && \"randomUUID\" in crypto\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n}\n\nexport interface CesiumIonLayerOptions {\n id?: string;\n name: string;\n assetId: number;\n kind: CesiumIonAssetKind;\n /** Metres to shift a tileset vertically; ignored for imagery. */\n altitudeOffset?: number;\n}\n\n/**\n * Build the store layer for a Cesium Ion asset. A tileset is a `3d-tiles`\n * layer whose `sourceKind` keeps it out of the 2D deck.gl control; imagery is\n * a `raster` layer with no tiles, marked external so the 2D sync leaves it\n * alone. Both persist only the asset id: the token is runtime configuration.\n */\nexport function createCesiumIonLayer(options: CesiumIonLayerOptions): GeoLibreLayer {\n const id = options.id ?? newLayerId();\n const isTileset = options.kind === \"3d-tiles\";\n const altitudeOffset = Number.isFinite(options.altitudeOffset) ? options.altitudeOffset! : 0;\n return {\n id,\n name: options.name,\n type: isTileset ? \"3d-tiles\" : \"raster\",\n source: {\n type: isTileset ? \"3d-tiles\" : \"raster\",\n ionAssetId: options.assetId,\n sourceId: id,\n ...(isTileset ? { altitudeOffset } : {}),\n },\n visible: true,\n opacity: 1,\n style: { ...DEFAULT_LAYER_STYLE },\n metadata: {\n sourceKind: CESIUM_ION_SOURCE_KIND,\n externalNativeLayer: true,\n identifiable: false,\n sourceId: id,\n nativeLayerIds: [id],\n ...(isTileset ? { customLayerType: \"3d-tiles\", altitudeOffset } : {}),\n },\n };\n}\n","/**\n * `${NAME}` references in request header values.\n *\n * A header such as `Authorization: Bearer ${TILES_TOKEN}` is saved as typed\n * and resolved from the project's Environment Variables when a request is\n * made, so the secret itself never sits on the layer. This module is pure (no\n * store import): `credentials.ts` depends on it, and the store depends on\n * `credentials.ts`.\n */\n\n/** `${NAME}` where NAME follows the environment-variable name rule. */\nconst HEADER_REFERENCE = /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\nconst HAS_HEADER_REFERENCE = /\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}/;\n/**\n * Optional auth scheme word, whitespace, one reference, nothing else. Kept in\n * lockstep with `_HEADER_REFERENCE_ONLY` in `python/src/geolibre/project.py`.\n */\nconst REFERENCE_ONLY = /^(?:[A-Za-z][A-Za-z0-9._-]*\\s+)?\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}$/;\n\nconst warnedMissing = new Set<string>();\n\nexport function hasHeaderReferences(headers: Record<string, string> | undefined): boolean {\n if (!headers) return false;\n return Object.values(headers).some(\n (value) => typeof value === \"string\" && HAS_HEADER_REFERENCE.test(value),\n );\n}\n\n/**\n * Whether `value` carries no secret of its own: at most a scheme word such as\n * `Bearer` followed by a single `${NAME}`. Redaction keeps such values.\n */\nexport function isHeaderReferenceOnly(value: string): boolean {\n return REFERENCE_ONLY.test(value.trim());\n}\n\n/**\n * Whether credential-bearing request headers may be sent to `url`: HTTPS, or\n * plain HTTP to loopback so a local dev server still works. The scheme is read\n * off a parsed URL, so an unusually cased `HTTPS://` is not misread as\n * plaintext; a relative or unparseable URL is refused.\n */\nexport function allowsCredentialHeaders(url: string): boolean {\n try {\n const { protocol, hostname } = new URL(url);\n if (protocol === \"https:\") return true;\n return (\n protocol === \"http:\" &&\n (hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"[::1]\")\n );\n } catch {\n return false;\n }\n}\n\n/**\n * `headers` with every `${NAME}` replaced by `values[NAME]`. A header that\n * references an unset (or empty) variable is omitted rather than sent with a\n * hole in it. Returns the input unchanged when nothing references a variable,\n * and `undefined` when no header remains.\n */\nexport function resolveHeaderReferences(\n headers: Record<string, string> | undefined,\n values: Readonly<Record<string, string>>,\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n if (!Object.values(headers).some((value) => typeof value === \"string\" && value.includes(\"${\"))) {\n return headers;\n }\n const resolved: Record<string, string> = {};\n for (const [name, value] of Object.entries(headers)) {\n if (typeof value !== \"string\") continue;\n let missing: string | null = null;\n const next = value.replace(HEADER_REFERENCE, (_match, variable: string) => {\n const replacement = Object.hasOwn(values, variable) ? values[variable] : \"\";\n if (!replacement) missing ??= variable;\n return replacement;\n });\n if (missing !== null) {\n const warnKey = `${name}:${missing}`;\n if (!warnedMissing.has(warnKey)) {\n warnedMissing.add(warnKey);\n console.warn(\n `[GeoLibre] Request header \"${name}\" references unset environment variable ${missing}; the header is not sent.`,\n );\n }\n continue;\n }\n resolved[name] = next;\n }\n return Object.keys(resolved).length > 0 ? resolved : undefined;\n}\n","import { isHeaderReferenceOnly } from \"./header-references\";\nimport type { GeoLibreProject, LayerConnection } from \"./types\";\n\n/**\n * Project fields whose values are credentials. Keep this registry as the\n * schema-level inventory. Keep the explicit preference/plugin handling in\n * redactProjectCredentials in sync when adding entries outside layer config.\n */\nexport const PROJECT_CREDENTIAL_FIELDS = {\n preferences: [\"environmentVariables\", \"geocoding.apiKeys\"],\n layerConfiguration: [\n \"requestHeaders\",\n \"headers\",\n \"authorization\",\n \"apiKey\",\n \"apiKeys\",\n \"accessToken\",\n \"token\",\n \"password\",\n \"clientSecret\",\n \"connectionString\",\n \"secret\",\n \"bearer\",\n \"auth\",\n \"authKey\",\n \"sasToken\",\n \"subscriptionKey\",\n \"signature\",\n \"pwd\",\n ],\n pluginState: [\"plugins.settings\"],\n} as const;\n\n/**\n * Plugin settings that survive redaction: plugin id to the sub-keys kept from\n * its blob, or `null` to keep the whole blob.\n *\n * A plugin's settings are free-form and an external plugin can persist a\n * credential there, so the default is to drop all of it. What is listed here is\n * map-control *composition* — the swipe split, legend entries and colors, the\n * colorbar's range and ramp — which is what a shared or exported project needs\n * in order to render the map it was built as, and is structured rather than\n * free text. Dropping it silently stripped every legend, colorbar, and swipe\n * from an export.\n *\n * The components plugin's `html` sub-key is deliberately absent: it holds a\n * custom HTML panel the user authored by hand (`ComponentHtmlGuiEntryState`),\n * so it can carry anything, including a URL with a token in it. It is dropped\n * like any unknown blob. Mirrored by `PUBLISHABLE_PLUGIN_SETTINGS` in\n * `python/src/geolibre/project.py`; the two must agree.\n */\nexport const PUBLISHABLE_PLUGIN_SETTINGS: Readonly<Record<string, readonly string[] | null>> = {\n \"maplibre-gl-swipe\": null,\n \"maplibre-gl-components\": [\"legend\", \"colorbar\"],\n // This is the source of truth for dock-owned temporal layers. Dropping it\n // leaves only their external-native store mirrors in a shared project; those\n // mirrors contain a MapLibre source id, not enough information to recreate\n // the COG/mosaic/tiles for a recipient. The retained blob is still passed\n // through redactConfigurationValue below, including every source URL.\n \"maplibre-gl-time-slider\": null,\n // Feed toggles (one boolean per feed) plus a numeric clock speed — no URLs,\n // no keys, nothing user-authored. Listed as a whole blob rather than by key\n // because the feed set grows with every new feed; an enumerated list would\n // silently start counting each new toggle as a credential, which is the bug\n // this entry fixes. Still swept by redactConfigurationValue below.\n \"gods-eye-view\": null,\n // Point class edits keyed by (node key, index): compressed numbers, no\n // user text. Source URLs are values, so the sweep below still scrubs a\n // credentialed one.\n \"geolibre-point-cloud-annotation\": null,\n};\n\nexport interface CredentialRedactionResult {\n project: GeoLibreProject;\n /** Stable project paths removed or rewritten by the redaction pass. */\n redactedPaths: string[];\n /**\n * Opaque `path=hash` fingerprints identifying *which* secret sat at each\n * redacted path. A caller that remembers an explicit \"keep credentials\"\n * decision compares these between saves, so a different credential, such as\n * a swapped layer or rotated token, is confirmed again instead of being\n * written to disk under the earlier answer. Paths alone cannot carry that:\n * they are positional, so a replacement layer inherits the path of the one it\n * replaced.\n */\n redactedFingerprints: string[];\n /**\n * Whether any redacted value could not be serialized, and so has no entry in\n * {@link redactedFingerprints}. Such a value cannot be compared with what the\n * user accepted, so a caller reusing a remembered decision must ask again\n * rather than assume the unfingerprintable credential is unchanged.\n */\n hasUnfingerprintableCredential: boolean;\n /** Number of individual credential-bearing fields removed or rewritten. */\n redactedCount: number;\n}\n\n/** Collector threaded through the redaction pass. */\ninterface RedactionAccumulator {\n paths: string[];\n fingerprints: string[];\n count: number;\n /** Whether some redacted value could not be serialized, and so not fingerprinted. */\n unfingerprintable: boolean;\n}\n\n/**\n * Hash a credential value to a short, stable 64-bit token: two independent\n * FNV-1a-style lanes, the first with the canonical FNV prime and the second\n * with a MurmurHash3 mixing constant so the lanes do not move together.\n *\n * The digest never leaves memory and is only ever compared for equality, so it\n * needs to be stable and cheap rather than cryptographic. Hashing rather than\n * retaining the value keeps the secret itself out of the remembered choices.\n *\n * A collision costs a *missed* confirmation, not a spurious one: a caller\n * comparing fingerprints would treat a different secret as one the user already\n * accepted. That is why this is 64 bits rather than 32, and why a value that\n * cannot be serialized returns null rather than hashing a lossy `String(value)`\n * that would collapse every such value onto `[object Object]`.\n *\n * @returns The digest, or null when the value cannot be serialized.\n */\nfunction fingerprintValue(value: unknown): string | null {\n let serialized: string;\n try {\n serialized = JSON.stringify(value) ?? String(value);\n } catch {\n return null;\n }\n let low = 0x811c9dc5;\n let high = 0x27220a95;\n for (let index = 0; index < serialized.length; index += 1) {\n const code = serialized.charCodeAt(index);\n low = Math.imul(low ^ code, 0x01000193);\n high = Math.imul(high ^ code, 0x85ebca6b);\n }\n return `${(low >>> 0).toString(36)}.${(high >>> 0).toString(36)}`;\n}\n\n/** Record one redaction, with the fingerprint of the value being removed. */\nfunction recordRedaction(\n accumulator: RedactionAccumulator,\n path: string,\n value: unknown,\n count = 1,\n): void {\n const digest = fingerprintValue(value);\n accumulator.paths.push(path);\n // A value that cannot be serialized fails closed: it is reported through\n // `hasUnfingerprintableCredential` rather than fingerprinted, so a caller\n // asks again instead of reusing an answer it cannot verify still applies.\n if (digest === null) accumulator.unfingerprintable = true;\n else accumulator.fingerprints.push(`${path}=${digest}`);\n accumulator.count += count;\n}\n\n/**\n * Fold the spellings of one credential name together, so `apiKey`, `api_key`,\n * `api-key`, and `APIKEY` are a single registry entry on both the object-key\n * and the URL-parameter side.\n */\nfunction normalizeCredentialName(name: string): string {\n return name.toLowerCase().replace(/[-_]/g, \"\");\n}\n\nconst SENSITIVE_KEYS = new Set(\n PROJECT_CREDENTIAL_FIELDS.layerConfiguration.map(normalizeCredentialName),\n);\n/**\n * Credential names in query strings. This is the wider of the two registries by\n * design: everything here is also an object-key credential *except* `key` and\n * the Azure SAS positional parameters (`sv`/`sr`/`st`/`se`/`sp`/`sig`/`skoid`),\n * which are only credentials because of where they appear. As configuration\n * field names they collide with ordinary state — `sr` is a spatial reference on\n * an ArcGIS source, `key` is a generic identifier — so matching them on object\n * keys would silently drop layer configuration rather than a secret.\n */\nconst URL_CREDENTIAL_PARAMS = new Set(\n [\n ...PROJECT_CREDENTIAL_FIELDS.layerConfiguration,\n \"key\",\n \"sig\",\n \"se\",\n \"sp\",\n \"sv\",\n \"sr\",\n \"st\",\n \"skoid\",\n ].map(normalizeCredentialName),\n);\n/**\n * Depth at which the redaction pass stops descending and fails closed.\n * Exported so a caller that predicts what redaction will remove (the Share\n * dialog's readiness check) scans exactly as deep as this pass does, instead of\n * keeping a second, shallower cap that would silently disagree.\n */\nexport const MAX_REDACT_DEPTH = 12;\n\n/** Whether an object key in layer/plugin configuration holds a credential. */\nexport function isCredentialFieldName(name: string): boolean {\n return SENSITIVE_KEYS.has(normalizeCredentialName(name));\n}\n\nfunction isCredentialParam(name: string): boolean {\n let decoded = name.replace(/\\+/g, \" \");\n try {\n decoded = decodeURIComponent(decoded);\n } catch {\n // Malformed names cannot match the registry, but must not break export.\n }\n const lowered = decoded.toLowerCase();\n return (\n URL_CREDENTIAL_PARAMS.has(normalizeCredentialName(lowered)) || lowered.startsWith(\"x-amz-\")\n );\n}\n\n/**\n * Whether a URL query-parameter name carries a credential. This is the wider of\n * the two registries (see `URL_CREDENTIAL_PARAMS`). Exported so a caller that\n * must leave the parameter in place — the native diagnostics log rewrites its\n * value to `[redacted]` rather than dropping the pair — shares this registry\n * instead of keeping a second, narrower list that can drift from it.\n */\nexport function isCredentialUrlParam(name: string): boolean {\n return isCredentialParam(name);\n}\n\nfunction redactParameterString(value: string): string {\n return value\n .split(\"&\")\n .filter((pair) => pair !== \"\" && !isCredentialParam(pair.split(\"=\", 1)[0]))\n .join(\"&\");\n}\n\n/**\n * Remove credentials from URL-shaped values without encoding tile-template\n * placeholders such as `{z}/{x}/{y}`.\n */\nexport function redactUrlCredentials(value: string): string {\n const hashIndex = value.indexOf(\"#\");\n const beforeHash = hashIndex === -1 ? value : value.slice(0, hashIndex);\n const fragment = hashIndex === -1 ? undefined : value.slice(hashIndex + 1);\n const queryIndex = beforeHash.indexOf(\"?\");\n let base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);\n const query = queryIndex === -1 ? undefined : beforeHash.slice(queryIndex + 1);\n\n const schemeIndex = base.indexOf(\"://\");\n if (schemeIndex !== -1) {\n const authorityStart = schemeIndex + 3;\n const authorityEnd = base.indexOf(\"/\", authorityStart);\n const authority = base.slice(authorityStart, authorityEnd === -1 ? undefined : authorityEnd);\n const at = authority.lastIndexOf(\"@\");\n if (at !== -1) {\n base =\n base.slice(0, authorityStart) +\n authority.slice(at + 1) +\n (authorityEnd === -1 ? \"\" : base.slice(authorityEnd));\n }\n }\n\n const keptQuery = query === undefined ? undefined : redactParameterString(query);\n const keptFragment =\n fragment === undefined || !fragment.includes(\"=\") ? fragment : redactParameterString(fragment);\n return base + (keptQuery ? `?${keptQuery}` : \"\") + (keptFragment ? `#${keptFragment}` : \"\");\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isGeoJsonPayload(value: Record<string, unknown>): boolean {\n return (\n value.type === \"FeatureCollection\" ||\n value.type === \"Feature\" ||\n value.type === \"GeometryCollection\"\n );\n}\n\nfunction redactConfigurationValue(\n value: unknown,\n path: string,\n accumulator: RedactionAccumulator,\n depth = 0,\n): unknown {\n if (depth >= MAX_REDACT_DEPTH) {\n // Fail closed. A deeply nested configuration shape is not needed to render\n // any built-in layer, and returning it unchanged would let a credential\n // bypass the invariant merely by exceeding the traversal cap.\n recordRedaction(accumulator, path, value);\n return undefined;\n }\n if (typeof value === \"string\") {\n const redacted = redactUrlCredentials(value);\n if (redacted !== value) {\n recordRedaction(accumulator, path, value);\n }\n return redacted;\n }\n if (Array.isArray(value)) {\n return value.map((item, index) =>\n redactConfigurationValue(item, `${path}[${index}]`, accumulator, depth + 1),\n );\n }\n if (!isPlainObject(value)) return value;\n if (isGeoJsonPayload(value)) return structuredClone(value);\n\n const result: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value)) {\n const nestedPath = path ? `${path}.${key}` : key;\n const normalizedKey = normalizeCredentialName(key);\n if (\n (normalizedKey === \"requestheaders\" || normalizedKey === \"headers\") &&\n isPlainObject(nested)\n ) {\n // `Bearer ${TOKEN}` names a variable instead of carrying a secret, so it\n // survives; any other header value is dropped. Mirrors `_redact_config`\n // in python/src/geolibre/project.py.\n const kept: Record<string, string> = {};\n const removed: Record<string, unknown> = {};\n for (const [name, headerValue] of Object.entries(nested)) {\n if (typeof headerValue === \"string\" && isHeaderReferenceOnly(headerValue)) {\n kept[name] = headerValue;\n } else {\n removed[name] = headerValue;\n }\n }\n if (Object.keys(removed).length > 0) recordRedaction(accumulator, nestedPath, removed);\n if (Object.keys(kept).length > 0) result[key] = kept;\n continue;\n }\n if (isCredentialFieldName(key)) {\n recordRedaction(accumulator, nestedPath, nested);\n continue;\n }\n result[key] = redactConfigurationValue(nested, nestedPath, accumulator, depth + 1);\n }\n return result;\n}\n\nfunction countLeafValues(value: unknown): number {\n if (Array.isArray(value)) return value.reduce((count, item) => count + countLeafValues(item), 0);\n if (isPlainObject(value)) {\n return Object.values(value).reduce(\n (count: number, nested) => count + countLeafValues(nested),\n 0,\n );\n }\n return 1;\n}\n\n/**\n * Settings keys that registry-listed external plugins declared publishable,\n * keyed by plugin id. `null` keeps the whole blob. Populated at runtime by the\n * host from the curated plugin registry, never from the project file itself,\n * so a project cannot vouch for its own state. The Python mirror in\n * `python/src/geolibre/project.py` only knows the static built-in list, so a\n * project redacted there still drops these plugins' state.\n */\nconst registryPublishableSettings = new Map<string, readonly string[] | null>();\n\n/**\n * Replace the registry-declared publishable plugin settings.\n *\n * Built-in ids in {@link PUBLISHABLE_PLUGIN_SETTINGS} always win, so a registry\n * entry cannot widen a first-party plugin's allowlist.\n *\n * @param entries Plugin ids mapped to the settings keys to keep, or `null` to\n * keep the whole blob. Ids that declare nothing should be omitted.\n */\nexport function setRegistryPublishableSettings(\n entries: Iterable<readonly [string, readonly string[] | null]>,\n): void {\n registryPublishableSettings.clear();\n for (const [id, keys] of entries) {\n if (Object.prototype.hasOwnProperty.call(PUBLISHABLE_PLUGIN_SETTINGS, id)) continue;\n registryPublishableSettings.set(id, keys);\n }\n}\n\n/**\n * Look up the publishable settings for a plugin id.\n *\n * @param id Plugin id.\n * @returns `undefined` when nothing is publishable, `null` to keep the whole\n * blob, otherwise the list of keys to keep.\n */\nfunction publishableSettingsFor(id: string): readonly string[] | null | undefined {\n if (Object.prototype.hasOwnProperty.call(PUBLISHABLE_PLUGIN_SETTINGS, id)) {\n return PUBLISHABLE_PLUGIN_SETTINGS[id];\n }\n return registryPublishableSettings.get(id);\n}\n\n/**\n * Return a detached project safe for any external egress.\n *\n * Environment variables and geocoder keys are always removed. Layer\n * configuration is recursively scrubbed for credential fields and credential\n * URL parameters. Plugin settings are omitted wholesale because external\n * plugins can persist arbitrary shapes; retaining unknown state cannot provide\n * a no-secret guarantee. The first-party map controls in\n * {@link PUBLISHABLE_PLUGIN_SETTINGS} are the exception: their state is known\n * and carries no credentials, and dropping it stripped the legend, colorbar,\n * and swipe from the exported map. Manifest URLs, activation, and control\n * positions stay intact so recipients can still load and configure the plugin\n * themselves. A registry-listed external plugin can opt in through the\n * `publishableSettings` field of its registry entry (see\n * {@link setRegistryPublishableSettings}); what it keeps is still swept below.\n */\nexport function redactProjectCredentials(project: GeoLibreProject): CredentialRedactionResult {\n const accumulator: RedactionAccumulator = {\n paths: [],\n fingerprints: [],\n count: 0,\n unfingerprintable: false,\n };\n const basemapStyleUrl =\n typeof project.basemapStyleUrl === \"string\"\n ? redactUrlCredentials(project.basemapStyleUrl)\n : project.basemapStyleUrl;\n if (basemapStyleUrl !== project.basemapStyleUrl) {\n recordRedaction(accumulator, \"basemapStyleUrl\", project.basemapStyleUrl);\n }\n const geocoding = project.preferences?.geocoding\n ? { ...project.preferences.geocoding, apiKeys: {} }\n : project.preferences?.geocoding;\n if (geocoding) {\n for (const field of [\"forwardEndpoint\", \"reverseEndpoint\"] as const) {\n const endpoint = geocoding[field];\n if (typeof endpoint !== \"string\") continue;\n const redacted = redactUrlCredentials(endpoint);\n if (redacted !== endpoint) {\n geocoding[field] = redacted;\n recordRedaction(accumulator, `preferences.geocoding.${field}`, endpoint);\n }\n }\n }\n // The Mapbox-only style is a URL like the shared basemap and can carry an\n // access token; sweep it the same way so the save prompt counts it and a\n // \"strip\" choice actually removes it.\n const mapboxStyleUrl = project.preferences?.map?.mapboxStyleUrl;\n const redactedMapboxStyleUrl =\n typeof mapboxStyleUrl === \"string\" ? redactUrlCredentials(mapboxStyleUrl) : mapboxStyleUrl;\n if (redactedMapboxStyleUrl !== mapboxStyleUrl) {\n recordRedaction(accumulator, \"preferences.map.mapboxStyleUrl\", mapboxStyleUrl);\n }\n const preferences = project.preferences\n ? {\n ...project.preferences,\n // Only rows explicitly marked non-secret travel; everything else is\n // treated as a credential.\n environmentVariables: (project.preferences.environmentVariables ?? []).filter(\n (variable) => variable.secret === false,\n ),\n geocoding,\n ...(redactedMapboxStyleUrl !== mapboxStyleUrl\n ? { map: { ...project.preferences.map, mapboxStyleUrl: redactedMapboxStyleUrl } }\n : {}),\n }\n : project.preferences;\n // A secret row with an empty value carries nothing (desktop keeps the value\n // in the keychain), so it must not count toward the save prompt. A populated\n // row with no name still needs an explicit keep/strip choice.\n const populatedEnvironmentVariables =\n project.preferences?.environmentVariables?.filter(\n (variable) => variable.secret !== false && variable.value !== \"\",\n ) ?? [];\n if (populatedEnvironmentVariables.length > 0) {\n recordRedaction(\n accumulator,\n \"preferences.environmentVariables\",\n populatedEnvironmentVariables,\n populatedEnvironmentVariables.length,\n );\n }\n const geocodingApiKeys = project.preferences?.geocoding?.apiKeys ?? {};\n if (Object.keys(geocodingApiKeys).length > 0) {\n recordRedaction(\n accumulator,\n \"preferences.geocoding.apiKeys\",\n geocodingApiKeys,\n Object.keys(geocodingApiKeys).length,\n );\n }\n\n const layers = (project.layers ?? []).map((layer, index) => {\n // `export: false` strips the feature data this project *carries* — the\n // inline `geojson` and the `embeddedGeoJSON` snapshot. It deliberately\n // leaves `source`/`connection` alone, so a layer that fetches live (WFS,\n // ArcGIS FeatureServer, a remote GeoJSON or tile URL) still travels with a\n // reachable URL and re-fetches the same data when the shared project is\n // opened. Stripping those would ship a layer that cannot render at all;\n // whether that is the right trade is tracked in the docs' caveat.\n const isNoExport = layer.capabilities?.export === false;\n const cleanMetadata = redactConfigurationValue(\n layer.metadata,\n `layers[${index}].metadata`,\n accumulator,\n ) as Record<string, unknown>;\n if (isNoExport && \"embeddedGeoJSON\" in cleanMetadata) {\n delete cleanMetadata.embeddedGeoJSON;\n }\n return {\n ...layer,\n ...(isNoExport ? { geojson: undefined } : {}),\n source: redactConfigurationValue(\n layer.source,\n `layers[${index}].source`,\n accumulator,\n ) as Record<string, unknown>,\n metadata: cleanMetadata,\n ...(typeof layer.sourcePath === \"string\"\n ? {\n sourcePath: redactConfigurationValue(\n layer.sourcePath,\n `layers[${index}].sourcePath`,\n accumulator,\n ) as string,\n }\n : {}),\n // `connection.lastError` is free-form text taken from a caught error, and a\n // refresh path that words it as `Failed to fetch ${url}` would carry the\n // request's credential parameters. Sweeping it costs nothing and keeps the\n // no-secret guarantee from depending on how an error message is phrased.\n ...(layer.connection\n ? {\n connection: redactConfigurationValue(\n layer.connection,\n `layers[${index}].connection`,\n accumulator,\n ) as LayerConnection,\n }\n : {}),\n };\n });\n\n let plugins = project.plugins;\n if (plugins) {\n const manifestUrls = plugins.manifestUrls.map((url, index) => {\n const redacted = redactUrlCredentials(url);\n if (redacted !== url) {\n recordRedaction(accumulator, `plugins.manifestUrls[${index}]`, url);\n }\n return redacted;\n });\n const settings = plugins.settings ?? {};\n const kept: Record<string, unknown> = {};\n const dropped: Record<string, unknown> = {};\n for (const [id, value] of Object.entries(settings)) {\n const allowed = publishableSettingsFor(id);\n if (allowed === undefined) {\n dropped[id] = value;\n continue;\n }\n if (allowed === null) {\n kept[id] = value;\n continue;\n }\n // An unexpected shape is dropped rather than passed through.\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n dropped[id] = value;\n continue;\n }\n const subset: Record<string, unknown> = {};\n const rest: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n if (allowed.includes(key)) subset[key] = item;\n else rest[key] = item;\n }\n if (Object.keys(subset).length > 0) kept[id] = subset;\n if (Object.keys(rest).length > 0) dropped[id] = rest;\n }\n if (Object.keys(dropped).length > 0) {\n recordRedaction(accumulator, \"plugins.settings\", dropped, countLeafValues(dropped));\n }\n // What survives is still swept by the same pass layer configuration gets,\n // so a credentialed URL inside a kept blob is scrubbed rather than trusted.\n plugins = {\n ...plugins,\n manifestUrls,\n settings: redactConfigurationValue(kept, \"plugins.settings\", accumulator) as Record<\n string,\n unknown\n >,\n };\n }\n\n return {\n project: {\n // Remaining project-level fields are structural/user content with no\n // current credential-bearing schema. Add explicit handling above when a\n // future field (for example a processing parameter) accepts credentials.\n ...project,\n basemapStyleUrl,\n ...(preferences ? { preferences } : {}),\n layers,\n ...(plugins ? { plugins } : {}),\n ...(project.metadata\n ? {\n metadata: redactConfigurationValue(project.metadata, \"metadata\", accumulator) as Record<\n string,\n unknown\n >,\n }\n : {}),\n },\n redactedPaths: [...new Set(accumulator.paths)],\n redactedFingerprints: [...new Set(accumulator.fingerprints)],\n hasUnfingerprintableCredential: accumulator.unfingerprintable,\n redactedCount: accumulator.count,\n };\n}\n\n/**\n * Remove credentials from a single configuration value (a layer `source`,\n * `metadata`, or `sourcePath`) with the same rules the project redaction pass\n * applies to layer configuration: credential-named fields and non-reference\n * header values are dropped, and credential URL parameters are stripped from\n * every URL-shaped string.\n *\n * @param value - The configuration value to scrub. Not mutated.\n * @returns A detached, redacted copy.\n */\nexport function redactConfigurationCredentials<T>(value: T): T {\n const accumulator: RedactionAccumulator = {\n paths: [],\n fingerprints: [],\n count: 0,\n unfingerprintable: false,\n };\n return redactConfigurationValue(value, \"\", accumulator) as T;\n}\n\n/** Convenience wrapper for callers that only need the safe project. */\nexport function redactCredentials(project: GeoLibreProject): GeoLibreProject {\n return redactProjectCredentials(project).project;\n}\n","import { normalizeArcgisWkid } from \"./arcgis-projection\";\nimport { normalizeCesiumBasemap } from \"./cesium-imagery\";\nimport { parseCesiumIonAssetId } from \"./cesium-ion\";\nimport { redactUrlCredentials } from \"./credentials\";\nimport { v4 as uuidv4 } from \"uuid\";\nimport {\n DEFAULT_BASEMAP,\n DEFAULT_LAYER_STYLE,\n DEFAULT_LEGEND_CONFIG,\n DEFAULT_PROJECT_PREFERENCES,\n DEFAULT_DASHBOARD_COLUMNS,\n DEFAULT_MAP_GRID_LAYOUT,\n DEFAULT_PRIMARY_RENDERER,\n DEFAULT_STORY_MAP,\n MAX_DASHBOARD_COLUMNS,\n MAX_MAP_GRID_DIM,\n MIN_DASHBOARD_COLUMNS,\n PROJECT_VERSION,\n type DashboardWidget,\n type ProjectBookmark,\n type ProjectBookmarkGroup,\n type DashboardWidgetAggregation,\n type DashboardWidgetType,\n type GeoLibreLayer,\n type GeoLibreProject,\n type IndicatorAggregation,\n type LayerGroup,\n type LayerStyle,\n type LegendConfig,\n type LegendCustomEntry,\n type LegendCustomItem,\n type LegendItemOverride,\n type MapGridLayout,\n type MapRendererKind,\n type MapScaleUnit,\n type MapViewState,\n MAX_PROCESSING_HISTORY,\n type ModelGraphEdge,\n type ModelGraphNode,\n type ModelGraphNodeKind,\n type ModelToolProvider,\n type ProcessingModel,\n type ProcessingModelGraph,\n type ProcessingRun,\n type ProcessingRunKind,\n type SecondaryMapView,\n type ProcessingModelStep,\n type ProjectPluginControlPosition,\n type ProjectPluginState,\n type ProjectPreferences,\n type RuntimeEnvironmentVariable,\n type StoryChapter,\n type StoryChapterAlignment,\n type StoryChapterAnimation,\n type StoryInsetPosition,\n type StoryLayerOpacityChange,\n type StoryMap,\n type StorySlideMode,\n type StyleLibraryEntry,\n type CommentAnchor,\n type CommentAuthor,\n type CommentReply,\n type ProjectComment,\n type ProjectInteraction,\n} from \"./types\";\nimport { DEFAULT_LAYER_GROUP_OPACITY, normalizeGroupContiguity } from \"./layer-groups\";\nimport { normalizeStyleLibraryEntries } from \"./style-library\";\nimport { normalizeLayerCapabilities } from \"./capabilities\";\nimport { validateMapExpression } from \"./expressions\";\nimport { normalizeLayerDescriptiveMetadata } from \"./layer-descriptive-metadata\";\nimport {\n createDefaultPrintLayout,\n isDefaultPrintLayout,\n normalizePrintLayoutConfig,\n scrubPrintLayoutForLayers,\n type PrintLayoutConfig,\n} from \"./print-layout-config\";\nimport { getEllipsoid } from \"./ellipsoids\";\nimport {\n scrubWidgetsForRemovedLayers,\n scrubCommentsForRemovedLayers,\n scrubLegendForRemovedLayers,\n} from \"./layer-ref-scrub\";\n\n/** Placeholder name a project carries before the user names it. */\nexport const DEFAULT_PROJECT_NAME = \"Untitled Project\";\n\nexport interface CreateProjectOptions {\n basemapStyleUrl?: string;\n mapView?: MapViewState;\n /** Celestial body the project describes; defaults to Earth when omitted. */\n ellipsoidId?: string;\n}\n\nexport function normalizeBlankBackgroundColor(value: unknown): string | null {\n return typeof value === \"string\" && /^#[0-9a-f]{6}$/i.test(value) ? value : null;\n}\n\nexport function createDefaultMapView(): MapViewState {\n return {\n center: [-100, 40],\n zoom: 2,\n bearing: 0,\n pitch: 0,\n };\n}\n\nexport function createEmptyProject(\n name = DEFAULT_PROJECT_NAME,\n options: CreateProjectOptions = {},\n): GeoLibreProject {\n return {\n version: PROJECT_VERSION,\n name,\n mapView: options.mapView ?? createDefaultMapView(),\n basemapStyleUrl: options.basemapStyleUrl ?? DEFAULT_BASEMAP,\n basemapVisible: true,\n basemapOpacity: 1,\n blankBackgroundColor: null,\n layers: [],\n layerGroups: [],\n styles: {},\n // A copy, never the shared constant: a caller that edits the new project's\n // preferences — `project.preferences.map.cesiumBasemap = …` — would\n // otherwise rewrite the app-wide defaults for the rest of the process, and\n // every later \"what is the default\" read would answer with its edit.\n preferences: {\n ...DEFAULT_PROJECT_PREFERENCES,\n map: {\n ...DEFAULT_PROJECT_PREFERENCES.map,\n ...(options.ellipsoidId ? { ellipsoidId: getEllipsoid(options.ellipsoidId).id } : {}),\n },\n },\n legend: { ...DEFAULT_LEGEND_CONFIG },\n comments: [],\n metadata: {},\n };\n}\n\n/**\n * GeoJSON `type` values that mark a subtree as feature data rather than project\n * structure. Everything under one of these is written compactly by\n * {@link serializeProject}.\n */\nconst GEOJSON_TYPES = new Set([\n \"FeatureCollection\",\n \"Feature\",\n \"GeometryCollection\",\n \"Point\",\n \"MultiPoint\",\n \"LineString\",\n \"MultiLineString\",\n \"Polygon\",\n \"MultiPolygon\",\n]);\n\n/** One level of indentation in a serialized project. */\nconst PROJECT_INDENT = \" \";\n\n/**\n * Whether a value is a GeoJSON feature, geometry, or collection.\n *\n * Decided from the `type` string alone, so this is an implicit contract on the\n * project schema: no field may store a non-GeoJSON object under a `type` of one\n * of the nine {@link GEOJSON_TYPES} names, or it would silently be written\n * compact as if it were feature data. Nothing in `types.ts` does today; a new\n * field that wants one of those names (a drawing-tool or geometry-filter config,\n * say) needs a different discriminator.\n */\nfunction isGeoJsonValue(value: object): boolean {\n const type = (value as { type?: unknown }).type;\n return typeof type === \"string\" && GEOJSON_TYPES.has(type);\n}\n\n/**\n * Serialize a value exactly as `JSON.stringify(value, null, 2)` would, except\n * that GeoJSON features, geometries and collections are written compactly on a\n * single line.\n *\n * Coordinate arrays are never hand-edited, but indenting them costs roughly\n * three bytes of whitespace for every one byte of data: a project embedding\n * 13,000 features weighed 179 MB pretty-printed and 54 MB compact\n * (GeoLibre#1829), which is the difference between reopening and running the\n * tab out of memory. The surrounding project structure stays indented so the\n * file is still readable and diffs still make sense.\n *\n * @param value Value to serialize.\n * @param depth Current nesting depth, driving the indent width.\n * @param key Property name (or stringified array index) this value sits under,\n * `\"\"` at the root — the argument `JSON.stringify` passes to `toJSON`.\n * @param ancestors Containers currently open on the recursion stack, used to\n * detect cycles.\n * @param presets Objects whose serialized text is already known at the depth\n * they sit at, written verbatim instead of being walked again. Used by\n * {@link serializeProjectWithLayerCache} to splice in cached layers.\n * @returns The serialized text, or undefined for values `JSON.stringify` also\n * drops (undefined, functions, symbols).\n */\nfunction serializeProjectValue(\n value: unknown,\n depth: number,\n key: string,\n ancestors: Set<object>,\n presets?: ReadonlyMap<object, string>,\n): string | undefined {\n if (presets !== undefined && value !== null && typeof value === \"object\") {\n const preset = presets.get(value);\n if (preset !== undefined) return preset;\n }\n if (value !== null && typeof value === \"object\") {\n // Honor the toJSON hook the way JSON.stringify does, so a value that\n // replaces itself is inspected in its replaced form. It receives the same\n // key JSON.stringify would pass, since a custom hook may branch on it.\n const toJSON = (value as { toJSON?: unknown }).toJSON;\n if (typeof toJSON === \"function\") {\n value = (toJSON as (key: string) => unknown).call(value, key);\n }\n }\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n // A boxed Number/String/Boolean writes as its primitive rather than as an\n // object, the way JSON.stringify unwraps it.\n if (value instanceof Number || value instanceof String || value instanceof Boolean) {\n return JSON.stringify(value);\n }\n // Feature data: hand the whole subtree to JSON.stringify with no spacing.\n if (isGeoJsonValue(value)) return JSON.stringify(value);\n\n // A cycle would recurse until the stack overflowed, and the RangeError that\n // raises is indistinguishable from the string-length cap the save path reads\n // as \"project too large\". Fail the way JSON.stringify does instead. Only the\n // open ancestors are tracked, so a value referenced twice side by side (not a\n // cycle) still serializes.\n if (ancestors.has(value)) throw new TypeError(\"Converting circular structure to JSON\");\n ancestors.add(value);\n try {\n const pad = PROJECT_INDENT.repeat(depth + 1);\n const closePad = PROJECT_INDENT.repeat(depth);\n if (Array.isArray(value)) {\n if (value.length === 0) return \"[]\";\n // Indexed rather than mapped so a sparse array's holes are visited: like\n // an unserializable entry, a hole becomes null, matching JSON.stringify.\n const entries = Array.from(\n { length: value.length },\n (_unused, index) =>\n serializeProjectValue(value[index], depth + 1, String(index), ancestors, presets) ??\n \"null\",\n );\n return `[\\n${pad}${entries.join(`,\\n${pad}`)}\\n${closePad}]`;\n }\n const entries: string[] = [];\n for (const [entryKey, entry] of Object.entries(value)) {\n const serialized = serializeProjectValue(entry, depth + 1, entryKey, ancestors, presets);\n // An unserializable object value is omitted, matching JSON.stringify.\n if (serialized === undefined) continue;\n entries.push(`${JSON.stringify(entryKey)}: ${serialized}`);\n }\n if (entries.length === 0) return \"{}\";\n return `{\\n${pad}${entries.join(`,\\n${pad}`)}\\n${closePad}}`;\n } finally {\n ancestors.delete(value);\n }\n}\n\n/**\n * Serialize a project to `.geolibre.json` text: indented project structure with\n * compact (unindented) embedded GeoJSON. See {@link serializeProjectValue} for\n * why the two halves are formatted differently.\n *\n * @param project Project to serialize.\n * @returns The file contents to write.\n */\nexport function serializeProject(project: GeoLibreProject): string {\n return (\n serializeProjectValue(\n { ...project, layers: project.layers.map(portableLayer) },\n 0,\n \"\",\n new Set(),\n ) ?? \"null\"\n );\n}\n\n/**\n * Per-layer serialized text kept across {@link serializeProjectWithLayerCache}\n * calls, keyed by the layer record each entry was built from. A `WeakMap`, so a\n * layer the store has replaced or removed drops its text with it.\n */\nexport type ProjectLayerSerializationCache = WeakMap<object, { index: number; text: string }>;\n\n/** Create an empty {@link ProjectLayerSerializationCache}. */\nexport function createProjectLayerSerializationCache(): ProjectLayerSerializationCache {\n return new WeakMap();\n}\n\n/**\n * Serialize a project exactly as {@link serializeProject} does, reusing the\n * text of every layer whose source record is unchanged since an earlier call.\n *\n * Feature data lives inside layer records, so re-stringifying every layer on\n * each autosave costs megabytes of main-thread work for a project with large\n * GeoJSON layers — even when only the camera moved (GeoLibre#2633). The store\n * replaces a layer object whenever it changes, so object identity is a sound\n * cache key: a camera move reuses every layer, and an edit to one layer\n * re-serializes only that one.\n *\n * `layerSources[i]` must be the record `project.layers[i]` was derived from\n * (e.g. the store's `layers`, before `projectFromStore` prepared them), and that\n * derivation must depend on the record alone — as `buildProjectSnapshot`'s does\n * — or a cached entry could outlive a change it should have reflected. When the\n * two arrays differ in length the cache is bypassed rather than trusted.\n *\n * @param project Project to serialize, as built from `layerSources`.\n * @param layerSources Immutable source record for each entry of\n * `project.layers`, in the same order, used as the cache key.\n * @param cache Cache shared between calls; see\n * {@link createProjectLayerSerializationCache}.\n * @returns The same text {@link serializeProject} returns for `project`.\n */\nexport function serializeProjectWithLayerCache(\n project: GeoLibreProject,\n layerSources: readonly object[],\n cache: ProjectLayerSerializationCache,\n): string {\n const steps = serializeProjectWithLayerCacheSteps(project, layerSources, cache);\n for (;;) {\n const step = steps.next();\n if (step.done) return step.value;\n }\n}\n\n/** Time a main-thread serialization slice may run before it yields (ms). */\nconst SERIALIZE_SLICE_MS = 8;\n\n/** Resolve on a macrotask, letting input and paint run in between slices. */\nfunction yieldToMainThread(): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, 0));\n}\n\n/**\n * {@link serializeProjectWithLayerCache} that hands the main thread back\n * between layers, so autosave on a project with several large layers is many\n * short tasks instead of one 300-400 ms freeze (GeoLibre#2869).\n *\n * The cache makes a camera move free, so the cost lands on the first autosave\n * and on edits to a big layer: each layer that misses the cache is serialized\n * in its own slice, and the thread is released once a slice has run longer than\n * a few milliseconds. A single huge layer is still one slice. The result is\n * identical to the synchronous call. `project` and `layerSources` must not be\n * mutated while this runs; the store replaces records rather than editing them,\n * so a snapshot taken from it stays valid.\n *\n * @param project Project to serialize, as built from `layerSources`.\n * @param layerSources Immutable source record for each entry of\n * `project.layers`, in the same order, used as the cache key.\n * @param cache Cache shared between calls.\n * @param yieldFn Hands the thread back; replaced in tests.\n * @returns The same text {@link serializeProject} returns for `project`.\n */\nexport async function serializeProjectWithLayerCacheAsync(\n project: GeoLibreProject,\n layerSources: readonly object[],\n cache: ProjectLayerSerializationCache,\n yieldFn: () => Promise<void> = yieldToMainThread,\n): Promise<string> {\n const steps = serializeProjectWithLayerCacheSteps(project, layerSources, cache);\n let sliceStart = Date.now();\n for (;;) {\n const step = steps.next();\n if (step.done) return step.value;\n if (Date.now() - sliceStart >= SERIALIZE_SLICE_MS) {\n await yieldFn();\n sliceStart = Date.now();\n }\n }\n}\n\n/** Shared body of the sync and async serializers; yields once per layer text built. */\nfunction* serializeProjectWithLayerCacheSteps(\n project: GeoLibreProject,\n layerSources: readonly object[],\n cache: ProjectLayerSerializationCache,\n): Generator<void, string, void> {\n if (layerSources.length !== project.layers.length) return serializeProject(project);\n const layers = project.layers.map(portableLayer);\n // Presets are keyed by layer object, so a record listed twice would get one\n // index's text in both places. The store never does that; bypass if it does.\n if (new Set(layers).size !== layers.length) return serializeProject(project);\n const presets = new Map<object, string>();\n for (let index = 0; index < layers.length; index += 1) {\n const layer = layers[index];\n const source = layerSources[index];\n const cached = cache.get(source);\n // The layer is serialized under its array index as the key (a `toJSON`\n // hook would see it), so text is reused only at the index it was built for.\n let text = cached?.index === index ? cached.text : undefined;\n if (text === undefined) {\n // Depth 2: the root object is depth 0 and its `layers` array depth 1.\n text = serializeProjectValue(layer, 2, String(index), new Set()) ?? \"null\";\n cache.set(source, { index, text });\n yield;\n }\n presets.set(layer, text);\n }\n return serializeProjectValue({ ...project, layers }, 0, \"\", new Set(), presets) ?? \"null\";\n}\n\nexport function parseProject(json: string): GeoLibreProject {\n const data = JSON.parse(json) as Partial<GeoLibreProject>;\n if (!data.version || !data.name || !data.mapView) {\n throw new Error(\"Invalid GeoLibre project: missing required fields\");\n }\n const layerGroups = normalizeLayerGroups(data.layerGroups);\n const validGroupIds = new Set(layerGroups.map((g) => g.id));\n const layers = (data.layers ?? [])\n .map(normalizeLayer)\n .map((layer) =>\n layer.groupId && !validGroupIds.has(layer.groupId) ? { ...layer, groupId: undefined } : layer,\n );\n const selectedLayerId =\n data.selectedLayerId === null\n ? null\n : typeof data.selectedLayerId === \"string\" &&\n layers.some((layer) => layer.id === data.selectedLayerId)\n ? data.selectedLayerId\n : undefined;\n const basemapStyleUrl = data.basemapStyleUrl ?? DEFAULT_BASEMAP;\n const basemapVisible = data.basemapVisible ?? true;\n const basemapOpacity = data.basemapOpacity ?? 1;\n const blankBackgroundColor = normalizeBlankBackgroundColor(data.blankBackgroundColor);\n // Secondary panes already go through normalizeMapViewState; the primary\n // camera must too so a hand-edited project cannot store an out-of-range\n // view that MapLibre would silently clamp, leaving saved state wrong.\n const mapView = normalizeMapViewState(data.mapView);\n const { mapLayout, secondaryMapViews } = resolveMapGrid(\n normalizeMapLayout(data.mapLayout),\n normalizeSecondaryMapViews(data.secondaryMapViews),\n { mapView },\n );\n const styleLibrary = normalizeStyleLibraryEntries(data.styleLibrary);\n const parsedComments = normalizeProjectComments(data.comments);\n const parsedInteraction = normalizeProjectInteraction(data.interaction);\n return {\n version: data.version,\n name: data.name,\n mapView,\n basemapStyleUrl,\n basemapVisible,\n basemapOpacity,\n blankBackgroundColor,\n layers,\n ...(selectedLayerId !== undefined ? { selectedLayerId } : {}),\n ...(layerGroups.length > 0 ? { layerGroups } : {}),\n styles: data.styles ?? {},\n preferences: normalizeProjectPreferences(data.preferences),\n plugins: normalizeProjectPlugins(data.plugins) ?? undefined,\n legend: normalizeLegendConfig(data.legend),\n printLayout: normalizePrintLayoutConfig(data.printLayout) ?? undefined,\n storymap: normalizeStoryMap(data.storymap) ?? undefined,\n models: normalizeModels(data.models) ?? undefined,\n processingHistory: normalizeProcessingHistory(data.processingHistory) ?? undefined,\n obia: normalizeObiaWorkbench(data.obia) ?? undefined,\n widgets: normalizeWidgets(data.widgets) ?? undefined,\n ...bookmarkFields(data.bookmarks, data.bookmarkGroups),\n ...(data.dashboardColumns === undefined\n ? {}\n : { dashboardColumns: normalizeDashboardColumns(data.dashboardColumns) }),\n // Only persist the grid when it is larger than a single pane, so default\n // single-map projects serialize byte-identically to before this feature.\n ...(mapLayout.rows * mapLayout.cols > 1\n ? {\n mapLayout,\n secondaryMapViews,\n ...(normalizeString(data.primaryMapLabel)\n ? { primaryMapLabel: normalizeString(data.primaryMapLabel) }\n : {}),\n }\n : {}),\n // The primary renderer is independent of the grid, so it sits outside the\n // `mapLayout` block above: a 1x1 Cesium project has no grid to persist.\n ...(normalizePrimaryRenderer(data.primaryRenderer)\n ? { primaryRenderer: normalizePrimaryRenderer(data.primaryRenderer)! }\n : {}),\n ...(styleLibrary.length > 0 ? { styleLibrary } : {}),\n ...(parsedComments.length > 0 ? { comments: parsedComments } : {}),\n ...(parsedInteraction ? { interaction: parsedInteraction } : {}),\n metadata: data.metadata ?? {},\n };\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) `layerGroups` array into valid\n * {@link LayerGroup} records, dropping entries without a usable id and\n * de-duplicating by id. Always returns an array (empty when absent).\n *\n * @param value Raw `layerGroups` value from the project JSON.\n * @returns Normalized, de-duplicated group definitions.\n */\nfunction normalizeLayerGroups(value: unknown): LayerGroup[] {\n if (!Array.isArray(value)) return [];\n const groups: LayerGroup[] = [];\n const seen = new Set<string>();\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<LayerGroup>;\n const id = typeof candidate.id === \"string\" ? candidate.id.trim() : \"\";\n if (!id || seen.has(id)) continue;\n seen.add(id);\n const opacity =\n typeof candidate.opacity === \"number\" && Number.isFinite(candidate.opacity)\n ? Math.min(Math.max(candidate.opacity, 0), 1)\n : DEFAULT_LAYER_GROUP_OPACITY;\n groups.push({\n id,\n name: typeof candidate.name === \"string\" ? candidate.name : id,\n ...(typeof candidate.parentId === \"string\" && candidate.parentId.trim()\n ? { parentId: candidate.parentId.trim() }\n : {}),\n collapsed: candidate.collapsed === true,\n visible: candidate.visible !== false,\n opacity,\n });\n }\n const ids = new Set(groups.map((group) => group.id));\n const byId = new Map(groups.map((group) => [group.id, group]));\n return groups.map((group) => {\n if (!group.parentId || !ids.has(group.parentId) || group.parentId === group.id) {\n return group.parentId ? { ...group, parentId: undefined } : group;\n }\n let id: string | undefined = group.parentId;\n const seen = new Set([group.id]);\n while (id) {\n if (seen.has(id)) return { ...group, parentId: undefined };\n seen.add(id);\n id = byId.get(id)?.parentId;\n }\n return group;\n });\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) legend config into a valid\n * {@link LegendConfig}, dropping malformed entries. Returns undefined when no\n * usable config is present so the default is applied downstream.\n */\nexport function 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 */\n/**\n * Accept the Object-Based Analysis workbench blob (`obia`) only as a plain,\n * JSON-serializable object carrying a numeric `version`. Its fields are owned\n * and validated by the desktop app, which treats anything it does not\n * recognise as absent; core only guarantees the shape survives a round trip.\n *\n * @param value Raw `obia` value from the project JSON.\n * @returns A detached copy, or `null` when absent or malformed.\n */\nexport function normalizeObiaWorkbench(value: unknown): Record<string, unknown> | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n if (typeof (value as { version?: unknown }).version !== \"number\") return null;\n try {\n const copy: unknown = JSON.parse(JSON.stringify(value));\n return copy && typeof copy === \"object\" ? (copy as Record<string, unknown>) : null;\n } catch {\n return null;\n }\n}\n\nexport function normalizeProcessingHistory(value: unknown): ProcessingRun[] | null {\n if (!Array.isArray(value)) return null;\n // Bound the work for a crafted or corrupted file (shared/collaboration\n // projects reach this path too): only the newest entries can survive the\n // cap anyway, so ignore all but a generous tail up front.\n const source =\n value.length > MAX_PROCESSING_HISTORY * 10 ? value.slice(-MAX_PROCESSING_HISTORY * 10) : value;\n const runs: ProcessingRun[] = [];\n const seen = new Set<string>();\n for (const entry of source) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<ProcessingRun>;\n const id = normalizeString(candidate.id).trim();\n let toolId = normalizeString(candidate.toolId).trim();\n const kind = candidate.kind;\n if (!id || !toolId || seen.has(id)) continue;\n if (!kind || !PROCESSING_RUN_KINDS.has(kind)) continue;\n seen.add(id);\n const inputLayerNames =\n candidate.inputLayerNames && typeof candidate.inputLayerNames === \"object\"\n ? Object.fromEntries(\n Object.entries(candidate.inputLayerNames).filter(\n ([, name]) => typeof name === \"string\",\n ),\n )\n : undefined;\n const outputLayerNames = Array.isArray(candidate.outputLayerNames)\n ? candidate.outputLayerNames.filter((name): name is string => typeof name === \"string\")\n : undefined;\n let parameters: Record<string, unknown> =\n candidate.parameters && typeof candidate.parameters === \"object\"\n ? { ...(candidate.parameters as Record<string, unknown>) }\n : {};\n const migrated = LEGACY_H3_PROCESSING_TOOL_IDS[toolId];\n if (migrated) {\n toolId = migrated;\n if (parameters.dggsType == null) parameters = { ...parameters, dggsType: \"h3\" };\n }\n runs.push({\n id,\n kind,\n toolId,\n toolName: normalizeString(candidate.toolName) || toolId,\n engine: normalizeString(candidate.engine),\n parameters,\n ...(inputLayerNames && Object.keys(inputLayerNames).length > 0 ? { inputLayerNames } : {}),\n ...(outputLayerNames?.length ? { outputLayerNames } : {}),\n ...(normalizeString(candidate.inputPath)\n ? { inputPath: normalizeString(candidate.inputPath) }\n : {}),\n ...(normalizeString(candidate.outputPath)\n ? { outputPath: normalizeString(candidate.outputPath) }\n : {}),\n startedAt: normalizeString(candidate.startedAt),\n ...(Number.isFinite(candidate.durationMs)\n ? { durationMs: Math.max(0, Number(candidate.durationMs)) }\n : {}),\n // Only an explicit \"success\" earns the green checkmark; a missing or\n // corrupted status from hand-edited JSON degrades to \"error\" rather than\n // presenting an indeterminate run as having succeeded.\n status: candidate.status === \"success\" ? \"success\" : \"error\",\n ...(normalizeString(candidate.error) ? { error: normalizeString(candidate.error) } : {}),\n });\n }\n if (runs.length === 0) return null;\n return runs.slice(-MAX_PROCESSING_HISTORY);\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) camera object into a valid\n * {@link MapViewState}, falling back to the default view for missing parts.\n */\nexport function normalizeMapViewState(value: unknown): MapViewState {\n const fallback = createDefaultMapView();\n if (!value || typeof value !== \"object\") return fallback;\n const candidate = value as Partial<MapViewState>;\n const center = Array.isArray(candidate.center) ? candidate.center : fallback.center;\n // Clamp to MapLibre's valid ranges (matching normalizeStoryChapter) so a\n // hand-edited project file can't store an out-of-range camera that jumpTo\n // would silently clamp or reject, leaving the saved state inconsistent with\n // what lands on screen. Bearing wraps into [0, 360).\n const view: MapViewState = {\n center: [\n clampCoordinate(normalizeNumber(center[0], fallback.center[0]), -180, 180),\n clampCoordinate(normalizeNumber(center[1], fallback.center[1]), -90, 90),\n ],\n zoom: clamp(normalizeNumber(candidate.zoom, fallback.zoom), 0, 24),\n bearing: ((normalizeNumber(candidate.bearing, fallback.bearing) % 360) + 360) % 360,\n pitch: clamp(normalizeNumber(candidate.pitch, fallback.pitch), 0, 85),\n };\n if (\n Array.isArray(candidate.bbox) &&\n candidate.bbox.length === 4 &&\n candidate.bbox.every((n) => Number.isFinite(n))\n ) {\n view.bbox = [\n Number(candidate.bbox[0]),\n Number(candidate.bbox[1]),\n Number(candidate.bbox[2]),\n Number(candidate.bbox[3]),\n ];\n }\n return view;\n}\n\n/**\n * Coerce an untrusted `mapLayout` into a valid {@link MapGridLayout}. Returns\n * null when absent or effectively single-pane so default projects stay\n * byte-identical (the field is only written when the grid is larger than 1x1).\n */\nexport function normalizeMapLayout(value: unknown): MapGridLayout | null {\n if (!value || typeof value !== \"object\") return null;\n const candidate = value as Partial<MapGridLayout>;\n const rows = clamp(Math.floor(normalizeNumber(candidate.rows, 1)), 1, MAX_MAP_GRID_DIM);\n const cols = clamp(Math.floor(normalizeNumber(candidate.cols, 1)), 1, MAX_MAP_GRID_DIM);\n if (rows * cols <= 1) return null;\n return {\n rows,\n cols,\n syncView: normalizeBoolean(candidate.syncView, DEFAULT_MAP_GRID_LAYOUT.syncView),\n };\n}\n\n/**\n * Coerce an untrusted `primaryRenderer` into a known engine id (issue #2217).\n *\n * Returns null for the default 2D map — absent, unknown, or an explicit\n * `\"maplibre\"` — because the field is only written when it is not the default,\n * so a MapLibre project serializes byte-identically to before this existed.\n * The return type is narrowed to `\"cesium\" | \"mapbox\" | \"arcgis\" | null` rather\n * than the full {@link MapRendererKind} for that reason: `\"maplibre\"` is never a\n * result.\n */\nexport function normalizePrimaryRenderer(value: unknown): \"cesium\" | \"mapbox\" | \"arcgis\" | null {\n return value === \"cesium\" || value === \"mapbox\" || value === \"arcgis\" ? value : null;\n}\n\n/**\n * Coerce an untrusted `secondaryMapViews` array into valid\n * {@link SecondaryMapView} records, dropping entries without a usable id and\n * de-duplicating by id. Returns null when none are valid.\n */\nexport function normalizeSecondaryMapViews(value: unknown): SecondaryMapView[] | null {\n if (!Array.isArray(value)) return null;\n const views: SecondaryMapView[] = [];\n const seen = new Set<string>();\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<SecondaryMapView>;\n const id = normalizeString(candidate.id).trim();\n if (!id || seen.has(id)) continue;\n seen.add(id);\n const label = normalizeString(candidate.label);\n // Only the known engine ids survive; an absent/unknown value is omitted so\n // the pane defaults to the 2D map (back-compat with pre-globe projects).\n /* eslint-disable local/no-renderer-kind-checks -- validates a renderer name */\n const viewKind =\n candidate.viewKind === \"cesium\" ||\n candidate.viewKind === \"maplibre\" ||\n candidate.viewKind === \"mapbox\" ||\n candidate.viewKind === \"arcgis\"\n ? candidate.viewKind\n : undefined;\n /* eslint-enable local/no-renderer-kind-checks */\n views.push({\n id,\n view: normalizeMapViewState(candidate.view),\n ...(label ? { label } : {}),\n ...(viewKind ? { viewKind } : {}),\n layerVisibility: normalizeLayerVisibility(candidate.layerVisibility),\n });\n }\n return views.length > 0 ? views : null;\n}\n\n/** Coerce an untrusted per-layer visibility map into `Record<string, boolean>`. */\nfunction normalizeLayerVisibility(value: unknown): Record<string, boolean> {\n if (!value || typeof value !== \"object\") return {};\n const result: Record<string, boolean> = {};\n for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {\n if (typeof raw === \"boolean\") result[key] = raw;\n }\n return result;\n}\n\n/**\n * Reconcile a parsed grid layout with its secondary panes so the store invariant\n * holds: `secondaryMapViews.length === rows * cols - 1`. Surplus panes are\n * dropped; missing panes are filled by cloning the primary map. A null/absent\n * layout (or a 1x1 grid) collapses to the single-map default.\n */\nexport function resolveMapGrid(\n layout: MapGridLayout | null,\n secondaryViews: SecondaryMapView[] | null,\n primary: { mapView: MapViewState },\n): { mapLayout: MapGridLayout; secondaryMapViews: SecondaryMapView[] } {\n if (!layout) {\n return { mapLayout: { ...DEFAULT_MAP_GRID_LAYOUT }, secondaryMapViews: [] };\n }\n const desired = layout.rows * layout.cols - 1;\n let views = secondaryViews ?? [];\n if (views.length > desired) {\n views = views.slice(0, desired);\n } else if (views.length < desired) {\n const seen = new Set(views.map((v) => v.id));\n const additions: SecondaryMapView[] = [];\n for (let i = views.length; i < desired; i++) {\n let id = `secondary-${i}`;\n // Append a counter (rather than growing the string) so a crafted file\n // with colliding ids resolves in O(1) per attempt instead of O(n).\n let suffix = 0;\n while (seen.has(id)) id = `secondary-${i}-${++suffix}`;\n seen.add(id);\n additions.push({\n id,\n view: { ...primary.mapView },\n layerVisibility: {},\n });\n }\n views = [...views, ...additions];\n }\n return { mapLayout: layout, secondaryMapViews: views };\n}\n\n/** A 3- or 6-digit hex color, the only widget color format we persist. */\nconst HEX_COLOR = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n\n/** Upper bound for a persisted histogram bin count, mirroring the chart\n * renderer's clamp (`MAX_HISTOGRAM_BINS` in the desktop app's chart helpers). */\nconst MAX_PERSISTED_BINS = 50;\n\n/** Upper bound for a persisted list-widget row limit, mirroring the widget\n * editor's row-count input (`max={500}` in `WidgetEditorDialog`). */\nconst MAX_PERSISTED_LIST_ROWS = 500;\n\n// Spelled as a Record so adding a member to DashboardWidgetType fails to\n// compile until it is listed here. A plain array accepted a short list\n// silently, and a type missing from it makes normalizeWidgets drop every widget\n// of that type — which is how selector widgets vanished on save and reload.\nconst DASHBOARD_WIDGET_TYPES = Object.keys({\n histogram: true,\n scatter: true,\n bar: true,\n line: true,\n box: true,\n pie: true,\n indicator: true,\n selector: true,\n list: true,\n} satisfies Record<DashboardWidgetType, true>) as readonly DashboardWidgetType[];\nconst DASHBOARD_WIDGET_AGGREGATIONS: readonly DashboardWidgetAggregation[] = [\n \"count\",\n \"sum\",\n \"mean\",\n];\nconst INDICATOR_AGGREGATIONS: readonly IndicatorAggregation[] = [\n \"count\",\n \"sum\",\n \"mean\",\n \"min\",\n \"max\",\n \"median\",\n];\n\n/** Bookmarks kept per project, as the panel's own default limit allows. */\nexport const MAX_PROJECT_BOOKMARKS = 500;\n\n/**\n * Coerce an untrusted `bookmarkGroups` array into valid folders: drops entries\n * without a string id and de-duplicates by id. A missing name becomes\n * \"Folder\"; `collapsed` defaults to false.\n *\n * @param value Raw `bookmarkGroups` value from the project JSON.\n * @returns The folders (possibly empty).\n */\nexport function normalizeBookmarkGroups(value: unknown): ProjectBookmarkGroup[] {\n if (!Array.isArray(value)) return [];\n const groups: ProjectBookmarkGroup[] = [];\n const seen = new Set<string>();\n for (const raw of value) {\n if (!raw || typeof raw !== \"object\") continue;\n const entry = raw as Record<string, unknown>;\n const id = typeof entry.id === \"string\" ? entry.id.trim() : \"\";\n if (!id || seen.has(id)) continue;\n seen.add(id);\n groups.push({\n id,\n name: typeof entry.name === \"string\" && entry.name.trim() ? entry.name : \"Folder\",\n collapsed: entry.collapsed === true,\n });\n }\n return groups;\n}\n\n/**\n * Coerce an untrusted `bookmarks` array into valid saved views: drops entries\n * without a string id or a finite, in-range camera, de-duplicates by id, wraps\n * the bearing into 0-360, and drops a `groupId` that names no folder in\n * `groups`. Keeps at most {@link MAX_PROJECT_BOOKMARKS}.\n *\n * @param value Raw `bookmarks` value from the project JSON.\n * @param groups The project's (normalized) folders.\n * @returns The bookmarks (possibly empty).\n */\nexport function normalizeBookmarks(\n value: unknown,\n groups: readonly ProjectBookmarkGroup[] = [],\n): ProjectBookmark[] {\n if (!Array.isArray(value)) return [];\n const groupIds = new Set(groups.map((group) => group.id));\n const bookmarks: ProjectBookmark[] = [];\n const seen = new Set<string>();\n const finite = (input: unknown): number | null =>\n typeof input === \"number\" && Number.isFinite(input) ? input : null;\n for (const raw of value) {\n if (bookmarks.length >= MAX_PROJECT_BOOKMARKS) break;\n if (!raw || typeof raw !== \"object\") continue;\n const entry = raw as Record<string, unknown>;\n const id = typeof entry.id === \"string\" ? entry.id.trim() : \"\";\n const lng = finite(entry.lng);\n const lat = finite(entry.lat);\n const zoom = finite(entry.zoom);\n if (!id || seen.has(id) || lng === null || lat === null || zoom === null) continue;\n if (lng < -180 || lng > 180 || lat < -90 || lat > 90 || zoom < 0 || zoom > 24) continue;\n seen.add(id);\n const pitch = Math.min(85, Math.max(0, finite(entry.pitch) ?? 0));\n const bearing = (((finite(entry.bearing) ?? 0) % 360) + 360) % 360;\n const bookmark: ProjectBookmark = {\n id,\n name: typeof entry.name === \"string\" ? entry.name : \"\",\n lng,\n lat,\n zoom,\n pitch,\n bearing,\n createdAt: finite(entry.createdAt) ?? 0,\n };\n // Folder ids are trimmed, so compare a trimmed groupId with them.\n const groupId = typeof entry.groupId === \"string\" ? entry.groupId.trim() : \"\";\n if (groupId && groupIds.has(groupId)) bookmark.groupId = groupId;\n if (entry.extra && typeof entry.extra === \"object\" && !Array.isArray(entry.extra)) {\n bookmark.extra = entry.extra as Record<string, unknown>;\n }\n bookmarks.push(bookmark);\n }\n return bookmarks;\n}\n\n/**\n * The `bookmarks`/`bookmarkGroups` project fields for normalized input, each\n * omitted when empty so a bookmark-less project stays free of the keys.\n */\nfunction bookmarkFields(\n rawBookmarks: unknown,\n rawGroups: unknown,\n): { bookmarks?: ProjectBookmark[]; bookmarkGroups?: ProjectBookmarkGroup[] } {\n const bookmarkGroups = normalizeBookmarkGroups(rawGroups);\n const bookmarks = normalizeBookmarks(rawBookmarks, bookmarkGroups);\n return {\n ...(bookmarks.length > 0 ? { bookmarks } : {}),\n ...(bookmarkGroups.length > 0 ? { bookmarkGroups } : {}),\n };\n}\n\n/**\n * Coerce an untrusted (possibly hand-edited) `widgets` array into valid\n * {@link DashboardWidget} records. Drops widgets without a usable id, layer id,\n * or recognized chart type, de-duplicates by id, and keeps only the optional\n * keys that are present and well-typed (the Dashboard panel falls back to\n * sensible defaults for anything missing). Returns `null` when there is nothing\n * worth persisting, so a widget-less project stays free of the key.\n *\n * @param value Raw `widgets` value from the project JSON.\n * @returns Normalized widgets, or `null` when none survive.\n */\nexport function normalizeWidgets(value: unknown): DashboardWidget[] | null {\n if (!Array.isArray(value)) return null;\n const widgets: DashboardWidget[] = [];\n const seen = new Set<string>();\n for (const entry of value) {\n if (!entry || typeof entry !== \"object\") continue;\n const candidate = entry as Partial<DashboardWidget>;\n const id = normalizeString(candidate.id).trim();\n const layerId = normalizeString(candidate.layerId).trim();\n if (!id || !layerId || seen.has(id)) continue;\n const type = candidate.type;\n if (!type || !DASHBOARD_WIDGET_TYPES.includes(type)) continue;\n seen.add(id);\n const widget: DashboardWidget = { id, layerId, type };\n const title = normalizeString(candidate.title).trim();\n if (title) widget.title = title;\n const color = normalizeString(candidate.color).trim();\n if (HEX_COLOR.test(color)) widget.color = color;\n const field = normalizeString(candidate.field).trim();\n if (field) widget.field = field;\n const xField = normalizeString(candidate.xField).trim();\n if (xField) widget.xField = xField;\n const yField = normalizeString(candidate.yField).trim();\n if (yField) widget.yField = yField;\n if (typeof candidate.bins === \"number\" && Number.isFinite(candidate.bins)) {\n // Persist only a sane positive bin count; the histogram renderer clamps to\n // [1, 50], so mirror that here rather than round-tripping 0 or huge values.\n const bins = Math.trunc(candidate.bins);\n if (bins >= 1) widget.bins = Math.min(MAX_PERSISTED_BINS, bins);\n }\n const category = normalizeString(candidate.category).trim();\n if (category) widget.category = category;\n if (\n candidate.aggregation &&\n DASHBOARD_WIDGET_AGGREGATIONS.includes(candidate.aggregation) &&\n // A pie has no \"average\"; the renderer would silently treat mean as sum,\n // so drop it here and let the default (count) stand for hand-edited files.\n !(type === \"pie\" && candidate.aggregation === \"mean\")\n ) {\n widget.aggregation = candidate.aggregation;\n }\n const valueField = normalizeString(candidate.valueField).trim();\n if (valueField) widget.valueField = valueField;\n // Indicator widget fields (issue #1381). Only an indicator reads them, so\n // drop them elsewhere rather than round-tripping dead configuration.\n if (type === \"indicator\") {\n if (\n candidate.indicatorAggregation &&\n INDICATOR_AGGREGATIONS.includes(candidate.indicatorAggregation)\n ) {\n widget.indicatorAggregation = candidate.indicatorAggregation;\n }\n // Prefix/suffix are not trimmed: a leading/trailing space is intentional\n // (e.g. \" ha\" or \"$ \").\n const prefix = normalizeString(candidate.prefix);\n if (prefix) widget.prefix = prefix;\n const suffix = normalizeString(candidate.suffix);\n if (suffix) widget.suffix = suffix;\n }\n // Selector widget fields (issue #1381). Only a selector reads the flag, and\n // false is the default, so persist it only when it is on.\n if (type === \"selector\" && candidate.multiple === true) {\n widget.multiple = true;\n }\n // List widget fields (issue #1381). normalizeWidgets also runs on the save\n // path (projectFromStore), so dropping these would blank a list widget the\n // moment its project is saved — the renderer falls back to \"no data\"\n // without listFields.\n if (type === \"list\") {\n if (Array.isArray(candidate.listFields)) {\n const listFields = candidate.listFields\n .map((entry) => normalizeString(entry).trim())\n .filter((entry) => entry !== \"\");\n if (listFields.length > 0) widget.listFields = listFields;\n }\n const sortBy = normalizeString(candidate.sortBy).trim();\n if (sortBy) widget.sortBy = sortBy;\n if (candidate.sortDir === \"asc\" || candidate.sortDir === \"desc\") {\n widget.sortDir = candidate.sortDir;\n }\n if (typeof candidate.limit === \"number\" && Number.isFinite(candidate.limit)) {\n // Clamp to the editor's range so a hand-edited 0 or 10_000 cannot reach\n // the renderer.\n const limit = Math.trunc(candidate.limit);\n if (limit >= 1) widget.limit = Math.min(MAX_PERSISTED_LIST_ROWS, limit);\n }\n }\n widgets.push(widget);\n }\n return widgets.length > 0 ? widgets : null;\n}\n\n/**\n * Clamp an untrusted dashboard column count into the supported range, falling\n * back to the default for a missing or non-finite value.\n *\n * @param value Raw `dashboardColumns` value from the project JSON.\n * @returns An integer column count within [MIN, MAX].\n */\nexport function normalizeDashboardColumns(value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n return DEFAULT_DASHBOARD_COLUMNS;\n }\n return Math.max(MIN_DASHBOARD_COLUMNS, Math.min(MAX_DASHBOARD_COLUMNS, Math.trunc(value)));\n}\n\n/**\n * Coerce an untrusted `interaction` block into a {@link ProjectInteraction}.\n *\n * Layer ids are kept even when no such layer exists: the block is replayed\n * against whatever layers the project has when it opens, and the loader drops\n * the ones it cannot find. Control names are kept as written for the same\n * reason; the app ignores names it does not know.\n *\n * @param value - Raw `interaction` value from the project JSON or the store.\n * @returns The normalized block, or null when it carries nothing to apply.\n */\nexport function normalizeProjectInteraction(value: unknown): ProjectInteraction | null {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return null;\n const raw = value as { identify?: unknown; controls?: unknown };\n const result: ProjectInteraction = {};\n if (raw.identify === null) {\n result.identify = null;\n } else if (typeof raw.identify === \"string\" && raw.identify) {\n result.identify = raw.identify;\n } else if (Array.isArray(raw.identify)) {\n const ids = [\n ...new Set(raw.identify.filter((id): id is string => typeof id === \"string\" && !!id)),\n ];\n if (ids.length > 0) result.identify = ids;\n }\n if (raw.controls && typeof raw.controls === \"object\" && !Array.isArray(raw.controls)) {\n const controls: Record<string, boolean> = {};\n for (const [name, shown] of Object.entries(raw.controls)) {\n if (name && typeof shown === \"boolean\") controls[name] = shown;\n }\n if (Object.keys(controls).length > 0) result.controls = controls;\n }\n return \"identify\" in result || result.controls ? result : null;\n}\n\nfunction normalizeProjectPreferences(preferences: unknown): ProjectPreferences {\n if (!preferences || typeof preferences !== \"object\") {\n return DEFAULT_PROJECT_PREFERENCES;\n }\n\n const candidate = preferences as Partial<ProjectPreferences>;\n const map = candidate.map ?? {};\n // Every MapPreferences field is normalized explicitly below, so the map\n // object is not spread in: that would forward unknown keys from a\n // hand-edited project file straight into app state.\n return {\n map: {\n ...DEFAULT_PROJECT_PREFERENCES.map,\n bounds: normalizeBounds((map as Partial<ProjectPreferences[\"map\"]>).bounds),\n minZoom: normalizeNumber(\n (map as Partial<ProjectPreferences[\"map\"]>).minZoom,\n DEFAULT_PROJECT_PREFERENCES.map.minZoom,\n ),\n maxZoom: normalizeNumber(\n (map as Partial<ProjectPreferences[\"map\"]>).maxZoom,\n DEFAULT_PROJECT_PREFERENCES.map.maxZoom,\n ),\n maxPitch: normalizeNumber(\n (map as Partial<ProjectPreferences[\"map\"]>).maxPitch,\n DEFAULT_PROJECT_PREFERENCES.map.maxPitch,\n ),\n restrictBounds: Boolean((map as Partial<ProjectPreferences[\"map\"]>).restrictBounds),\n renderWorldCopies: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).renderWorldCopies,\n true,\n ),\n projection:\n (map as Partial<ProjectPreferences[\"map\"]>).projection === \"mercator\"\n ? \"mercator\"\n : \"globe\",\n // Coerce unknown/missing bodies to Earth so measurements never break.\n ellipsoidId: getEllipsoid((map as Partial<ProjectPreferences[\"map\"]>).ellipsoidId).id,\n scaleUnit: normalizeScaleUnit((map as Partial<ProjectPreferences[\"map\"]>).scaleUnit),\n // Absent in every project written before #1813, and the default is off,\n // so an older project opens without the elevation lookup enabled.\n showPointerElevation: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).showPointerElevation,\n DEFAULT_PROJECT_PREFERENCES.map.showPointerElevation,\n ),\n // Missing means follow the saved project basemap. Do not reapply the\n // new-project Streets default after a user has selected a shared style.\n mapboxStyleUrl:\n normalizeString((map as Partial<ProjectPreferences[\"map\"]>).mapboxStyleUrl) || undefined,\n arcgisBasemap:\n normalizeString((map as Partial<ProjectPreferences[\"map\"]>).arcgisBasemap) || undefined,\n // Missing (every project before #2708) or Web Mercator is the default map.\n arcgisWkid: normalizeArcgisWkid((map as Partial<ProjectPreferences[\"map\"]>).arcgisWkid),\n // Missing means follow the saved project basemap, as it does for\n // `mapboxStyleUrl` above: a project written before this field existed\n // chose nothing, and reapplying the new-project default would repaint\n // its globe with Ion imagery the next time it opened. New projects get\n // the default from `DEFAULT_PROJECT_PREFERENCES` and save it explicitly.\n cesiumBasemap: normalizeCesiumBasemap(\n (map as Partial<ProjectPreferences[\"map\"]>).cesiumBasemap,\n ),\n // Older projects omit this field and continue to open with terrain off.\n terrainEnabled: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).terrainEnabled,\n DEFAULT_PROJECT_PREFERENCES.map.terrainEnabled,\n ),\n terrainIonAssetId:\n parseCesiumIonAssetId((map as Partial<ProjectPreferences[\"map\"]>).terrainIonAssetId) ??\n undefined,\n // Kept as a free string here; the app coerces an unknown notation to\n // decimal degrees when it renders, so a hand-edited project cannot break\n // the readout.\n coordinateFormat:\n typeof (map as Partial<ProjectPreferences[\"map\"]>).coordinateFormat === \"string\"\n ? ((map as Partial<ProjectPreferences[\"map\"]>).coordinateFormat as string)\n : DEFAULT_PROJECT_PREFERENCES.map.coordinateFormat,\n // Absent in projects written before the EPSG readout existed; only a\n // positive integer survives, so a hand-edited value cannot reach proj4.\n coordinateEpsgCode: normalizeEpsgCode(\n (map as Partial<ProjectPreferences[\"map\"]>).coordinateEpsgCode,\n ),\n // Older projects omit this field and keep fitting to newly added data.\n zoomToNewLayers: normalizeBoolean(\n (map as Partial<ProjectPreferences[\"map\"]>).zoomToNewLayers,\n DEFAULT_PROJECT_PREFERENCES.map.zoomToNewLayers,\n ),\n },\n environmentVariables: Array.isArray(candidate.environmentVariables)\n ? candidate.environmentVariables\n .map(normalizeEnvironmentVariable)\n .filter((variable): variable is RuntimeEnvironmentVariable => Boolean(variable))\n : [],\n geocoding: normalizeGeocodingPreferences(candidate.geocoding),\n };\n}\n\n/**\n * Keep a stored EPSG code only when it is a positive integer.\n *\n * Args:\n * value: The stored value, of unknown shape.\n *\n * Returns:\n * The code, or undefined for anything else.\n */\nfunction normalizeEpsgCode(value: unknown): number | undefined {\n return typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\nfunction normalizeGeocodingPreferences(geocoding: unknown): ProjectPreferences[\"geocoding\"] {\n if (!geocoding || typeof geocoding !== \"object\") {\n return { ...DEFAULT_PROJECT_PREFERENCES.geocoding, apiKeys: {} };\n }\n const candidate = geocoding as Partial<ProjectPreferences[\"geocoding\"]>;\n const apiKeys: Record<string, string> = {};\n if (candidate.apiKeys && typeof candidate.apiKeys === \"object\") {\n for (const [key, value] of Object.entries(candidate.apiKeys)) {\n const normalizedKey = key.trim();\n if (normalizedKey && typeof value === \"string\") {\n apiKeys[normalizedKey] = value;\n }\n }\n }\n return {\n providerId:\n typeof candidate.providerId === \"string\" && candidate.providerId.trim()\n ? candidate.providerId.trim()\n : DEFAULT_PROJECT_PREFERENCES.geocoding.providerId,\n apiKeys,\n forwardEndpoint:\n typeof candidate.forwardEndpoint === \"string\" && candidate.forwardEndpoint.trim()\n ? candidate.forwardEndpoint.trim()\n : undefined,\n reverseEndpoint:\n typeof candidate.reverseEndpoint === \"string\" && candidate.reverseEndpoint.trim()\n ? candidate.reverseEndpoint.trim()\n : undefined,\n email:\n typeof candidate.email === \"string\" && candidate.email.trim()\n ? candidate.email.trim()\n : undefined,\n };\n}\n\n/** Coerce an unknown value to a supported scale unit, defaulting to metric. */\nfunction normalizeScaleUnit(value: unknown): MapScaleUnit {\n return value === \"imperial\" || value === \"nautical\" ? value : \"metric\";\n}\n\nfunction normalizeBounds(bounds: unknown): ProjectPreferences[\"map\"][\"bounds\"] {\n if (\n Array.isArray(bounds) &&\n bounds.length === 4 &&\n bounds.every((value) => Number.isFinite(value))\n ) {\n // Clamp to valid lng/lat ranges so the stored bounds match what the map\n // controller applies, then keep the ordering check so an empty or\n // inverted region falls back to the default instead of being persisted.\n const west = clampCoordinate(Number(bounds[0]), -180, 180);\n const south = clampCoordinate(Number(bounds[1]), -85, 85);\n const east = clampCoordinate(Number(bounds[2]), -180, 180);\n const north = clampCoordinate(Number(bounds[3]), -85, 85);\n if (west < east && south < north) {\n return [west, south, east, north];\n }\n }\n\n return DEFAULT_PROJECT_PREFERENCES.map.bounds;\n}\n\nfunction normalizeNumber(value: unknown, fallback: number): number {\n return Number.isFinite(value) ? Number(value) : fallback;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction clampCoordinate(value: number, min: number, max: number): number {\n return clamp(value, min, max);\n}\n\nfunction normalizeBoolean(value: unknown, fallback: boolean): boolean {\n return typeof value === \"boolean\" ? value : fallback;\n}\n\nconst ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nfunction normalizeEnvironmentVariable(variable: unknown): RuntimeEnvironmentVariable | null {\n if (!variable || typeof variable !== \"object\") return null;\n const candidate = variable as Partial<RuntimeEnvironmentVariable>;\n const key = typeof candidate.key === \"string\" ? candidate.key.trim() : \"\";\n if (!key || !ENVIRONMENT_VARIABLE_NAME_PATTERN.test(key)) return null;\n\n return {\n key,\n value: typeof candidate.value === \"string\" ? candidate.value : \"\",\n enabled: normalizeBoolean(candidate.enabled, true),\n ...(candidate.secret === false ? { secret: false } : {}),\n };\n}\n\nconst PROJECT_PLUGIN_CONTROL_POSITIONS = new Set<ProjectPluginControlPosition>([\n \"top-left\",\n \"top-right\",\n \"bottom-left\",\n \"bottom-right\",\n]);\n\nfunction normalizeProjectPlugins(plugins: unknown): ProjectPluginState | null {\n if (!plugins || typeof plugins !== \"object\") return null;\n\n const candidate = plugins as Partial<ProjectPluginState>;\n const manifestUrls = Array.isArray(candidate.manifestUrls)\n ? uniqueStrings(candidate.manifestUrls).filter(isAllowedPluginManifestUrl)\n : [];\n const activePluginIds = Array.isArray(candidate.activePluginIds)\n ? uniqueStrings(candidate.activePluginIds)\n : [];\n const mapControlPositions: Record<string, ProjectPluginControlPosition> = {};\n const settings: Record<string, unknown> = {};\n\n if (candidate.mapControlPositions && typeof candidate.mapControlPositions === \"object\") {\n for (const [pluginId, position] of Object.entries(candidate.mapControlPositions)) {\n if (\n typeof pluginId === \"string\" &&\n pluginId.trim() &&\n PROJECT_PLUGIN_CONTROL_POSITIONS.has(position as ProjectPluginControlPosition)\n ) {\n mapControlPositions[pluginId.trim()] = position as ProjectPluginControlPosition;\n }\n }\n }\n\n if (candidate.settings && typeof candidate.settings === \"object\") {\n for (const [pluginId, value] of Object.entries(candidate.settings)) {\n if (typeof pluginId === \"string\" && pluginId.trim() && isJsonCompatible(value)) {\n settings[pluginId.trim()] = value;\n }\n }\n }\n\n return {\n manifestUrls,\n activePluginIds,\n mapControlPositions,\n settings,\n };\n}\n\n// Plugin manifest URLs lead to fetched and executed code, so both the\n// Settings dialog and project-file loading enforce the same scheme rule:\n// HTTPS, or HTTP on a loopback host for local development.\nexport function isAllowedPluginManifestUrl(url: string): boolean {\n try {\n const { protocol, hostname } = new URL(url);\n return (\n protocol === \"https:\" ||\n (protocol === \"http:\" && [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(hostname))\n );\n } catch {\n return false;\n }\n}\n\nfunction uniqueStrings(values: unknown[]): string[] {\n const seen = new Set<string>();\n const normalized: string[] = [];\n\n for (const value of values) {\n if (typeof value !== \"string\") continue;\n const id = value.trim();\n if (!id || seen.has(id)) continue;\n seen.add(id);\n normalized.push(id);\n }\n\n return normalized;\n}\n\nfunction isJsonCompatible(value: unknown): boolean {\n if (value === null) return true;\n\n switch (typeof value) {\n case \"string\":\n case \"boolean\":\n return true;\n case \"number\":\n return Number.isFinite(value);\n case \"object\":\n if (Array.isArray(value)) return value.every(isJsonCompatible);\n if (!isPlainObject(value)) return false;\n return Object.values(value).every(isJsonCompatible);\n default:\n return false;\n }\n}\n\nfunction isPlainObject(value: object): boolean {\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n}\n\n/** Browser byte URLs belong to the live session, never a saved project. */\nfunction withoutLocalRasterBytes(layer: GeoLibreLayer): GeoLibreLayer {\n if (layer.metadata?.localBytesUrl === undefined) return layer;\n const { localBytesUrl: _localBytesUrl, ...metadata } = layer.metadata;\n return { ...layer, metadata };\n}\n\n/**\n * Drop a Zarr layer's request headers. They authenticate the store (a bearer\n * token, an API key), so they are credentials: the Zarr adds keep them in a\n * session-only map the renderer reads (`registerZarrHeaders` in\n * @geolibre/map), but a project saved before that change carries them on\n * `source` (opengeos/GeoLibre#2643). Not applied on parse, so such a project\n * still authenticates for the session it is opened in.\n *\n * @param layer - Any layer.\n * @returns The layer without `source.headers` when it is a Zarr layer, else\n * the same object.\n */\nfunction withoutZarrHeaders(layer: GeoLibreLayer): GeoLibreLayer {\n if (layer.type !== \"zarr\" || layer.source?.headers === undefined) return layer;\n const { headers: _headers, ...source } = layer.source;\n return { ...layer, source };\n}\n\n/**\n * Strip the session-only state every serializer must leave out, whether or not\n * the project came through `projectFromStore`.\n *\n * @param layer - Any layer.\n * @returns The layer as it may be written to a project file.\n */\nfunction portableLayer(layer: GeoLibreLayer): GeoLibreLayer {\n return withoutZarrHeaders(withoutLocalRasterBytes(layer));\n}\n\nfunction normalizeLayer(layer: GeoLibreLayer): GeoLibreLayer {\n layer = withoutLocalRasterBytes(layer);\n // `capabilities` is split off the spread rather than overwritten: a raw value\n // that normalizes to nothing (`{}`, an array, a string, an object with no\n // boolean flag) must not survive into the normalized layer and be written\n // back out on the next save.\n const {\n capabilities: rawCapabilities,\n filterExpression: rawFilterExpression,\n descriptiveMetadata: rawDescriptiveMetadata,\n ...rest\n } = layer;\n const capabilities = normalizeLayerCapabilities(rawCapabilities);\n // Same split for the user-authored catalog metadata: a block that cleans to\n // nothing (every field blank) is dropped rather than round-tripped.\n const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);\n const filterExpression =\n Array.isArray(rawFilterExpression) &&\n rawFilterExpression.length > 0 &&\n validateMapExpression(JSON.stringify(rawFilterExpression), { expectedType: \"boolean\" }).ok\n ? rawFilterExpression\n : undefined;\n return {\n ...rest,\n style: { ...DEFAULT_LAYER_STYLE, ...layer.style },\n visible: layer.visible ?? true,\n opacity: layer.opacity ?? 1,\n metadata: layer.metadata ?? {},\n source: layer.source ?? {},\n ...(capabilities ? { capabilities } : {}),\n ...(filterExpression ? { filterExpression } : {}),\n ...(descriptiveMetadata ? { descriptiveMetadata } : {}),\n };\n}\n\nexport function normalizeProjectComments(rawComments: unknown): ProjectComment[] {\n if (!Array.isArray(rawComments)) return [];\n const result: ProjectComment[] = [];\n for (const item of rawComments) {\n if (!item || typeof item !== \"object\") continue;\n const c = item as Record<string, unknown>;\n if (typeof c.id !== \"string\" || !c.id) continue;\n\n if (!c.anchor || typeof c.anchor !== \"object\") continue;\n const anchorObj = c.anchor as Record<string, unknown>;\n let anchor: CommentAnchor;\n if (\n anchorObj.type === \"point\" &&\n Array.isArray(anchorObj.lngLat) &&\n anchorObj.lngLat.length === 2 &&\n typeof anchorObj.lngLat[0] === \"number\" &&\n typeof anchorObj.lngLat[1] === \"number\"\n ) {\n anchor = { type: \"point\", lngLat: [anchorObj.lngLat[0], anchorObj.lngLat[1]] };\n } else if (\n anchorObj.type === \"feature\" &&\n typeof anchorObj.layerId === \"string\" &&\n (typeof anchorObj.featureId === \"string\" || typeof anchorObj.featureId === \"number\")\n ) {\n const featLngLat =\n Array.isArray(anchorObj.lngLat) &&\n anchorObj.lngLat.length === 2 &&\n typeof anchorObj.lngLat[0] === \"number\" &&\n typeof anchorObj.lngLat[1] === \"number\"\n ? ([anchorObj.lngLat[0], anchorObj.lngLat[1]] as [number, number])\n : undefined;\n anchor = {\n type: \"feature\",\n layerId: anchorObj.layerId,\n featureId: anchorObj.featureId,\n ...(featLngLat ? { lngLat: featLngLat } : {}),\n };\n } else {\n continue;\n }\n\n const authorObj =\n c.author && typeof c.author === \"object\" ? (c.author as Record<string, unknown>) : {};\n const HEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n const author: CommentAuthor = {\n name:\n typeof authorObj.name === \"string\" && authorObj.name.trim()\n ? authorObj.name.trim()\n : \"Anonymous\",\n color:\n typeof authorObj.color === \"string\" && HEX.test(authorObj.color.trim())\n ? authorObj.color.trim()\n : \"#3b82f6\",\n };\n\n const replies: CommentReply[] = [];\n if (Array.isArray(c.replies)) {\n for (const rItem of c.replies) {\n if (!rItem || typeof rItem !== \"object\") continue;\n const r = rItem as Record<string, unknown>;\n if (typeof r.id !== \"string\" || !r.id) continue;\n const rAuthorObj =\n r.author && typeof r.author === \"object\" ? (r.author as Record<string, unknown>) : {};\n replies.push({\n id: r.id,\n author: {\n name:\n typeof rAuthorObj.name === \"string\" && rAuthorObj.name.trim()\n ? rAuthorObj.name.trim()\n : \"Anonymous\",\n color:\n typeof rAuthorObj.color === \"string\" && HEX.test(rAuthorObj.color.trim())\n ? rAuthorObj.color.trim()\n : \"#3b82f6\",\n },\n body: typeof r.body === \"string\" ? r.body : \"\",\n createdAt: typeof r.createdAt === \"string\" ? r.createdAt : new Date().toISOString(),\n });\n }\n }\n\n result.push({\n id: c.id,\n anchor,\n author,\n body: typeof c.body === \"string\" ? c.body : \"\",\n createdAt: typeof c.createdAt === \"string\" ? c.createdAt : new Date().toISOString(),\n resolved: Boolean(c.resolved),\n replies,\n });\n }\n return result;\n}\n\nexport function projectFromStore(state: {\n projectName: string;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n blankBackgroundColor?: string | null;\n layers: GeoLibreLayer[];\n selectedLayerId?: string | null;\n layerGroups?: LayerGroup[];\n preferences: ProjectPreferences;\n plugins?: ProjectPluginState | null;\n legend?: LegendConfig | null;\n printLayout?: PrintLayoutConfig | null;\n storymap?: StoryMap | null;\n models?: ProcessingModel[] | null;\n processingHistory?: ProcessingRun[] | null;\n obia?: Record<string, unknown> | null;\n widgets?: DashboardWidget[] | null;\n bookmarks?: ProjectBookmark[] | null;\n bookmarkGroups?: ProjectBookmarkGroup[] | null;\n dashboardColumns?: number;\n mapLayout?: MapGridLayout;\n secondaryMapViews?: SecondaryMapView[];\n primaryMapLabel?: string;\n primaryRenderer?: MapRendererKind;\n /** Project-scoped Style Manager entries (the store's `projectStyleLibrary`). */\n styleLibrary?: StyleLibraryEntry[] | null;\n comments?: ProjectComment[] | null;\n /** Startup interaction state loaded with the project, written back as-is. */\n interaction?: ProjectInteraction | null;\n metadata: Record<string, unknown>;\n}): GeoLibreProject {\n const layers = state.layers.filter((layer) => !isSessionOnlyLayer(layer));\n const styles: Record<string, LayerStyle> = {};\n for (const layer of 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 obia = normalizeObiaWorkbench(state.obia);\n const widgets = normalizeWidgets(state.widgets);\n const comments = normalizeProjectComments(state.comments);\n const interaction = normalizeProjectInteraction(state.interaction);\n // Persist a non-default column count only; a default-layout dashboard (or a\n // widget-less project) stays free of the key for legacy readers.\n const dashboardColumns =\n state.dashboardColumns === undefined\n ? DEFAULT_DASHBOARD_COLUMNS\n : normalizeDashboardColumns(state.dashboardColumns);\n // Persist every group (including empty folders, which the UI supports). The\n // key is spread only when non-empty so legacy readers that don't recognise it\n // are unaffected; normalizeLayerGroups round-trips them back on load.\n const layerGroups = state.layerGroups ?? [];\n // Persist the grid only when it is more than a single pane; default single-map\n // projects stay byte-identical and unaffected by this feature. The reconcile\n // keeps `secondaryMapViews` exactly `rows * cols - 1` long even if state drifted.\n const { mapLayout, secondaryMapViews } = resolveMapGrid(\n normalizeMapLayout(state.mapLayout),\n normalizeSecondaryMapViews(state.secondaryMapViews),\n { mapView: state.mapView },\n );\n const persistGrid = mapLayout.rows * mapLayout.cols > 1;\n const styleLibrary = normalizeStyleLibraryEntries(state.styleLibrary);\n const selectedLayerId =\n state.selectedLayerId === null\n ? null\n : typeof state.selectedLayerId === \"string\" &&\n layers.some((layer) => layer.id === state.selectedLayerId)\n ? state.selectedLayerId\n : undefined;\n return {\n version: PROJECT_VERSION,\n name: state.projectName,\n mapView: state.mapView,\n basemapStyleUrl: state.basemapStyleUrl,\n basemapVisible: state.basemapVisible,\n basemapOpacity: state.basemapOpacity,\n ...(state.blankBackgroundColor ? { blankBackgroundColor: state.blankBackgroundColor } : {}),\n layers: 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 ...(obia ? { obia } : {}),\n ...(widgets ? { widgets } : {}),\n ...bookmarkFields(state.bookmarks, state.bookmarkGroups),\n ...(dashboardColumns !== DEFAULT_DASHBOARD_COLUMNS ? { dashboardColumns } : {}),\n ...(persistGrid\n ? {\n mapLayout,\n secondaryMapViews,\n ...(normalizeString(state.primaryMapLabel)\n ? { primaryMapLabel: normalizeString(state.primaryMapLabel) }\n : {}),\n }\n : {}),\n // Written only for the non-default renderer, and independently of the grid:\n // a single-pane Cesium project persists `primaryRenderer` with no\n // `mapLayout`, and a MapLibre project writes neither.\n ...(normalizePrimaryRenderer(state.primaryRenderer)\n ? { primaryRenderer: normalizePrimaryRenderer(state.primaryRenderer)! }\n : {}),\n ...(styleLibrary.length > 0 ? { styleLibrary } : {}),\n ...(comments.length > 0 ? { comments } : {}),\n ...(interaction ? { interaction } : {}),\n metadata: state.metadata,\n };\n}\n\n// An external native layer can drop its persisted `geojson` only if its\n// features can be reconstructed on reopen, i.e. it has a fetchable source URL\n// (the Add Vector Layer / WFS / geojson-url cases). Layers loaded from local\n// files or built in-memory (e.g. by a plugin's drawing/annotation control)\n// have no such URL, so the persisted `geojson` is their ONLY copy and must be\n// kept.\nfunction hasRestorableSourceUrl(layer: GeoLibreLayer): boolean {\n const sourceUrl = layer.source.url;\n const originalUrl = layer.metadata.originalUrl;\n return (\n (typeof sourceUrl === \"string\" && sourceUrl.trim() !== \"\") ||\n (typeof originalUrl === \"string\" && originalUrl.trim() !== \"\")\n );\n}\n\n/**\n * Whether a layer lives only for the current session and is never written to a\n * project file. A plugin sets `metadata.sessionOnly` on a store row that mirrors\n * something it draws itself and cannot restore, such as a point cloud streamed\n * from a short-lived signed URL, so the row shows in the Layers panel while the\n * plugin runs but a reopened project carries no dead entry for it.\n *\n * @param layer - The layer to check.\n * @returns `true` when the layer must be left out of saved projects.\n */\nexport function isSessionOnlyLayer(layer: GeoLibreLayer): boolean {\n return layer.metadata.sessionOnly === true;\n}\n\nfunction prepareLayerForSave(layer: GeoLibreLayer): GeoLibreLayer {\n const hasGeometryEdits = layer.metadata.geometryEdited === true;\n layer = portableLayer(layer);\n // WFS feature URLs can contain inline authentication supplied by a plugin.\n // Persist a sanitized reference, and keep the fetched collection embedded so\n // the saved project remains renderable without storing those credentials.\n // The stripped flag is persisted alongside that sanitized reference: once the\n // reference is stored credential-free, a later save of the reopened project\n // cannot tell that the URL ever needed a secret, and would drop the embedded\n // features the reference can no longer re-fetch without it.\n let wfsCredentialsRedacted = layer.metadata.wfsCredentialsRedacted === true;\n if (layer.metadata.sourceKind === \"wfs-getfeature\") {\n const sourceUrl =\n typeof layer.source.url === \"string\"\n ? redactUrlCredentials(layer.source.url)\n : layer.source.url;\n const sourcePath =\n typeof layer.sourcePath === \"string\"\n ? redactUrlCredentials(layer.sourcePath)\n : layer.sourcePath;\n const originalUrl =\n typeof layer.metadata.originalUrl === \"string\"\n ? redactUrlCredentials(layer.metadata.originalUrl)\n : layer.metadata.originalUrl;\n const stripped =\n sourceUrl !== layer.source.url ||\n sourcePath !== layer.sourcePath ||\n originalUrl !== layer.metadata.originalUrl;\n wfsCredentialsRedacted ||= stripped;\n if (stripped) {\n layer = {\n ...layer,\n source: sourceUrl === layer.source.url ? layer.source : { ...layer.source, url: sourceUrl },\n sourcePath,\n metadata: {\n ...layer.metadata,\n ...(originalUrl === layer.metadata.originalUrl ? {} : { originalUrl }),\n wfsCredentialsRedacted: true,\n },\n };\n }\n }\n // This flag describes unsaved changes to the live source, not persisted\n // project state. A reference-only save reloads the original geometries;\n // carrying the flag into that project would warn about nonexistent edits.\n if (layer.metadata.geometryEdited !== undefined) {\n const { geometryEdited: _geometryEdited, ...metadata } = layer.metadata;\n layer = { ...layer, metadata };\n }\n // The live time filter is derived from the Time Slider's current date, so it\n // is transient: strip it before saving so a reopened project never starts\n // with a stale time-window filter hiding most of a layer's features. The\n // binding config in `metadata.timeBinding` persists, and the Time Slider\n // re-applies the filter the next time it activates.\n if (layer.timeFilter !== undefined) {\n const { timeFilter: _timeFilter, ...rest } = layer;\n layer = rest;\n }\n if (layer.embedFilter !== undefined) {\n const { embedFilter: _embedFilter, ...rest } = layer;\n layer = rest;\n }\n // Catalog metadata is written only when it says something: a record a\n // plugin (or an older build) left blank, or with blank fields, is cleaned or\n // dropped so an empty `descriptiveMetadata` block never reaches the file.\n if (layer.descriptiveMetadata !== undefined) {\n const { descriptiveMetadata: rawDescriptiveMetadata, ...rest } = layer;\n const descriptiveMetadata = normalizeLayerDescriptiveMetadata(rawDescriptiveMetadata);\n layer = descriptiveMetadata ? { ...rest, descriptiveMetadata } : rest;\n }\n\n // Some live plugin layers publish a large in-memory row model solely for\n // the Attribute Table and rebuild it from their feed on activation. Keeping\n // those rows in the store makes them queryable; embedding them in every\n // project/autosave would persist stale positions and can cross the history\n // snapshot ceiling.\n if (layer.geojson && layer.metadata.transientGeojson === true) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n // Live CZML feeds likewise rebuild their renderer payload on activation.\n // Persisting thousands of packets in every autosave duplicates the feed,\n // stores stale positions, and can exceed the history snapshot limit.\n if (layer.source.czmlData !== undefined && layer.metadata.transientCzml === true) {\n const { czmlData: _czmlData, ...source } = layer.source;\n layer = { ...layer, source };\n }\n\n // External native layers that restore their features from a source URL keep\n // a `geojson` copy on the map only for the attribute table; it is redundant\n // in a saved project and would only bloat it, so strip it. Layers without a\n // restorable URL (local-file or in-memory) keep their `geojson` because it is\n // the sole copy GeoLibre's restore path (`ensureExternalGeoJsonNativeLayer`)\n // re-renders from.\n //\n // Add Vector Layer (`maplibre-gl-vector`) layers are the exception: they are\n // restored by the control, not from `geojson` — from the file path on desktop\n // or embedded `metadata.embeddedGeoJSON` on the web. Their `geojson` is only\n // the attribute table's copy, so persisting it would silently embed the whole\n // dataset (bypassing the web embed prompt) instead of saving the path. Strip\n // it regardless of a restorable URL.\n const isVectorControlLayer = layer.metadata.sourceKind === \"maplibre-gl-vector\";\n if (\n layer.metadata.externalNativeLayer === true &&\n layer.geojson &&\n (hasRestorableSourceUrl(layer) || isVectorControlLayer)\n ) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n const wfsSourceUrl = layer.source.url;\n let hasHttpWfsSource = false;\n if (typeof wfsSourceUrl === \"string\") {\n try {\n const url = new URL(wfsSourceUrl);\n hasHttpWfsSource = url.protocol === \"http:\" || url.protocol === \"https:\";\n } catch {\n hasHttpWfsSource = false;\n }\n }\n\n // Host-managed WFS records reload through their persisted successful\n // GetFeature URL. Keep unedited feature collections reference-only in saved\n // projects. Edited geometry becomes authoritative embedded data, so omit\n // the request URL in that saved copy; otherwise a later save/reopen could\n // replace the edits with the service response.\n if (\n layer.type === \"geojson\" &&\n layer.metadata.sourceKind === \"wfs-getfeature\" &&\n layer.geojson &&\n hasHttpWfsSource\n ) {\n if (hasGeometryEdits) {\n const { url: _url, ...source } = layer.source;\n layer = { ...layer, source };\n } else if (!wfsCredentialsRedacted) {\n const { geojson: _geojson, ...rest } = layer;\n // Credential-bearing references are deliberately kept with their data.\n layer = rest;\n }\n }\n\n // An Add Vector Layer layer GeoLibre adopted (`maplibre-gl-vector-adopted`,\n // ADOPTED_VECTOR_SOURCE_KIND in @geolibre/plugins) holds its features in\n // `geojson`, but a URL-backed one saves the URL and is re-read through the\n // control on reopen, like the control layer it was. A browser-picked file has\n // no URL and keeps its features, like a drag-and-drop layer.\n if (\n layer.geojson &&\n layer.metadata.sourceKind === \"maplibre-gl-vector-adopted\" &&\n hasRestorableSourceUrl(layer)\n ) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n // A local-file layer the desktop host can re-read from its absolute path on\n // reopen (a drag-dropped or Add Data vector file) does not embed its features\n // either: the path is saved and the data is reloaded from disk. The flag is\n // only set when a real path was captured (desktop), so a web project — which\n // cannot re-read a path — never sets it and keeps the embedded copy.\n if (layer.geojson && layer.metadata.localFileReloadable === true) {\n const { geojson: _geojson, ...rest } = layer;\n layer = rest;\n }\n\n if (layer.type === \"wms\") {\n const tiles = layer.source.tiles;\n if (!Array.isArray(tiles)) return layer;\n const portableTiles = tiles.map((tile) => portableWmsTileUrl(tile));\n return portableTiles.some((tile, index) => tile !== tiles[index])\n ? { ...layer, source: { ...layer.source, tiles: portableTiles } }\n : layer;\n }\n\n if (layer.type !== \"xyz\") return layer;\n\n const originalUrl =\n typeof layer.metadata.originalUrl === \"string\" && layer.metadata.originalUrl.trim()\n ? layer.metadata.originalUrl\n : typeof layer.source.url === \"string\" && layer.source.url.trim()\n ? layer.source.url\n : null;\n if (!originalUrl) return layer;\n\n const metadata = { ...layer.metadata };\n delete metadata.resolvedUrl;\n\n // The collapse below rewinds a resolved short URL (or a desktop protocol URL)\n // back to what the user typed, because those tile URLs are not portable. A\n // TileJSON layer is the exception: `tiles` holds the document's own https\n // templates, which are portable, while its `originalUrl` is the *document*\n // URL and carries no {z}/{x}/{y}. Collapsing onto it would leave the saved\n // layer unable to request a tile until a re-fetch succeeds — and\n // `resolveProjectXyzLayers` keeps the on-disk layer when the document is\n // unreachable, so an offline reopen would strand it. Rewind only `url`.\n const tiles = typeof layer.metadata.tilejsonUrl === \"string\" ? {} : { tiles: [originalUrl] };\n\n return {\n ...layer,\n source: {\n ...layer.source,\n ...tiles,\n url: originalUrl,\n },\n metadata,\n };\n}\n\n/**\n * The plain HTTP(S) WMS template inside the desktop app's `geolibre-wms://`\n * wrapper (its CORS-exempt native fetcher), or the tile unchanged when it is\n * not wrapped or the wrapped URL is not HTTP(S). Shared by project export and\n * the ArcGIS engine's story-export templates.\n *\n * @param tile - A tile template, wrapped or not.\n * @returns The unwrapped template, or `tile` itself.\n */\nexport function portableWmsTileUrl(tile: unknown): unknown {\n // Keep this protocol prefix in sync with WMS_TILE_PROTOCOL in the desktop\n // app, which packages/core cannot import without reversing dependencies.\n if (typeof tile !== \"string\" || !tile.startsWith(\"geolibre-wms://\")) return tile;\n try {\n const url = new URL(tile).searchParams.get(\"url\");\n if (!url) return tile;\n const parsed = new URL(url);\n return parsed.protocol === \"http:\" || parsed.protocol === \"https:\" ? url : tile;\n } catch {\n return tile;\n }\n}\n\n/**\n * The store form of one project layer: its style completed from\n * {@link DEFAULT_LAYER_STYLE} and the project's top-level `styles` entry.\n *\n * Legacy and externally-authored projects can carry a partial top-level style\n * alongside newer fields on the layer itself. Those layer fields are kept,\n * while the top-level copy stays authoritative where it explicitly supplies a\n * value.\n *\n * @param project - The project the layer belongs to (for its `styles` map).\n * @param layer - One of `project.layers`.\n * @returns A new layer record ready for the store.\n */\nexport function hydrateProjectLayer(\n project: Pick<GeoLibreProject, \"styles\">,\n layer: GeoLibreLayer,\n): GeoLibreLayer {\n const topLevel = project.styles?.[layer.id];\n return {\n ...layer,\n style: topLevel\n ? { ...DEFAULT_LAYER_STYLE, ...layer.style, ...topLevel }\n : { ...DEFAULT_LAYER_STYLE, ...layer.style },\n };\n}\n\nexport function applyProjectToStore(project: GeoLibreProject): {\n projectName: string;\n mapView: MapViewState;\n basemapStyleUrl: string;\n basemapVisible: boolean;\n basemapOpacity: number;\n blankBackgroundColor: string | null;\n layers: GeoLibreLayer[];\n layerGroups: LayerGroup[];\n preferences: ProjectPreferences;\n projectPlugins: ProjectPluginState | null;\n legend: LegendConfig;\n printLayout: PrintLayoutConfig;\n storymap: StoryMap | null;\n models: ProcessingModel[];\n processingHistory: ProcessingRun[];\n obiaWorkbench: Record<string, unknown> | null;\n widgets: DashboardWidget[];\n bookmarks: ProjectBookmark[];\n bookmarkGroups: ProjectBookmarkGroup[];\n dashboardColumns: number;\n mapLayout: MapGridLayout;\n secondaryMapViews: SecondaryMapView[];\n primaryMapLabel: string;\n primaryRenderer: MapRendererKind;\n projectStyleLibrary: StyleLibraryEntry[];\n comments: ProjectComment[];\n projectInteraction: ProjectInteraction | null;\n metadata: Record<string, unknown>;\n} {\n const layers = project.layers.map((layer) => hydrateProjectLayer(project, layer));\n // Re-normalize here (even though `parseProject` already did) because\n // `applyProjectToStore` is a public entry point also reached directly by\n // programmatic/newProject loads that never passed through `parseProject`, so\n // this stays a hardening boundary for untrusted group data. The call is\n // idempotent on already-normalized input.\n const layerGroups = normalizeLayerGroups(project.layerGroups);\n const validGroupIds = new Set(layerGroups.map((g) => g.id));\n // Drop dangling groupIds, then restore the contiguity invariant the layer\n // panel relies on, in case the project was hand-edited or produced externally\n // with a group's members interleaved among unrelated layers.\n const normalizedLayers = normalizeGroupContiguity(\n layers.map((layer) =>\n layer.groupId && !validGroupIds.has(layer.groupId) ? { ...layer, groupId: undefined } : layer,\n ),\n );\n const basemapStyleUrl = project.basemapStyleUrl;\n const basemapVisible = project.basemapVisible ?? true;\n const basemapOpacity = project.basemapOpacity ?? 1;\n const blankBackgroundColor = normalizeBlankBackgroundColor(project.blankBackgroundColor);\n // Reconcile the (possibly hand-edited or programmatic) grid so the store's\n // invariant `secondaryMapViews.length === rows * cols - 1` always holds.\n const mapView = normalizeMapViewState(project.mapView);\n const { mapLayout, secondaryMapViews } = resolveMapGrid(\n normalizeMapLayout(project.mapLayout),\n normalizeSecondaryMapViews(project.secondaryMapViews),\n { mapView },\n );\n\n // Scrub cross-references that point at layers not present in the loaded\n // project (orphans from hand-editing or a partial project file).\n const existingLayerIds = new Set(normalizedLayers.map((l) => l.id));\n const widgets = normalizeWidgets(project.widgets) ?? [];\n const comments = normalizeProjectComments(project.comments);\n const legend = normalizeLegendConfig(project.legend) ?? {\n ...DEFAULT_LEGEND_CONFIG,\n };\n\n const allReferencedIds = new Set<string>();\n for (const w of widgets) allReferencedIds.add(w.layerId);\n for (const c of comments) {\n if (c.anchor.type === \"feature\") allReferencedIds.add(c.anchor.layerId);\n }\n for (const id of legend.order) allReferencedIds.add(id);\n for (const key of Object.keys(legend.overrides)) {\n const base = key.includes(\"::\") ? key.slice(0, key.indexOf(\"::\")) : key;\n allReferencedIds.add(base);\n }\n if (legend.customEntries) {\n for (const key of Object.keys(legend.customEntries)) {\n if (!key.startsWith(\"custom:\")) allReferencedIds.add(key);\n }\n }\n\n const orphanIds = new Set([...allReferencedIds].filter((id) => !existingLayerIds.has(id)));\n\n const scrubbedWidgets =\n orphanIds.size > 0 ? scrubWidgetsForRemovedLayers(widgets, orphanIds) : widgets;\n const scrubbedComments =\n orphanIds.size > 0 ? scrubCommentsForRemovedLayers(comments, orphanIds) : comments;\n const scrubbedLegend =\n orphanIds.size > 0 ? scrubLegendForRemovedLayers(legend, orphanIds) : legend;\n const bookmarkGroups = normalizeBookmarkGroups(project.bookmarkGroups);\n // The composer's data/atlas blocks name a layer directly rather than through\n // `allReferencedIds`, so they are scrubbed against the surviving layer set.\n const printLayout = scrubPrintLayoutForLayers(\n normalizePrintLayoutConfig(project.printLayout) ?? createDefaultPrintLayout(),\n existingLayerIds,\n );\n\n return {\n projectName: project.name,\n mapView,\n basemapStyleUrl,\n basemapVisible,\n basemapOpacity,\n blankBackgroundColor,\n layers: normalizedLayers,\n layerGroups,\n preferences: normalizeProjectPreferences(project.preferences),\n projectPlugins: normalizeProjectPlugins(project.plugins),\n legend: scrubbedLegend,\n printLayout,\n storymap: normalizeStoryMap(project.storymap),\n models: normalizeModels(project.models) ?? [],\n processingHistory: normalizeProcessingHistory(project.processingHistory) ?? [],\n obiaWorkbench: normalizeObiaWorkbench(project.obia),\n widgets: scrubbedWidgets,\n bookmarkGroups,\n bookmarks: normalizeBookmarks(project.bookmarks, bookmarkGroups),\n dashboardColumns: normalizeDashboardColumns(project.dashboardColumns),\n mapLayout,\n secondaryMapViews,\n primaryMapLabel: normalizeString(project.primaryMapLabel),\n // An unknown or absent value resolves to the 2D map, so a project written\n // before #2217 (and any hand-edited one) opens on MapLibre as before.\n primaryRenderer: normalizePrimaryRenderer(project.primaryRenderer) ?? DEFAULT_PRIMARY_RENDERER,\n projectStyleLibrary: normalizeStyleLibraryEntries(project.styleLibrary),\n comments: scrubbedComments,\n projectInteraction: normalizeProjectInteraction(project.interaction),\n metadata: project.metadata,\n };\n}\n\n/**\n * Create an unlinked copy of a project, suffixed with \"(copy)\" by default and\n * stripped of share-specific metadata (shareId, shareUrl, etc.).\n */\nexport function detachProjectCopy(\n project: GeoLibreProject,\n options: { nameSuffix?: string } = {},\n): GeoLibreProject {\n const suffix = options.nameSuffix ?? \"(copy)\";\n const rawName = project.name.trim() || DEFAULT_PROJECT_NAME;\n const name = suffix ? (rawName.endsWith(suffix) ? rawName : `${rawName} ${suffix}`) : rawName;\n\n const metadata = { ...(project.metadata ?? {}) };\n for (const key of Object.keys(metadata)) {\n if (/^share/i.test(key)) {\n delete metadata[key];\n }\n }\n\n return {\n ...project,\n id: uuidv4(),\n name,\n metadata,\n };\n}\n\n/**\n * Create a template snapshot of a project. Optionally strips data layers while\n * keeping basemap, layer groups, styles, legend config, preferences, widgets, and\n * print layout.\n */\nexport function createProjectTemplate(\n project: GeoLibreProject,\n options: { name?: string; stripDataLayers?: boolean } = {},\n): GeoLibreProject {\n const detached = detachProjectCopy(project, { nameSuffix: \"\" });\n const name = options.name?.trim() || detached.name;\n const stripDataLayers = options.stripDataLayers !== false;\n\n const layers = stripDataLayers ? [] : detached.layers;\n\n return {\n ...detached,\n name,\n layers,\n metadata: {\n ...detached.metadata,\n isTemplate: true,\n },\n };\n}\n","/**\n * Structured differ for two {@link GeoLibreProject} documents, used to show\n * what changed between autosave snapshots (GeoLibre#2858, \"Project snapshot\n * diff\").\n *\n * The result is a summary rather than a JSON patch: layers are matched by id\n * and reported as added, removed, moved or changed; per-layer changes are\n * grouped (style, labels, filter, source, other) with truncated before/after\n * previews; embedded GeoJSON is summarized as counts of added, removed and\n * modified features. Everything is pure and synchronous.\n *\n * Embedded GeoJSON can be large, so each feature is hashed at most once per\n * object identity (memoized in a `WeakMap`), and layers whose `geojson`\n * reference is shared between the two projects are skipped outright.\n */\nimport type { FeatureCollection } from \"geojson\";\nimport type { GeoLibreLayer, GeoLibreProject, ProjectPluginState } from \"./types\";\n\n/** One changed value, with bounded human-readable previews of each side. */\nexport interface ProjectValueChange {\n /** Dotted key path relative to the section, e.g. `fillColor` or `labels.field`. */\n path: string;\n /** Preview of the earlier value; `undefined` when the key was absent. */\n before?: string;\n /** Preview of the later value; `undefined` when the key is now absent. */\n after?: string;\n}\n\n/** Counts of embedded GeoJSON feature changes in one layer. */\nexport interface FeatureChangeSummary {\n added: number;\n removed: number;\n modified: number;\n unchanged: number;\n /**\n * How features were matched: `\"id\"` when every feature carries a unique\n * `id`, `\"geometry\"` when none does (matched by geometry hash), `\"mixed\"`\n * otherwise.\n */\n matchedBy: \"id\" | \"geometry\" | \"mixed\";\n}\n\n/** Changes to one layer between the two projects. */\nexport interface LayerDiff {\n id: string;\n /** Layer name in the later project (or the earlier one when removed). */\n name: string;\n /** Layer type in the later project (or the earlier one when removed). */\n type: string;\n status: \"added\" | \"removed\" | \"changed\";\n /** Set when the layer was renamed. */\n renamed?: { before: string; after: string };\n /**\n * Set when the layer changed position relative to the layers present in\n * both projects (bottom-to-top indexes in each project's `layers`).\n */\n moved?: { before: number; after: number };\n visibility?: { before: boolean; after: boolean };\n opacity?: { before: number; after: number };\n /** Style keys other than labels. */\n style: ProjectValueChange[];\n /** Label style keys (`style.labels.*`), without the `labels.` prefix. */\n labels: ProjectValueChange[];\n /** `filterExpression` and `quickFilters`. */\n filter: ProjectValueChange[];\n /** `type`, `source.*` and `sourcePath`. */\n source: ProjectValueChange[];\n /** Any other persisted layer field (popup, joins, metadata.*, ...). */\n other: ProjectValueChange[];\n /** Embedded GeoJSON feature changes; absent when the data is unchanged. */\n features?: FeatureChangeSummary;\n /** Feature count for an added or removed layer with embedded GeoJSON. */\n featureCount?: number;\n}\n\n/** Changes to one plugin's project state. */\nexport interface PluginDiff {\n id: string;\n /** `added`/`removed` mean the plugin was activated/deactivated. */\n status: \"added\" | \"removed\" | \"changed\";\n changes: ProjectValueChange[];\n}\n\n/** The full structured difference between two projects. */\nexport interface ProjectDiff {\n layers: {\n added: LayerDiff[];\n removed: LayerDiff[];\n changed: LayerDiff[];\n /** True when any layer present in both projects changed position. */\n reordered: boolean;\n };\n camera: ProjectValueChange[];\n basemap: ProjectValueChange[];\n /** Map projection (`preferences.map.projection`). */\n projection: ProjectValueChange[];\n /** Other project preferences (`preferences.*` minus the projection). */\n preferences: ProjectValueChange[];\n plugins: PluginDiff[];\n /** Plugin manifest URLs added to or removed from the project. */\n pluginManifests: { added: string[]; removed: string[] };\n /** Project name (`name`), `metadata.description` and other metadata keys. */\n metadata: ProjectValueChange[];\n /** Other top-level sections that changed (`legend`, `storymap`, ...). */\n sections: string[];\n /** Total number of reported changes; 0 means the projects are equivalent. */\n changeCount: number;\n}\n\n/** Options for {@link diffProjects}. */\nexport interface ProjectDiffOptions {\n /** Maximum characters of a before/after preview (default 80). */\n maxPreviewLength?: number;\n}\n\nconst DEFAULT_PREVIEW_LENGTH = 80;\nconst CAMERA_EPSILON = 1e-6;\n\n/** Layer fields reported in their own group, never under `other`. */\nconst LAYER_GROUPED_KEYS = new Set([\n \"id\",\n \"name\",\n \"type\",\n \"visible\",\n \"opacity\",\n \"style\",\n \"geojson\",\n \"filterExpression\",\n \"quickFilters\",\n \"source\",\n \"sourcePath\",\n // Derived from the layer order on save; reordering is reported separately.\n \"beforeId\",\n]);\n\n/** Top-level project fields reported in their own group, never as a section. */\nconst PROJECT_GROUPED_KEYS = new Set([\n \"id\",\n \"version\",\n \"name\",\n \"mapView\",\n \"basemapStyleUrl\",\n \"basemapVisible\",\n \"basemapOpacity\",\n \"blankBackgroundColor\",\n \"primaryRenderer\",\n \"layers\",\n \"styles\",\n \"preferences\",\n \"plugins\",\n \"metadata\",\n // UI selection, not content.\n \"selectedLayerId\",\n]);\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Deep structural equality for JSON-like values. A key whose value is\n * `undefined` is treated as absent, so an in-memory record compares equal to\n * its JSON round trip.\n *\n * @param a - First value.\n * @param b - Second value.\n * @returns True when the two values are structurally equal.\n */\nexport function jsonValuesEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (typeof a === \"number\" && typeof b === \"number\") return Number.isNaN(a) && Number.isNaN(b);\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (!jsonValuesEqual(a[i], b[i])) return false;\n return true;\n }\n if (isPlainObject(a)) {\n if (!isPlainObject(b)) return false;\n for (const key of Object.keys(a)) {\n if (a[key] === undefined) continue;\n if (!jsonValuesEqual(a[key], b[key])) return false;\n }\n for (const key of Object.keys(b)) {\n if (b[key] !== undefined && a[key] === undefined) return false;\n }\n return true;\n }\n return false;\n}\n\nfunction isBlank(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return isPlainObject(value) && Object.values(value).every((item) => item === undefined);\n}\n\n/**\n * Equality for reporting: like {@link jsonValuesEqual}, but an empty array,\n * an empty object, `null` and an absent value are interchangeable, since the\n * save path omits empty sections that an in-memory project still carries.\n */\nfunction sameForReport(a: unknown, b: unknown): boolean {\n return jsonValuesEqual(a, b) || (isBlank(a) && isBlank(b));\n}\n\n/**\n * Render a bounded JSON-ish preview of a value without serializing more of it\n * than the preview can show, so a large array or object costs no more than\n * `maxLength` characters of work.\n *\n * @param value - The value to preview.\n * @param maxLength - Maximum preview length; longer output ends with `…`.\n * @returns The preview, or `undefined` for `undefined`.\n */\nexport function previewValue(\n value: unknown,\n maxLength = DEFAULT_PREVIEW_LENGTH,\n): string | undefined {\n if (value === undefined) return undefined;\n if (typeof value === \"string\") {\n return value.length > maxLength ? `${value.slice(0, Math.max(0, maxLength - 1))}…` : value;\n }\n let out = \"\";\n let truncated = false;\n const emit = (text: string): boolean => {\n if (out.length + text.length > maxLength) {\n out += text.slice(0, Math.max(0, maxLength - out.length));\n truncated = true;\n return false;\n }\n out += text;\n return true;\n };\n const walk = (v: unknown): boolean => {\n if (v === null || typeof v !== \"object\") {\n if (typeof v === \"number\" && !Number.isFinite(v)) return emit(\"null\");\n // A long string can never fit, so quote only the part that could show\n // instead of serializing a nested blob (a data URI, say) in full.\n if (typeof v === \"string\" && v.length > maxLength) {\n return emit(JSON.stringify(v.slice(0, maxLength)));\n }\n const text = typeof v === \"bigint\" ? v.toString() : JSON.stringify(v);\n return emit(text ?? \"null\");\n }\n if (Array.isArray(v)) {\n if (!emit(\"[\")) return false;\n for (let i = 0; i < v.length; i++) {\n if (i > 0 && !emit(\",\")) return false;\n if (!walk(v[i] === undefined ? null : v[i])) return false;\n }\n return emit(\"]\");\n }\n if (!emit(\"{\")) return false;\n let first = true;\n for (const [key, item] of Object.entries(v as Record<string, unknown>)) {\n if (item === undefined) continue;\n if (!first && !emit(\",\")) return false;\n first = false;\n if (!emit(`${JSON.stringify(key)}:`)) return false;\n if (!walk(item)) return false;\n }\n return emit(\"}\");\n };\n walk(value);\n if (truncated) out = `${out.slice(0, Math.max(0, maxLength - 1))}…`;\n return out;\n}\n\n/**\n * Diff two records key by key. Nested plain objects are descended into up to\n * `depth` levels (paths joined with `.`); arrays and deeper values are\n * compared whole.\n */\nfunction diffRecords(\n before: Record<string, unknown> | undefined,\n after: Record<string, unknown> | undefined,\n options: { prefix?: string; depth?: number; skip?: Set<string>; maxLength: number },\n out: ProjectValueChange[],\n): void {\n const a = before ?? {};\n const b = after ?? {};\n const prefix = options.prefix ?? \"\";\n const depth = options.depth ?? 1;\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n for (const key of keys) {\n if (options.skip?.has(key)) continue;\n const x = a[key];\n const y = b[key];\n if (sameForReport(x, y)) continue;\n if (depth > 0 && isPlainObject(x) && isPlainObject(y)) {\n diffRecords(\n x,\n y,\n { prefix: `${prefix}${key}.`, depth: depth - 1, maxLength: options.maxLength },\n out,\n );\n continue;\n }\n out.push({\n path: `${prefix}${key}`,\n before: previewValue(x, options.maxLength),\n after: previewValue(y, options.maxLength),\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Feature hashing\n// ---------------------------------------------------------------------------\n\ninterface FeatureHash {\n /** Hash of the geometry alone. */\n geometry: string;\n /** Hash of geometry + properties (+ any other member). */\n full: string;\n}\n\nconst featureHashCache = new WeakMap<object, FeatureHash>();\n\n/** cyrb53: a fast, well-distributed 53-bit string hash. */\nfunction hashString(text: string, seed = 0): string {\n let h1 = 0xdeadbeef ^ seed;\n let h2 = 0x41c6ce57 ^ seed;\n for (let i = 0; i < text.length; i++) {\n const ch = text.charCodeAt(i);\n h1 = Math.imul(h1 ^ ch, 2654435761);\n h2 = Math.imul(h2 ^ ch, 1597334677);\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);\n h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);\n h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);\n return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36);\n}\n\nfunction hashFeature(feature: unknown): FeatureHash {\n if (typeof feature !== \"object\" || feature === null) {\n const text = JSON.stringify(feature ?? null);\n const h = hashString(text);\n return { geometry: h, full: h };\n }\n const cached = featureHashCache.get(feature);\n if (cached) return cached;\n const { geometry, properties, id: _id, ...rest } = feature as Record<string, unknown>;\n const geometryHash = hashString(JSON.stringify(geometry ?? null));\n const propertiesHash = hashString(JSON.stringify(properties ?? null), 1);\n const restKeys = Object.keys(rest).filter((key) => key !== \"type\" && rest[key] !== undefined);\n const restHash = restKeys.length > 0 ? hashString(JSON.stringify(rest), 2) : \"\";\n const result = { geometry: geometryHash, full: `${geometryHash}:${propertiesHash}:${restHash}` };\n featureHashCache.set(feature, result);\n return result;\n}\n\nfunction featureKey(feature: unknown): string | null {\n if (typeof feature !== \"object\" || feature === null) return null;\n const id = (feature as { id?: unknown }).id;\n if (typeof id === \"string\") return `s:${id}`;\n if (typeof id === \"number\" && Number.isFinite(id)) return `n:${id}`;\n return null;\n}\n\ninterface IndexedFeatures {\n byId: Map<string, string>;\n /** Geometry hash → full hashes of the id-less features with that geometry. */\n byGeometry: Map<string, string[]>;\n keyed: number;\n unkeyed: number;\n}\n\nfunction indexFeatures(features: readonly unknown[]): IndexedFeatures {\n const byId = new Map<string, string>();\n const byGeometry = new Map<string, string[]>();\n let keyed = 0;\n let unkeyed = 0;\n for (const feature of features) {\n const hash = hashFeature(feature);\n const key = featureKey(feature);\n if (key !== null && !byId.has(key)) {\n byId.set(key, hash.full);\n keyed++;\n continue;\n }\n // No id, or a duplicate id: fall back to geometry matching.\n const list = byGeometry.get(hash.geometry);\n if (list) list.push(hash.full);\n else byGeometry.set(hash.geometry, [hash.full]);\n unkeyed++;\n }\n return { byId, byGeometry, keyed, unkeyed };\n}\n\n/**\n * Count the features added, removed and modified between two feature\n * collections. Features are matched by `id` when they carry one; id-less\n * features are matched by geometry hash, so a feature whose properties\n * changed but whose geometry did not counts as modified, while a moved\n * id-less feature counts as one removal plus one addition.\n *\n * @param before - Earlier collection (absent counts as empty).\n * @param after - Later collection (absent counts as empty).\n * @returns The change counts.\n */\nexport function diffFeatureCollections(\n before: FeatureCollection | null | undefined,\n after: FeatureCollection | null | undefined,\n): FeatureChangeSummary {\n const a = indexFeatures(Array.isArray(before?.features) ? before.features : []);\n const b = indexFeatures(Array.isArray(after?.features) ? after.features : []);\n let added = 0;\n let removed = 0;\n let modified = 0;\n let unchanged = 0;\n\n for (const [key, hash] of a.byId) {\n const other = b.byId.get(key);\n if (other === undefined) removed++;\n else if (other === hash) unchanged++;\n else modified++;\n }\n for (const key of b.byId.keys()) if (!a.byId.has(key)) added++;\n\n const geometryKeys = new Set([...a.byGeometry.keys(), ...b.byGeometry.keys()]);\n for (const geometry of geometryKeys) {\n const left = a.byGeometry.get(geometry) ?? [];\n const right = b.byGeometry.get(geometry) ?? [];\n // Pair identical features first, then pair the rest as modified.\n const counts = new Map<string, number>();\n for (const full of left) counts.set(full, (counts.get(full) ?? 0) + 1);\n let same = 0;\n for (const full of right) {\n const n = counts.get(full) ?? 0;\n if (n > 0) {\n counts.set(full, n - 1);\n same++;\n }\n }\n const leftRest = left.length - same;\n const rightRest = right.length - same;\n const paired = Math.min(leftRest, rightRest);\n unchanged += same;\n modified += paired;\n removed += leftRest - paired;\n added += rightRest - paired;\n }\n\n const keyed = a.keyed + b.keyed;\n const unkeyed = a.unkeyed + b.unkeyed;\n const matchedBy = unkeyed === 0 ? \"id\" : keyed === 0 ? \"geometry\" : \"mixed\";\n return { added, removed, modified, unchanged, matchedBy };\n}\n\n// ---------------------------------------------------------------------------\n// Layers\n// ---------------------------------------------------------------------------\n\nfunction effectiveStyle(\n project: GeoLibreProject,\n layer: GeoLibreLayer,\n): Record<string, unknown> | undefined {\n const top = project.styles?.[layer.id];\n if (!top) return layer.style as unknown as Record<string, unknown> | undefined;\n return { ...(layer.style ?? {}), ...top } as unknown as Record<string, unknown>;\n}\n\nfunction featureCountOf(layer: GeoLibreLayer): number | undefined {\n return Array.isArray(layer.geojson?.features) ? layer.geojson.features.length : undefined;\n}\n\nfunction emptyLayerDiff(layer: GeoLibreLayer, status: LayerDiff[\"status\"]): LayerDiff {\n return {\n id: layer.id,\n name: layer.name,\n type: layer.type,\n status,\n style: [],\n labels: [],\n filter: [],\n source: [],\n other: [],\n };\n}\n\n/**\n * Ids (of layers present in both orders) that moved: everything outside a\n * longest increasing subsequence of their later positions, so dragging one\n * layer reports that layer rather than every layer it passed.\n */\nfunction movedLayerIds(beforeIds: string[], afterIndex: Map<string, number>): Set<string> {\n const common = beforeIds.filter((id) => afterIndex.has(id));\n const seq = common.map((id) => afterIndex.get(id)!);\n // Patience-sorting LIS with predecessor links.\n const tails: number[] = [];\n const prev = new Array<number>(seq.length).fill(-1);\n for (let i = 0; i < seq.length; i++) {\n let lo = 0;\n let hi = tails.length;\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (seq[tails[mid]] < seq[i]) lo = mid + 1;\n else hi = mid;\n }\n if (lo > 0) prev[i] = tails[lo - 1];\n tails[lo] = i;\n }\n const stable = new Set<string>();\n for (let i = tails.length ? tails[tails.length - 1] : -1; i >= 0; i = prev[i]) {\n stable.add(common[i]);\n }\n return new Set(common.filter((id) => !stable.has(id)));\n}\n\nfunction diffLayer(\n beforeProject: GeoLibreProject,\n afterProject: GeoLibreProject,\n a: GeoLibreLayer,\n b: GeoLibreLayer,\n maxLength: number,\n): LayerDiff {\n const diff = emptyLayerDiff(b, \"changed\");\n if (a.name !== b.name) diff.renamed = { before: a.name, after: b.name };\n if (Boolean(a.visible) !== Boolean(b.visible)) {\n diff.visibility = { before: Boolean(a.visible), after: Boolean(b.visible) };\n }\n if (!jsonValuesEqual(a.opacity, b.opacity))\n diff.opacity = { before: a.opacity, after: b.opacity };\n\n const styleA = effectiveStyle(beforeProject, a);\n const styleB = effectiveStyle(afterProject, b);\n if (!jsonValuesEqual(styleA, styleB)) {\n diffRecords(styleA, styleB, { skip: new Set([\"labels\"]), maxLength }, diff.style);\n const labelsA = styleA?.labels;\n const labelsB = styleB?.labels;\n if (!jsonValuesEqual(labelsA, labelsB)) {\n diffRecords(\n isPlainObject(labelsA) ? labelsA : undefined,\n isPlainObject(labelsB) ? labelsB : undefined,\n { maxLength },\n diff.labels,\n );\n }\n }\n\n diffRecords(\n { filterExpression: a.filterExpression, quickFilters: a.quickFilters },\n { filterExpression: b.filterExpression, quickFilters: b.quickFilters },\n { depth: 0, maxLength },\n diff.filter,\n );\n\n if (a.type !== b.type) {\n diff.source.push({ path: \"type\", before: a.type, after: b.type });\n }\n if (!jsonValuesEqual(a.source, b.source)) {\n diffRecords(a.source, b.source, { prefix: \"source.\", depth: 0, maxLength }, diff.source);\n }\n if (a.sourcePath !== b.sourcePath) {\n diff.source.push({\n path: \"sourcePath\",\n before: previewValue(a.sourcePath, maxLength),\n after: previewValue(b.sourcePath, maxLength),\n });\n }\n\n diffRecords(\n a as unknown as Record<string, unknown>,\n b as unknown as Record<string, unknown>,\n { skip: LAYER_GROUPED_KEYS, depth: 1, maxLength },\n diff.other,\n );\n\n if (a.geojson !== b.geojson && (a.geojson || b.geojson)) {\n const features = diffFeatureCollections(a.geojson, b.geojson);\n if (features.added || features.removed || features.modified) diff.features = features;\n }\n return diff;\n}\n\nfunction layerHasChanges(diff: LayerDiff): boolean {\n return Boolean(\n diff.renamed ||\n diff.moved ||\n diff.visibility ||\n diff.opacity ||\n diff.features ||\n diff.style.length ||\n diff.labels.length ||\n diff.filter.length ||\n diff.source.length ||\n diff.other.length,\n );\n}\n\nfunction layerChangeCount(diff: LayerDiff): number {\n return (\n (diff.renamed ? 1 : 0) +\n (diff.moved ? 1 : 0) +\n (diff.visibility ? 1 : 0) +\n (diff.opacity ? 1 : 0) +\n (diff.features ? 1 : 0) +\n diff.style.length +\n diff.labels.length +\n diff.filter.length +\n diff.source.length +\n diff.other.length\n );\n}\n\n// ---------------------------------------------------------------------------\n// Plugins\n// ---------------------------------------------------------------------------\n\nfunction diffPlugins(\n before: ProjectPluginState | undefined,\n after: ProjectPluginState | undefined,\n maxLength: number,\n): { plugins: PluginDiff[]; manifests: { added: string[]; removed: string[] } } {\n const activeA = new Set(before?.activePluginIds ?? []);\n const activeB = new Set(after?.activePluginIds ?? []);\n const settingsA = before?.settings ?? {};\n const settingsB = after?.settings ?? {};\n const positionsA = before?.mapControlPositions ?? {};\n const positionsB = after?.mapControlPositions ?? {};\n const ids = new Set([\n ...activeA,\n ...activeB,\n ...Object.keys(settingsA),\n ...Object.keys(settingsB),\n ...Object.keys(positionsA),\n ...Object.keys(positionsB),\n ]);\n const plugins: PluginDiff[] = [];\n for (const id of [...ids].sort()) {\n const changes: ProjectValueChange[] = [];\n const wasActive = activeA.has(id);\n const isActive = activeB.has(id);\n if (!jsonValuesEqual(positionsA[id], positionsB[id])) {\n changes.push({\n path: \"position\",\n before: previewValue(positionsA[id], maxLength),\n after: previewValue(positionsB[id], maxLength),\n });\n }\n const sa = settingsA[id];\n const sb = settingsB[id];\n if (!jsonValuesEqual(sa, sb)) {\n if (isPlainObject(sa) || isPlainObject(sb)) {\n diffRecords(\n isPlainObject(sa) ? sa : undefined,\n isPlainObject(sb) ? sb : undefined,\n { prefix: \"settings.\", depth: 0, maxLength },\n changes,\n );\n } else {\n changes.push({\n path: \"settings\",\n before: previewValue(sa, maxLength),\n after: previewValue(sb, maxLength),\n });\n }\n }\n if (wasActive !== isActive) {\n plugins.push({ id, status: isActive ? \"added\" : \"removed\", changes });\n } else if (changes.length > 0) {\n plugins.push({ id, status: \"changed\", changes });\n }\n }\n const urlsA = new Set(before?.manifestUrls ?? []);\n const urlsB = new Set(after?.manifestUrls ?? []);\n return {\n plugins,\n manifests: {\n added: [...urlsB].filter((url) => !urlsA.has(url)),\n removed: [...urlsA].filter((url) => !urlsB.has(url)),\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Project\n// ---------------------------------------------------------------------------\n\nfunction numbersDiffer(a: unknown, b: unknown): boolean {\n if (typeof a === \"number\" && typeof b === \"number\") return Math.abs(a - b) > CAMERA_EPSILON;\n return !jsonValuesEqual(a, b);\n}\n\n/**\n * Compute a structured summary of what changed from `before` to `after`.\n *\n * Layers are matched by id. Embedded GeoJSON is compared per feature (by id,\n * falling back to geometry hash) and each feature is hashed once per object,\n * so diffing the same parsed project against several others reuses its\n * hashes; callers keep and pass the same parsed project objects to benefit.\n *\n * @param before - The earlier project (e.g. an autosave snapshot).\n * @param after - The later project (e.g. the current project).\n * @param options - Preview length and other tuning.\n * @returns The grouped change summary.\n */\nexport function diffProjects(\n before: GeoLibreProject,\n after: GeoLibreProject,\n options: ProjectDiffOptions = {},\n): ProjectDiff {\n const maxLength = options.maxPreviewLength ?? DEFAULT_PREVIEW_LENGTH;\n const layersA = Array.isArray(before.layers) ? before.layers : [];\n const layersB = Array.isArray(after.layers) ? after.layers : [];\n const byIdA = new Map(layersA.map((layer) => [layer.id, layer]));\n const byIdB = new Map(layersB.map((layer) => [layer.id, layer]));\n const indexA = new Map(layersA.map((layer, i) => [layer.id, i]));\n const indexB = new Map(layersB.map((layer, i) => [layer.id, i]));\n const moved = movedLayerIds(\n layersA.map((layer) => layer.id),\n indexB,\n );\n\n const added: LayerDiff[] = [];\n const removed: LayerDiff[] = [];\n const changed: LayerDiff[] = [];\n for (const layer of layersB) {\n if (byIdA.has(layer.id)) continue;\n const diff = emptyLayerDiff(layer, \"added\");\n const count = featureCountOf(layer);\n if (count !== undefined) diff.featureCount = count;\n added.push(diff);\n }\n for (const layer of layersA) {\n if (byIdB.has(layer.id)) continue;\n const diff = emptyLayerDiff(layer, \"removed\");\n const count = featureCountOf(layer);\n if (count !== undefined) diff.featureCount = count;\n removed.push(diff);\n }\n // Report changed layers top-most first, matching the Layers panel.\n for (let i = layersB.length - 1; i >= 0; i--) {\n const b = layersB[i];\n const a = byIdA.get(b.id);\n if (!a) continue;\n const identical = a === b && before.styles?.[b.id] === after.styles?.[b.id];\n const diff = identical\n ? emptyLayerDiff(b, \"changed\")\n : diffLayer(before, after, a, b, maxLength);\n if (moved.has(b.id)) diff.moved = { before: indexA.get(b.id)!, after: indexB.get(b.id)! };\n if (layerHasChanges(diff)) changed.push(diff);\n }\n\n const camera: ProjectValueChange[] = [];\n const viewA = before.mapView ?? ({} as GeoLibreProject[\"mapView\"]);\n const viewB = after.mapView ?? ({} as GeoLibreProject[\"mapView\"]);\n const centerA = viewA.center ?? [];\n const centerB = viewB.center ?? [];\n if (numbersDiffer(centerA[0], centerB[0]) || numbersDiffer(centerA[1], centerB[1])) {\n const fmt = (c: unknown[]) =>\n c.length === 2 && c.every((n) => typeof n === \"number\")\n ? `${(c[0] as number).toFixed(5)}, ${(c[1] as number).toFixed(5)}`\n : undefined;\n camera.push({ path: \"center\", before: fmt(centerA), after: fmt(centerB) });\n }\n for (const key of [\"zoom\", \"bearing\", \"pitch\"] as const) {\n if (numbersDiffer(viewA[key], viewB[key])) {\n const fmt = (n: unknown) =>\n typeof n === \"number\" ? String(Math.round(n * 100) / 100) : previewValue(n, maxLength);\n camera.push({ path: key, before: fmt(viewA[key]), after: fmt(viewB[key]) });\n }\n }\n\n const basemap: ProjectValueChange[] = [];\n diffRecords(\n {\n basemapStyleUrl: before.basemapStyleUrl,\n basemapVisible: before.basemapVisible,\n basemapOpacity: before.basemapOpacity,\n blankBackgroundColor: before.blankBackgroundColor ?? undefined,\n primaryRenderer: before.primaryRenderer,\n },\n {\n basemapStyleUrl: after.basemapStyleUrl,\n basemapVisible: after.basemapVisible,\n basemapOpacity: after.basemapOpacity,\n blankBackgroundColor: after.blankBackgroundColor ?? undefined,\n primaryRenderer: after.primaryRenderer,\n },\n { depth: 0, maxLength: Math.max(maxLength, 160) },\n basemap,\n );\n\n const projection: ProjectValueChange[] = [];\n const prefsA = (before.preferences ?? {}) as unknown as Record<string, unknown>;\n const prefsB = (after.preferences ?? {}) as unknown as Record<string, unknown>;\n const mapA = isPlainObject(prefsA.map) ? prefsA.map : {};\n const mapB = isPlainObject(prefsB.map) ? prefsB.map : {};\n if (!jsonValuesEqual(mapA.projection, mapB.projection)) {\n projection.push({\n path: \"projection\",\n before: previewValue(mapA.projection, maxLength),\n after: previewValue(mapB.projection, maxLength),\n });\n }\n const preferences: ProjectValueChange[] = [];\n if (!jsonValuesEqual(prefsA, prefsB)) {\n diffRecords(\n { ...prefsA, map: { ...mapA, projection: undefined } },\n { ...prefsB, map: { ...mapB, projection: undefined } },\n { depth: 1, maxLength },\n preferences,\n );\n }\n\n const { plugins, manifests } = diffPlugins(before.plugins, after.plugins, maxLength);\n\n const metadata: ProjectValueChange[] = [];\n if (before.name !== after.name) {\n metadata.push({\n path: \"name\",\n before: previewValue(before.name, maxLength),\n after: previewValue(after.name, maxLength),\n });\n }\n diffRecords(before.metadata, after.metadata, { depth: 0, maxLength }, metadata);\n // Lead with the description, the field users edit, ahead of other keys.\n metadata.sort((x, y) => metadataRank(x.path) - metadataRank(y.path));\n\n const sections: string[] = [];\n const projectA = before as unknown as Record<string, unknown>;\n const projectB = after as unknown as Record<string, unknown>;\n for (const key of new Set([...Object.keys(projectA), ...Object.keys(projectB)])) {\n if (PROJECT_GROUPED_KEYS.has(key)) continue;\n if (!sameForReport(projectA[key], projectB[key])) sections.push(key);\n }\n sections.sort();\n\n const changeCount =\n added.length +\n removed.length +\n changed.reduce((sum, diff) => sum + layerChangeCount(diff), 0) +\n camera.length +\n basemap.length +\n projection.length +\n preferences.length +\n // A plugin counts each changed setting, like a layer counts each key; a\n // bare enable/disable counts once.\n plugins.reduce((sum, plugin) => sum + Math.max(1, plugin.changes.length), 0) +\n manifests.added.length +\n manifests.removed.length +\n metadata.length +\n sections.length;\n\n return {\n layers: { added, removed, changed, reordered: moved.size > 0 },\n camera,\n basemap,\n projection,\n preferences,\n plugins,\n pluginManifests: manifests,\n metadata,\n sections,\n changeCount,\n };\n}\n\nfunction metadataRank(path: string): number {\n if (path === \"name\") return 0;\n if (path === \"description\") return 1;\n return 2;\n}\n"],"mappings":"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"}