ofs-object 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -272,6 +272,15 @@ export declare abstract class BaseGLAdapter implements IMapAdapter {
272
272
  protected whenStyleLoaded(map: any): Promise<void>;
273
273
  bindDataSource(dataSource: OFSDataSource, thematicEngine?: OFSThematicEngine): void;
274
274
  protected syncAllLayers(): void;
275
+ /**
276
+ * Put a raster or vector tile layer on the map, or bring an existing one up to date.
277
+ *
278
+ * Tile layers are registered in the same `glLayers` table as feature layers, so removal, stacking
279
+ * and the clean hand-back in `removeOwnLayers` treat both kinds alike without knowing which is
280
+ * which. What they do not share is the data path: there is no GeoJSON to send, so nothing here
281
+ * touches sources after the first creation except to rebuild when the styling changes.
282
+ */
283
+ syncTileLayer(layer: OFSTileLayer): void;
275
284
  syncLayer(layer: OFSLayerData): void;
276
285
  /** Reordering several layers emits one update per layer; restack once */
277
286
  private scheduleLayerOrder;
@@ -308,9 +317,22 @@ export declare abstract class BaseGLAdapter implements IMapAdapter {
308
317
  * whole source has to be resent (every feature's label text changes at once).
309
318
  */
310
319
  protected syncLabelSource(layer: OFSLayerData): boolean;
320
+ /** Extra options for a layer's new GeoJSON source; an engine that needs some overrides this */
321
+ protected sourceOptionsFor(_layer: OFSLayerData): Record<string, unknown>;
322
+ /**
323
+ * Whether changes to this layer's source can be sent as a diff instead of the whole collection.
324
+ * The default is MapLibre's, whose `updateData` takes a diff on any GeoJSON source. Mapbox's
325
+ * takes something else and only on some sources, so MapboxAdapter overrides this and the next.
326
+ */
327
+ protected supportsPartialUpdate(_layer: OFSLayerData, source: any): boolean;
328
+ /** Send a diff worked out by `updateSourceData`, in the form the engine takes it */
329
+ protected sendPartialUpdate(source: any, diff: {
330
+ add: Feature<Geometry, any>[];
331
+ remove: (string | number)[];
332
+ }): void;
311
333
  /**
312
- * Send changes of a layer to its map source. Uses GeoJSONSource.updateData (MapLibre) with only
313
- * the added / changed / removed features when possible, otherwise falls back to setData.
334
+ * Send changes of a layer to its map source: only the added / changed / removed features when the
335
+ * engine can take a diff (see `supportsPartialUpdate`), otherwise the whole collection via setData.
314
336
  * Changed features are detected by object identity: OFSLayerData replaces the stored object on
315
337
  * every add or update.
316
338
  */
@@ -773,6 +795,16 @@ export declare function colorRampGradientCss(name: string, reverse?: boolean): s
773
795
  */
774
796
  export declare type ColorRampType = 'sequential' | 'diverging' | 'qualitative';
775
797
 
798
+ /**
799
+ * The colours a theme actually puts on the map, for a layer of the given geometry.
800
+ *
801
+ * A simple theme is one colour, but which one depends on the geometry: the fill of a polygon (or
802
+ * its outline when it is drawn unfilled), the stroke of a line, the marker of a point. A
803
+ * categorised or graduated theme puts every class colour it shows on the map. (New layers do not
804
+ * avoid those: see `OFSThematicEngine.defaultColorFor`.)
805
+ */
806
+ export declare function colorsOfTheme(theme: ThematicDefinition, geometry: LayerGeometryType): string[];
807
+
776
808
  export declare interface CommandArgSpec {
777
809
  /** Schema properties filled from the words before any --option, in order */
778
810
  positional?: string[];
@@ -1111,6 +1143,9 @@ export declare interface DistanceMeasure {
1111
1143
  closingMeters: number;
1112
1144
  }
1113
1145
 
1146
+ /** Below this CIEDE2000 distance two map colours are easy to confuse at a glance */
1147
+ export declare const DISTINCT_DELTA_E = 20;
1148
+
1114
1149
  /** Save an exported image in the browser */
1115
1150
  export declare function downloadMapImage(image: MapImage): void;
1116
1151
 
@@ -1478,6 +1513,12 @@ export declare function findIntersectingFeature(dataSource: OFSDataSource, targe
1478
1513
  feature: Feature<Geometry, any>;
1479
1514
  } | null;
1480
1515
 
1516
+ /**
1517
+ * The engine to build a map with, or null when the caller has not supplied one.
1518
+ * `options` wins over the global: passing a library explicitly should always be what gets used.
1519
+ */
1520
+ export declare function findMapEngine(global: MapEngineGlobal, options?: Record<string, any>): any | null;
1521
+
1481
1522
  export declare function forEachPosition(geometry: Geometry, fn: (c: Position) => void): void;
1482
1523
 
1483
1524
  /** Degrees as "45°30'20"" (bearing) or "45.5056°" */
@@ -1983,6 +2024,21 @@ export declare interface LandAreaMeasure {
1983
2024
  raiNganWa: string;
1984
2025
  }
1985
2026
 
2027
+ /**
2028
+ * Chosen by greedy max-min selection in CIEDE2000 over the Tailwind 400-800 chromatic families,
2029
+ * starting from a blue. Every pair is at least 20 apart and every colour is at least 22 from the
2030
+ * reserved colours below.
2031
+ *
2032
+ * The blue is `#2563eb`, not the `#3b82f6` every layer used to get: that one is only 16.8 from the
2033
+ * slate grey used for "all other values", which is too close to tell apart in a legend.
2034
+ *
2035
+ * It stops at 11 because that is where the space runs out: the twelfth pick is already under 20
2036
+ * from its nearest neighbour, and every pick after it is closer still. Past this many layers the
2037
+ * picker generates colours instead (see `pickLayerColor`), and they are as far apart as the space
2038
+ * allows rather than clearly distinct.
2039
+ */
2040
+ export declare const LAYER_COLOR_PALETTE: readonly string[];
2041
+
1986
2042
  export declare type LayerGeometryType = 'point' | 'line' | 'polygon' | 'circle' | 'symbol';
1987
2043
 
1988
2044
  /** Legend group of layers; groups can be nested */
@@ -2015,6 +2071,11 @@ export declare interface LayerMetadata {
2015
2071
  sourceFile?: string;
2016
2072
  /** Attribute schema in source order (kept for export; edited through OFSAttributeTools) */
2017
2073
  fields?: FieldSchema[];
2074
+ /**
2075
+ * Colour to draw the layer with (any CSS colour). Left out, the layer gets one chosen to stand
2076
+ * apart from the layers already on the map - see `pickLayerColor`.
2077
+ */
2078
+ color?: string;
2018
2079
  [key: string]: unknown;
2019
2080
  }
2020
2081
 
@@ -2040,7 +2101,11 @@ export declare interface LegendGroupViewModel {
2040
2101
  export declare interface LegendLayerViewModel {
2041
2102
  id: string;
2042
2103
  name: string;
2043
- type: LayerGeometryType;
2104
+ /**
2105
+ * Geometry for a feature layer, or the tile kind for one served from a tile server. Tile layers
2106
+ * carry no features and no symbology, so `items` is empty and `featureCount` is zero for them.
2107
+ */
2108
+ type: LayerGeometryType | 'raster' | 'vector';
2044
2109
  visible: boolean;
2045
2110
  /** Visible and all parent groups visible */
2046
2111
  rendered: boolean;
@@ -2156,6 +2221,33 @@ export declare function listExpressionVariables(): string[];
2156
2221
 
2157
2222
  export declare function listMarkerIcons(): string[];
2158
2223
 
2224
+ /**
2225
+ * Read a project back onto the map.
2226
+ *
2227
+ * Layers go in before groups are wired up and before the themes are applied, so that a theme never
2228
+ * arrives for a layer that is not there yet.
2229
+ */
2230
+ export declare function loadProject(dataSource: OFSDataSource, thematicEngine: OFSThematicEngine, project: OFSProjectFile, options?: LoadProjectOptions): LoadProjectResult;
2231
+
2232
+ export declare interface LoadProjectOptions {
2233
+ /**
2234
+ * Clear what is already loaded first (default). Turning this off merges the project in, which
2235
+ * replaces any layer whose id it also defines.
2236
+ */
2237
+ replace?: boolean;
2238
+ }
2239
+
2240
+ export declare interface LoadProjectResult {
2241
+ layers: string[];
2242
+ tileLayers: string[];
2243
+ groups: number;
2244
+ features: number;
2245
+ themes: number;
2246
+ view?: ProjectView;
2247
+ /** Layers the file described but carried no data for */
2248
+ emptyLayers: string[];
2249
+ }
2250
+
2159
2251
  export declare function localName(name: string): string;
2160
2252
 
2161
2253
  export declare type LocationPredicate = 'intersects' | 'within' | 'contains' | 'disjoint' | 'touches' | 'crosses' | 'within-distance';
@@ -2169,8 +2261,66 @@ export declare function looksGeographic(bounds: [number, number, number, number]
2169
2261
  */
2170
2262
  export declare function makeCurve(vertices: Coordinate[], bulges: number[], sagToleranceMeters?: number): OfsCurveMeta | null;
2171
2263
 
2264
+ /**
2265
+ * Largest layer given a dynamic source by default. A judgement call rather than a measurement - see
2266
+ * the class comment - and adjustable per adapter through `dynamicSourceMaxFeatures`.
2267
+ */
2268
+ export declare const MAPBOX_DYNAMIC_MAX_FEATURES = 5000;
2269
+
2270
+ /**
2271
+ * Mapbox GL adapter.
2272
+ *
2273
+ * ## Sending only what changed
2274
+ *
2275
+ * MapLibre's `updateData` takes a diff on any GeoJSON source. Mapbox's is a different thing: it
2276
+ * takes a FeatureCollection to merge in by id (a feature with a null geometry deletes), and only on a
2277
+ * source created with `dynamic: true` - on any other source it fires an error event and the update is
2278
+ * silently lost.
2279
+ *
2280
+ * And `dynamic: true` does not just switch that method on. It replaces geojson-vt, which cuts the data
2281
+ * into tiles and simplifies it for low zooms, with an index that scans every feature for every tile
2282
+ * and does not simplify at all; Mapbox's own source calls it the mode for "small data, frequently
2283
+ * updated". For a layer of tens of thousands of parcels that makes every zoomed-out tile carry every
2284
+ * parcel at full detail - far worse than resending the data.
2285
+ *
2286
+ * So a layer gets a dynamic source only while it is small (`dynamicSourceMaxFeatures`) - the drawing
2287
+ * and editing layers that change on every drag, which is what the mode is for - and every other layer
2288
+ * keeps the default source and `setData`. A dynamic layer that grows past the limit, or gains a
2289
+ * GeometryCollection (which dynamic mode cannot take), has its source rebuilt in the default mode. It
2290
+ * is not switched back if it shrinks: `setData` on a small layer is cheap anyway.
2291
+ */
2172
2292
  export declare class MapboxAdapter extends BaseGLAdapter {
2173
2293
  readonly engineName: "mapbox";
2294
+ /** Layers larger than this keep the default source (0 turns dynamic sources off) */
2295
+ dynamicSourceMaxFeatures: number;
2296
+ /** Layers whose source was created with `dynamic: true` */
2297
+ private dynamicSources;
2298
+ protected sourceOptionsFor(layer: OFSLayerData): Record<string, unknown>;
2299
+ protected supportsPartialUpdate(layer: OFSLayerData, source: any): boolean;
2300
+ /** Mapbox merges features in by id; a feature with no geometry is how it is told to delete one */
2301
+ protected sendPartialUpdate(source: any, diff: {
2302
+ add: Feature<Geometry, any>[];
2303
+ remove: (string | number)[];
2304
+ }): void;
2305
+ syncLayer(layer: OFSLayerData): void;
2306
+ protected updateSourceData(layer: OFSLayerData): void;
2307
+ removeLayer(layerId: string): void;
2308
+ private outgrewDynamicSource;
2309
+ /**
2310
+ * Replace a layer's source with one in the default mode. A source cannot change mode in place, and
2311
+ * its GL layers cannot outlive it, so all of them are removed and built again; feature state goes
2312
+ * with the old source, so the selection is put back afterwards.
2313
+ */
2314
+ private rebuildSource;
2315
+ private fitsDynamicSource;
2316
+ /** Mapbox before 3.4 has neither the option nor the method; `peerDependencies` allows >= 3.0 */
2317
+ private engineHasDynamicSources;
2318
+ /**
2319
+ * Build a map in `container`.
2320
+ *
2321
+ * `mapbox-gl` is not imported by this package - see `resolve-engine.ts` - so pass the library in
2322
+ * as `options.mapboxgl`, or load it from a script tag.
2323
+ */
2174
2324
  init(container: HTMLElement | string, options?: Record<string, any>): Promise<void>;
2175
2325
  }
2176
2326
 
@@ -2189,6 +2339,22 @@ export declare interface MapClickEvent {
2189
2339
  originalEvent: MouseEvent;
2190
2340
  }
2191
2341
 
2342
+ /**
2343
+ * Finding the map engine an adapter should build its map with.
2344
+ *
2345
+ * Neither `maplibre-gl` nor `mapbox-gl` is imported anywhere in this package, and that is
2346
+ * deliberate. Both are optional peer dependencies, but a static `import` of one is a hard
2347
+ * dependency for every consumer regardless of what they actually use: the import sits in the
2348
+ * bundle, the bundler has to resolve it, and an application built on Mapbox that never installed
2349
+ * `maplibre-gl` fails at build time with "Can't resolve 'maplibre-gl'" before a line of code runs.
2350
+ * It failed even for applications that create their own map and hand it over with `attach()`, where
2351
+ * the engine is never needed at all.
2352
+ *
2353
+ * So the engine is handed to the adapter rather than imported: through `options`, or from the global
2354
+ * a CDN script tag leaves on `window`.
2355
+ */
2356
+ export declare type MapEngineGlobal = 'maplibregl' | 'mapboxgl';
2357
+
2192
2358
  export declare class MapEngineNotInitializedError extends OFSError {
2193
2359
  constructor(message?: string);
2194
2360
  }
@@ -2220,6 +2386,13 @@ export declare interface MapImage {
2220
2386
 
2221
2387
  export declare class MapLibreAdapter extends BaseGLAdapter {
2222
2388
  readonly engineName: "maplibre";
2389
+ /**
2390
+ * Build a map in `container`.
2391
+ *
2392
+ * `maplibre-gl` is not imported by this package - see `resolve-engine.ts` for why - so pass the
2393
+ * library in as `options.maplibregl`, or load it from a script tag. An application that creates
2394
+ * its own map does not come through here at all; it calls `attach()`.
2395
+ */
2223
2396
  init(container: HTMLElement | string, options?: Record<string, any>): Promise<void>;
2224
2397
  }
2225
2398
 
@@ -2608,6 +2781,14 @@ export declare interface OfsCurveMeta {
2608
2781
 
2609
2782
  export declare class OFSDataSource extends TypedEventEmitter<OFSDataSourceEvents> {
2610
2783
  private layers;
2784
+ /**
2785
+ * Tile layers are kept apart from feature layers on purpose. Everything that reads
2786
+ * `getAllLayers()` - snapping, topology, export, the attribute table, the AI context - wants
2787
+ * features, and a tile layer has none to give. Mixing them into that list would mean auditing
2788
+ * every one of those callers for a case that can only ever be "skip this". `getDisplayLayers()`
2789
+ * brings the two back together for the things that do treat them alike: the legend and the stack.
2790
+ */
2791
+ private tileLayers;
2611
2792
  private groups;
2612
2793
  private activeLayerId;
2613
2794
  private selectionStore;
@@ -2635,6 +2816,7 @@ export declare class OFSDataSource extends TypedEventEmitter<OFSDataSourceEvents
2635
2816
  * Remove a layer by ID
2636
2817
  */
2637
2818
  removeLayer(layerId: string): boolean;
2819
+ private detachLayer;
2638
2820
  /**
2639
2821
  * Get layer by ID
2640
2822
  */
@@ -2648,7 +2830,36 @@ export declare class OFSDataSource extends TypedEventEmitter<OFSDataSourceEvents
2648
2830
  */
2649
2831
  getAllLayers(): OFSLayerData[];
2650
2832
  /**
2651
- * Set layer visibility
2833
+ * Add a raster or vector tile layer.
2834
+ *
2835
+ * ```ts
2836
+ * ds.addTileLayer({ id: 'ortho', name: 'Aerial', kind: 'raster',
2837
+ * tiles: ['https://tiles.example/{z}/{x}/{y}.png'], tileSize: 256, zIndex: -1 });
2838
+ * ```
2839
+ *
2840
+ * A vector tile set also needs `styles`, naming which layer inside the tile to draw and how; a
2841
+ * vector tile carries no styling of its own, so there is nothing to fall back on.
2842
+ */
2843
+ addTileLayer(metadata: TileLayerMetadata): OFSTileLayer;
2844
+ getTileLayer(layerId: string): OFSTileLayer | undefined;
2845
+ /** Tile layers, lowest in the stack first */
2846
+ getTileLayers(): OFSTileLayer[];
2847
+ removeTileLayer(layerId: string): boolean;
2848
+ /** Replace the drawing instructions of a vector tile layer */
2849
+ setTileLayerStyles(layerId: string, styles: OFSTileLayer['styles']): void;
2850
+ /**
2851
+ * Every layer that appears on the map, tile and feature alike, lowest first.
2852
+ *
2853
+ * This is what the legend lists and what decides the drawing order, so a raster basemap at
2854
+ * zIndex -1 sits under the parcels without either side knowing about the other.
2855
+ */
2856
+ getDisplayLayers(): (OFSLayerData | OFSTileLayer)[];
2857
+ /** A layer of either kind, for the settings the two have in common */
2858
+ private findAnyLayer;
2859
+ /** Tell the right listeners that a layer changed, whichever kind it is */
2860
+ private emitLayerChanged;
2861
+ /**
2862
+ * Set layer visibility. Works on tile layers too, so one legend toggle drives both kinds.
2652
2863
  */
2653
2864
  setLayerVisibility(layerId: string, visible: boolean): void;
2654
2865
  /**
@@ -2691,7 +2902,7 @@ export declare class OFSDataSource extends TypedEventEmitter<OFSDataSourceEvents
2691
2902
  moveLayerToGroup(layerId: string, groupId: string | null): void;
2692
2903
  /** True when the layer is inside the group, directly or through sub-groups */
2693
2904
  isLayerInGroup(layerId: string, groupId: string): boolean;
2694
- /** A layer is drawn when it and all of its parent groups are visible */
2905
+ /** A layer of either kind is drawn when it and all of its parent groups are visible */
2695
2906
  isLayerRendered(layerId: string): boolean;
2696
2907
  setActiveLayer(layerId: string | null): void;
2697
2908
  getActiveLayerId(): string | null;
@@ -2810,12 +3021,27 @@ export declare interface OFSDataSourceEvents {
2810
3021
  'layer:added': {
2811
3022
  layer: OFSLayerData;
2812
3023
  };
3024
+ /**
3025
+ * `replaced` is true when the layer went because `addLayer` was given its id again: the same layer
3026
+ * is coming straight back with new data, so whatever is kept per layer (its styling, say) should
3027
+ * stay. False when it was removed for good.
3028
+ */
2813
3029
  'layer:removed': {
2814
3030
  layerId: string;
3031
+ replaced: boolean;
2815
3032
  };
2816
3033
  'layer:updated': {
2817
3034
  layer: OFSLayerData;
2818
3035
  };
3036
+ 'tile-layer:added': {
3037
+ layer: OFSTileLayer;
3038
+ };
3039
+ 'tile-layer:removed': {
3040
+ layerId: string;
3041
+ };
3042
+ 'tile-layer:updated': {
3043
+ layer: OFSTileLayer;
3044
+ };
2819
3045
  'feature:added': {
2820
3046
  layerId: string;
2821
3047
  feature: Feature<Geometry, any>;
@@ -3465,7 +3691,25 @@ export declare interface OFSFeatureMetadata {
3465
3691
 
3466
3692
  export declare class OFSFileIO {
3467
3693
  private dataSource;
3468
- constructor(dataSource: OFSDataSource);
3694
+ private thematicEngine?;
3695
+ /**
3696
+ * `thematicEngine` is only needed for projects, which carry the styling of each layer. File
3697
+ * import and export never touch it, so it stays optional and `saveProject` says so if it is
3698
+ * missing rather than silently writing a project with no symbology in it.
3699
+ */
3700
+ constructor(dataSource: OFSDataSource, thematicEngine?: OFSThematicEngine | undefined);
3701
+ /** Attach the styling engine after construction, when the two are built in that order */
3702
+ useThematicEngine(engine: OFSThematicEngine): void;
3703
+ private requireThemes;
3704
+ /**
3705
+ * Write the whole map out as a project: the layers and their data, the tile layers, the groups,
3706
+ * the styling and the drawing order. See `project.ts`.
3707
+ */
3708
+ saveProject(options?: SaveProjectOptions): OFSProjectFile;
3709
+ /** Read a project back onto the map, from an object or from the text of a `.ofsproj` file */
3710
+ loadProject(project: OFSProjectFile | string, options?: LoadProjectOptions): LoadProjectResult;
3711
+ /** A project as a file ready to be downloaded or written to disk */
3712
+ exportProject(name?: string, options?: SaveProjectOptions): ExportedFile;
3469
3713
  /** Parse and add files as new layers */
3470
3714
  import(files: InputFile[], options?: ImportOptions): ImportToDataSourceResult;
3471
3715
  /** Browser File objects (from <input type="file"> or drag & drop) */
@@ -3580,6 +3824,11 @@ export declare class OFSLayerData {
3580
3824
  sourceCrs?: string;
3581
3825
  sourceFile?: string;
3582
3826
  fields?: LayerMetadata['fields'];
3827
+ /**
3828
+ * The colour the layer was added with, if one was given. This is the starting point only: once
3829
+ * the layer is on the map its colour lives in its theme, and restyling it does not update this.
3830
+ */
3831
+ readonly color?: string;
3583
3832
  private featuresMap;
3584
3833
  private spatialIndex;
3585
3834
  private indexItems;
@@ -3795,6 +4044,8 @@ export declare class OFSObject {
3795
4044
  private dynamicInput;
3796
4045
  /** Map handed over by the host application (config.map), attached on init() */
3797
4046
  private readonly hostMap;
4047
+ /** Defaults for the map init() builds; the engine itself is usually passed here */
4048
+ private readonly mapOptions;
3798
4049
  constructor(config?: OFSObjectConfig);
3799
4050
  /**
3800
4051
  * Initialize map adapter and bind data source.
@@ -3816,12 +4067,29 @@ export declare class OFSObject {
3816
4067
  /**
3817
4068
  * Clean up all resources
3818
4069
  */
4070
+ /**
4071
+ * Save the whole map as a project: layers and their data, tile layers, groups, styling, drawing
4072
+ * order, and where the map is looking. `ofs.io.saveProject()` does the same without the view.
4073
+ */
4074
+ saveProject(options?: SaveProjectOptions): OFSProjectFile;
4075
+ /** Reopen a saved project, and move the map back to where it was looking */
4076
+ loadProject(project: OFSProjectFile | string, options?: LoadProjectOptions): LoadProjectResult;
4077
+ /** Where the map is looking, or undefined before there is a map to ask */
4078
+ private currentView;
3819
4079
  destroy(): void;
3820
4080
  }
3821
4081
 
3822
4082
  export declare interface OFSObjectConfig {
3823
4083
  mapAdapter?: IMapAdapter;
3824
4084
  container?: HTMLElement | string;
4085
+ /**
4086
+ * Options for the map `init()` builds, merged under the ones passed to `init()` itself.
4087
+ *
4088
+ * This is where the map engine goes. Neither `maplibre-gl` nor `mapbox-gl` is imported by this
4089
+ * package, so that an application using one need not install the other, which means the library
4090
+ * has to be handed in: `{ mapOptions: { maplibregl } }` after `import maplibregl from
4091
+ * 'maplibre-gl'`. A map the host creates itself needs none of this - pass it as `map` below.
4092
+ */
3825
4093
  mapOptions?: Record<string, any>;
3826
4094
  /**
3827
4095
  * A MapLibre / Mapbox map the host application created and keeps owning (Angular, React, Vue).
@@ -3839,6 +4107,23 @@ export declare interface OFSObjectConfig {
3839
4107
  console?: IOFSConsole;
3840
4108
  }
3841
4109
 
4110
+ export declare interface OFSProjectFile {
4111
+ format: typeof PROJECT_FORMAT;
4112
+ version: number;
4113
+ savedAt: string;
4114
+ /** False when the file describes the layers and their styling but carries no feature data */
4115
+ includesFeatures: boolean;
4116
+ activeLayerId: string | null;
4117
+ groups: LayerGroup[];
4118
+ layers: ProjectLayer[];
4119
+ /** Tile layers are always small - a URL and some numbers - so they are always written in full */
4120
+ tileLayers: TileLayerMetadata[];
4121
+ themes: ThematicDefinition[];
4122
+ view?: ProjectView;
4123
+ /** Anything the host application wants to keep with the map */
4124
+ extra?: Record<string, unknown>;
4125
+ }
4126
+
3842
4127
  declare class OFSSelectionOverlay {
3843
4128
  private dataSource;
3844
4129
  private mapAdapter;
@@ -3909,7 +4194,34 @@ export declare class OFSSymbologyPanel {
3909
4194
 
3910
4195
  export declare class OFSThematicEngine extends TypedEventEmitter<ThematicEvents> {
3911
4196
  private themes;
3912
- constructor();
4197
+ private readonly random;
4198
+ private dataSource;
4199
+ private unbindDataSource;
4200
+ constructor(options?: ThematicEngineOptions);
4201
+ /**
4202
+ * Let the engine see the layers on the map.
4203
+ *
4204
+ * Without this it cannot know what colours are already in use, so every layer added without a
4205
+ * colour comes out the same default blue. With it, such a layer gets a colour that stands apart
4206
+ * from the others (see `pickLayerColor`), and a colour given in `addLayer({ color })` is applied.
4207
+ *
4208
+ * It also lets the engine forget the theme of a layer that is removed. An engine that is not bound
4209
+ * cannot see removals, and keeps every theme until `removeTheme` is called.
4210
+ *
4211
+ * OFSObject, the map adapter and the legend all call this, so it rarely needs calling by hand.
4212
+ * Binding the data source that is already bound does nothing.
4213
+ */
4214
+ bindDataSource(dataSource: OFSDataSource): void;
4215
+ /**
4216
+ * Give a new layer its theme as it is added rather than when it is first drawn. Colours are then
4217
+ * chosen in the order layers arrive - not the order some view happens to render them - and they
4218
+ * are in the theme from the start, so a project saved before the map has drawn still keeps them.
4219
+ *
4220
+ * When `addLayer` replaces a layer with the same id, the theme is still there. A colour passed to
4221
+ * `addLayer` is explicit, so it wins; without one, the layer keeps the styling it had - refreshing
4222
+ * a layer's data should not undo the way someone classified it.
4223
+ */
4224
+ private styleNewLayer;
3913
4225
  /**
3914
4226
  * Get current theme definition for a layer
3915
4227
  */
@@ -3969,6 +4281,17 @@ export declare class OFSThematicEngine extends TypedEventEmitter<ThematicEvents>
3969
4281
  private classList;
3970
4282
  private requireTheme;
3971
4283
  private defaultTheme;
4284
+ /**
4285
+ * Colour for a layer that has no theme yet: the one it was added with; otherwise, when the engine
4286
+ * can see the map, one clear of the colours the other layers show; otherwise the default blue.
4287
+ *
4288
+ * Only layers still on the map count, which is why this walks the layers rather than the themes.
4289
+ *
4290
+ * Only simple (single-colour) layers count, too. A categorised or graduated layer is read by its
4291
+ * legend - its colours stand for values, not for the layer - and a single ramp can take up most
4292
+ * of the palette, which would leave every new layer with a leftover colour.
4293
+ */
4294
+ private defaultColorFor;
3972
4295
  /**
3973
4296
  * Theme of a layer, creating the default simple theme silently (no event) when it has none
3974
4297
  */
@@ -3979,6 +4302,38 @@ export declare class OFSThematicEngine extends TypedEventEmitter<ThematicEvents>
3979
4302
  compile(layer: OFSLayerData): CompiledLayerStyle;
3980
4303
  }
3981
4304
 
4305
+ export declare class OFSTileLayer {
4306
+ readonly id: string;
4307
+ readonly kind: TileLayerKind;
4308
+ name: string;
4309
+ zIndex: number;
4310
+ visible: boolean;
4311
+ opacity: number;
4312
+ pinned: boolean;
4313
+ tag?: string;
4314
+ groupId?: string;
4315
+ readonly tiles: string[];
4316
+ readonly url?: string;
4317
+ readonly tileSize: number;
4318
+ readonly scheme: 'xyz' | 'tms';
4319
+ readonly minzoom?: number;
4320
+ readonly maxzoom?: number;
4321
+ readonly bounds?: BoundingBox;
4322
+ readonly attribution?: string;
4323
+ styles: VectorTileStyle[];
4324
+ readonly metadata: TileLayerMetadata;
4325
+ constructor(metadata: TileLayerMetadata);
4326
+ /** The source definition a GL map engine expects. Undefined keys are dropped, not sent as null. */
4327
+ toSourceSpec(): Record<string, any>;
4328
+ /**
4329
+ * The GL layers to draw this tile set with, bottom first.
4330
+ *
4331
+ * Opacity is folded into the paint properties rather than applied to the layer as a whole,
4332
+ * because GL styles have no layer-wide opacity: each type names its own.
4333
+ */
4334
+ toLayerSpecs(): Record<string, any>[];
4335
+ }
4336
+
3982
4337
  export declare class OFSTopologyPanel {
3983
4338
  private readonly dataSource;
3984
4339
  private readonly topology;
@@ -4265,6 +4620,9 @@ export declare function parseLngLat(raw: string): Coordinate | null;
4265
4620
  /** Parse a number, or null when the text is not a finite number */
4266
4621
  export declare function parseNumber(text: string): number | null;
4267
4622
 
4623
+ /** Parse a project from the text of a `.ofsproj` file */
4624
+ export declare function parseProject(text: string): OFSProjectFile;
4625
+
4268
4626
  export declare function parseWkb(bytes: Uint8Array, offset?: number): {
4269
4627
  geometry: Geometry;
4270
4628
  end: number;
@@ -4277,6 +4635,30 @@ export declare function parseXml(source: string): XmlElement;
4277
4635
  /** Boundary paths: polygon rings or line parts */
4278
4636
  export declare function pathsOf(geometry: Geometry): Position[][];
4279
4637
 
4638
+ /**
4639
+ * A colour for a new layer that is clearly different from the layers already on the map.
4640
+ *
4641
+ * In order, it takes a random palette colour that is distinct from:
4642
+ * 1. every layer on the map, visible or hidden;
4643
+ * 2. every visible layer (it may match a hidden one);
4644
+ * and when neither exists - more than about eleven layers - the colour furthest from the visible layers, out of the palette and a batch of random
4645
+ * colours. It always returns a colour: a layer that looks like another is a nuisance, a layer that
4646
+ * cannot be added is a failure.
4647
+ */
4648
+ export declare function pickLayerColor(options?: PickLayerColorOptions): string;
4649
+
4650
+ export declare interface PickLayerColorOptions {
4651
+ /** Colours of the layers currently shown. The new colour keeps clear of these first. */
4652
+ visible?: readonly string[];
4653
+ /**
4654
+ * Colours of layers that are loaded but hidden. Avoided as well while that is possible - a hidden
4655
+ * layer is one click from being shown - but given up before the visible ones are.
4656
+ */
4657
+ hidden?: readonly string[];
4658
+ /** Source of randomness in [0, 1); pass a seeded one to make the choice reproducible */
4659
+ random?: () => number;
4660
+ }
4661
+
4280
4662
  export declare interface PlanarArc {
4281
4663
  center: PlanarPoint;
4282
4664
  radius: number;
@@ -4346,9 +4728,27 @@ export declare function printImportResult(consoleService: IOFSConsole, result: I
4346
4728
  /** Print an import / export report: summary lines, then issues grouped by code */
4347
4729
  export declare function printIssues(consoleService: IOFSConsole, issues: ImportIssue[], limit?: number): void;
4348
4730
 
4731
+ export declare const PROJECT_FORMAT = "ofs-project";
4732
+
4733
+ export declare const PROJECT_VERSION = 1;
4734
+
4349
4735
  /** Plane to do the arc maths on, centred on the curve so distances come out in ground meters */
4350
4736
  export declare function projectionForVertices(vertices: Coordinate[]): PlanarProjection;
4351
4737
 
4738
+ export declare interface ProjectLayer {
4739
+ metadata: LayerMetadata;
4740
+ /** Left out when the project was saved without feature data */
4741
+ features?: Feature<Geometry, any>[];
4742
+ }
4743
+
4744
+ /** Where the map was looking when the project was saved */
4745
+ export declare interface ProjectView {
4746
+ center: Coordinate;
4747
+ zoom: number;
4748
+ bearing?: number;
4749
+ pitch?: number;
4750
+ }
4751
+
4352
4752
  export declare class PromptBuilder {
4353
4753
  /**
4354
4754
  * Summarize current map state for LLM prompt context.
@@ -4425,6 +4825,16 @@ export declare function renderSymbolSwatchSvg(symbol: SymbolStyle, geomType: Lay
4425
4825
  /** Report an error that was caught and recovered from */
4426
4826
  export declare function reportInternalError(scope: string, error: unknown): void;
4427
4827
 
4828
+ /** The engine to build a map with, or an error saying how to supply one */
4829
+ export declare function requireMapEngine(global: MapEngineGlobal, options?: Record<string, any>): any;
4830
+
4831
+ /**
4832
+ * Colours a layer must never be given, whatever else is on the map: the selection highlight (a
4833
+ * yellow layer hides what is selected on it), the grey used for "all other values" in a categorised
4834
+ * layer, and white and black, which vanish against light and dark basemaps.
4835
+ */
4836
+ export declare const RESERVED_LAYER_COLORS: readonly string[];
4837
+
4428
4838
  /**
4429
4839
  * Resolves all selected features across all layers in the dataset,
4430
4840
  * prioritizing active layer features first, followed by other layers,
@@ -4548,6 +4958,21 @@ export declare function runTopologyChecks(features: AnyFeature[], layer: {
4548
4958
  name?: string;
4549
4959
  }, options?: TopologyOptions): Generator<TopologyProgress, TopologyReport, void>;
4550
4960
 
4961
+ /** Write the current map out as a project */
4962
+ export declare function saveProject(dataSource: OFSDataSource, thematicEngine: OFSThematicEngine, options?: SaveProjectOptions): OFSProjectFile;
4963
+
4964
+ export declare interface SaveProjectOptions {
4965
+ /**
4966
+ * Write the features into the file (default). Turning this off keeps the layers, their styling
4967
+ * and their order but leaves the data out, for a project meant to be reconnected to a database
4968
+ * or to files that live elsewhere - and for a project of 98,000 parcels, which is 60 MB of
4969
+ * GeoJSON nobody wants in version control.
4970
+ */
4971
+ features?: boolean;
4972
+ view?: ProjectView;
4973
+ extra?: Record<string, unknown>;
4974
+ }
4975
+
4551
4976
  export declare interface ScaleBarPlan {
4552
4977
  /** Ground distance the bar represents, in meters */
4553
4978
  meters: number;
@@ -5001,6 +5426,14 @@ export declare interface ThematicDefinition {
5001
5426
  label?: LabelSettings;
5002
5427
  }
5003
5428
 
5429
+ export declare interface ThematicEngineOptions {
5430
+ /**
5431
+ * Randomness behind the colours given to layers added without one, in [0, 1). Pass a seeded
5432
+ * function to get the same colours every run (tests, screenshots, demos).
5433
+ */
5434
+ random?: () => number;
5435
+ }
5436
+
5004
5437
  export declare interface ThematicEvents {
5005
5438
  'theme:changed': {
5006
5439
  layerId: string;
@@ -5023,6 +5456,40 @@ export declare interface ThematicEvents {
5023
5456
  */
5024
5457
  export declare type ThematicType = 'simple' | 'categorical' | 'graduated';
5025
5458
 
5459
+ export declare type TileLayerKind = 'raster' | 'vector';
5460
+
5461
+ export declare interface TileLayerMetadata {
5462
+ id: string;
5463
+ name: string;
5464
+ kind: TileLayerKind;
5465
+ /**
5466
+ * Tile URL templates with `{z}`, `{x}` and `{y}` placeholders. Several entries are read as
5467
+ * mirrors of the same tile set, which is how a server spreads load across subdomains.
5468
+ */
5469
+ tiles?: string[];
5470
+ /** A TileJSON document describing the tile set, as an alternative to `tiles` */
5471
+ url?: string;
5472
+ /** Raster tile edge in pixels; 512 for the retina tiles many providers serve */
5473
+ tileSize?: number;
5474
+ /** `tms` counts rows from the bottom, the opposite of the usual `xyz` */
5475
+ scheme?: 'xyz' | 'tms';
5476
+ minzoom?: number;
5477
+ maxzoom?: number;
5478
+ /** Area the tile set covers; outside it the map does not ask for tiles */
5479
+ bounds?: BoundingBox;
5480
+ /** Credit line the provider requires; map engines show it in the corner */
5481
+ attribution?: string;
5482
+ /** Required for `vector`, meaningless for `raster` */
5483
+ styles?: VectorTileStyle[];
5484
+ zIndex?: number;
5485
+ visible?: boolean;
5486
+ opacity?: number;
5487
+ pinned?: boolean;
5488
+ tag?: string;
5489
+ groupId?: string;
5490
+ [key: string]: unknown;
5491
+ }
5492
+
5026
5493
  /** Split on whitespace, keeping "quoted text" together */
5027
5494
  export declare function tokenizeArgs(raw: string): string[];
5028
5495
 
@@ -5299,6 +5766,14 @@ export declare function validateFieldName(name: string, existing: FieldSchema[])
5299
5766
  */
5300
5767
  export declare function validateGeometry(geometry: Geometry | null | undefined, index: number, issues: ImportIssue[], ctx?: ValidationContext): Geometry | null;
5301
5768
 
5769
+ /**
5770
+ * Check that a value really is a project this build can open, and say what is wrong when it is not.
5771
+ *
5772
+ * A file from a newer release is refused rather than half-read: reading the parts that happen to
5773
+ * look familiar and quietly dropping the rest produces a map that looks plausible and is wrong.
5774
+ */
5775
+ export declare function validateProject(value: unknown): OFSProjectFile;
5776
+
5302
5777
  export declare interface ValidationContext {
5303
5778
  file?: string;
5304
5779
  closeRings?: boolean;
@@ -5306,6 +5781,27 @@ export declare interface ValidationContext {
5306
5781
  geographic?: boolean;
5307
5782
  }
5308
5783
 
5784
+ /**
5785
+ * How one named layer inside a vector tile should be drawn.
5786
+ *
5787
+ * Vector tiles carry several layers ("roads", "buildings", "parcels") and no instructions for
5788
+ * drawing any of them, so the caller says which to draw and how. Nothing is guessed: a tile set
5789
+ * whose styling is left out renders nothing, which is a silent blank map, so it is refused instead.
5790
+ */
5791
+ export declare interface VectorTileStyle {
5792
+ /** Name of the layer inside the tile, as the tile server publishes it */
5793
+ sourceLayer: string;
5794
+ type: 'fill' | 'line' | 'circle' | 'symbol';
5795
+ /** GL paint properties, passed through as given */
5796
+ paint?: Record<string, any>;
5797
+ layout?: Record<string, any>;
5798
+ filter?: any[];
5799
+ minzoom?: number;
5800
+ maxzoom?: number;
5801
+ /** Suffix for the GL layer id; needed when one source layer is drawn more than once */
5802
+ id?: string;
5803
+ }
5804
+
5309
5805
  export declare type WorkflowStatus = 'idle' | 'running' | 'waiting_input' | 'completed' | 'failed' | 'cancelled';
5310
5806
 
5311
5807
  /** Convert a world math angle (radians, CCW from East) to a user angle in the frame's convention [0, 360) */