ofs-object 0.1.0-beta.3 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,38 @@ npm install mapbox-gl
47
47
 
48
48
  > MapLibre v6 เป็น ES module ล้วน (ไม่มี CommonJS) ถ้าจะใช้ `require()` ใน Node ต้องใช้ MapLibre v5 หรือใช้ Mapbox แทน — ผ่าน bundler (Angular, Vite, webpack) ใช้ได้ตามปกติ
49
49
 
50
+ #### ⚠️ ต้องส่ง Map Engine เข้ามาเอง
51
+
52
+ **ไลบรารีนี้ไม่ `import` map engine ตัวไหนเลย** — เพราะถ้า import ตัวใดตัวหนึ่งไว้ มันจะกลายเป็น
53
+ dependency บังคับของทุกคน คนที่ใช้ Mapbox และไม่ได้ลง `maplibre-gl` จะ build ไม่ผ่านด้วย
54
+ `Can't resolve 'maplibre-gl'` ตั้งแต่ยังไม่ได้รันโค้ด
55
+
56
+ ดังนั้นตอนให้ `init()` สร้างแผนที่ ต้องส่ง library เข้ามาด้วย:
57
+
58
+ ```typescript
59
+ // MapLibre v6 ไม่มี default export → ใช้ namespace import
60
+ import * as maplibregl from 'maplibre-gl';
61
+
62
+ const ofs = new OFSObject({ mapOptions: { maplibregl } });
63
+ await ofs.init('map', { center: [100.5, 13.7], zoom: 12 });
64
+ ```
65
+
66
+ ```typescript
67
+ // Mapbox
68
+ import mapboxgl from 'mapbox-gl';
69
+
70
+ const ofs = new OFSObject({
71
+ mapAdapter: new MapboxAdapter(),
72
+ mapOptions: { mapboxgl, accessToken: 'pk....' },
73
+ });
74
+ await ofs.init('map');
75
+ ```
76
+
77
+ หรือโหลดจาก `<script>` tag ก็ได้ ไลบรารีจะอ่านจาก `window.maplibregl` / `window.mapboxgl` ให้เอง
78
+
79
+ > **ถ้าแอปสร้าง Map เอง** (แนะนำสำหรับ Angular/React/Vue — ดูหัวข้อด้านล่าง) แล้วส่งเข้ามาทาง
80
+ > `new OFSObject({ map })` **ไม่ต้องส่ง engine เลย** เพราะไลบรารีไม่ต้องสร้างแผนที่
81
+
50
82
  ### Stylesheet
51
83
 
52
84
  Widget ที่ไลบรารี mount ให้ (Command Console, Legend, แผง Symbology, ตาราง Attribute, แผง Topology) ต้องการไฟล์ CSS หนึ่งไฟล์:
@@ -95,12 +127,15 @@ npm install git+ssh://git@github.com/<user>/OnFarmObject.git#main
95
127
 
96
128
  ```typescript
97
129
  import { OFSObject, MapLibreAdapter, LegendWidget } from 'ofs-object';
130
+ import * as maplibregl from 'maplibre-gl';
98
131
  import 'maplibre-gl/dist/maplibre-gl.css';
99
132
 
100
133
  async function initMap() {
101
134
  // 1. สร้าง Instance หลักของ OFSObject (ใช้ MapLibre เป็นค่าเริ่มต้น)
135
+ // ต้องส่ง maplibregl เข้ามา เพราะไลบรารีไม่ import map engine เอง
102
136
  const ofs = new OFSObject({
103
137
  mapAdapter: new MapLibreAdapter(),
138
+ mapOptions: { maplibregl },
104
139
  });
105
140
 
106
141
  // 2. กำหนดแผนที่ลงบน HTML Container Element id="map"
@@ -259,8 +294,9 @@ initMap();
259
294
 
260
295
  <script type="module">
261
296
  import { OFSObject } from 'ofs-object';
297
+ import * as maplibregl from 'maplibre-gl';
262
298
 
263
- const ofs = new OFSObject();
299
+ const ofs = new OFSObject({ mapOptions: { maplibregl } });
264
300
  await ofs.init('map');
265
301
  ofs.consoleWidget.mount('console-mount');
266
302
  </script>
@@ -273,6 +309,7 @@ initMap();
273
309
  ```tsx
274
310
  import React, { useEffect, useRef } from 'react';
275
311
  import { OFSObject, LegendWidget } from 'ofs-object';
312
+ import * as maplibregl from 'maplibre-gl';
276
313
  import 'maplibre-gl/dist/maplibre-gl.css';
277
314
 
278
315
  export const MapView: React.FC = () => {
@@ -283,7 +320,7 @@ export const MapView: React.FC = () => {
283
320
  useEffect(() => {
284
321
  if (!mapContainerRef.current) return;
285
322
 
286
- const ofs = new OFSObject();
323
+ const ofs = new OFSObject({ mapOptions: { maplibregl } });
287
324
  ofsRef.current = ofs;
288
325
 
289
326
  ofs.init(mapContainerRef.current, {
@@ -339,7 +376,7 @@ export class MapViewComponent implements OnInit, OnDestroy {
339
376
  private ofs!: OFSObject;
340
377
 
341
378
  async ngOnInit() {
342
- this.ofs = new OFSObject();
379
+ this.ofs = new OFSObject({ mapOptions: { maplibregl } });
343
380
  await this.ofs.init(this.mapContainer.nativeElement, {
344
381
  center: [100.5, 13.7],
345
382
  zoom: 13,
@@ -455,6 +492,7 @@ const ofs = new OFSObject({ map, console: myAngularConsole });
455
492
  <script setup lang="ts">
456
493
  import { onMounted, onUnmounted, ref } from 'vue';
457
494
  import { OFSObject, LegendWidget } from 'ofs-object';
495
+ import * as maplibregl from 'maplibre-gl';
458
496
  import 'maplibre-gl/dist/maplibre-gl.css';
459
497
 
460
498
  const mapContainer = ref<HTMLDivElement | null>(null);
@@ -464,7 +502,7 @@ let ofs: OFSObject | null = null;
464
502
  onMounted(async () => {
465
503
  if (!mapContainer.value) return;
466
504
 
467
- ofs = new OFSObject();
505
+ ofs = new OFSObject({ mapOptions: { maplibregl } });
468
506
  await ofs.init(mapContainer.value, {
469
507
  center: [100.5, 13.7],
470
508
  zoom: 12,
@@ -507,5 +545,4 @@ onUnmounted(() => {
507
545
  ---
508
546
 
509
547
  ## สัญญาอนุญาต (License)
510
-
511
- ไลบรารีนี้เผยแพร่ภายใต้สัญญาอนุญาต **[MIT License](LICENSE)** © On-Farm Solution Co.,Ltd.
548
+ **[MIT License](LICENSE)** © On-Farm Solution Co.,Ltd.
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;
@@ -1478,6 +1487,12 @@ export declare function findIntersectingFeature(dataSource: OFSDataSource, targe
1478
1487
  feature: Feature<Geometry, any>;
1479
1488
  } | null;
1480
1489
 
1490
+ /**
1491
+ * The engine to build a map with, or null when the caller has not supplied one.
1492
+ * `options` wins over the global: passing a library explicitly should always be what gets used.
1493
+ */
1494
+ export declare function findMapEngine(global: MapEngineGlobal, options?: Record<string, any>): any | null;
1495
+
1481
1496
  export declare function forEachPosition(geometry: Geometry, fn: (c: Position) => void): void;
1482
1497
 
1483
1498
  /** Degrees as "45°30'20"" (bearing) or "45.5056°" */
@@ -2040,7 +2055,11 @@ export declare interface LegendGroupViewModel {
2040
2055
  export declare interface LegendLayerViewModel {
2041
2056
  id: string;
2042
2057
  name: string;
2043
- type: LayerGeometryType;
2058
+ /**
2059
+ * Geometry for a feature layer, or the tile kind for one served from a tile server. Tile layers
2060
+ * carry no features and no symbology, so `items` is empty and `featureCount` is zero for them.
2061
+ */
2062
+ type: LayerGeometryType | 'raster' | 'vector';
2044
2063
  visible: boolean;
2045
2064
  /** Visible and all parent groups visible */
2046
2065
  rendered: boolean;
@@ -2156,6 +2175,33 @@ export declare function listExpressionVariables(): string[];
2156
2175
 
2157
2176
  export declare function listMarkerIcons(): string[];
2158
2177
 
2178
+ /**
2179
+ * Read a project back onto the map.
2180
+ *
2181
+ * Layers go in before groups are wired up and before the themes are applied, so that a theme never
2182
+ * arrives for a layer that is not there yet.
2183
+ */
2184
+ export declare function loadProject(dataSource: OFSDataSource, thematicEngine: OFSThematicEngine, project: OFSProjectFile, options?: LoadProjectOptions): LoadProjectResult;
2185
+
2186
+ export declare interface LoadProjectOptions {
2187
+ /**
2188
+ * Clear what is already loaded first (default). Turning this off merges the project in, which
2189
+ * replaces any layer whose id it also defines.
2190
+ */
2191
+ replace?: boolean;
2192
+ }
2193
+
2194
+ export declare interface LoadProjectResult {
2195
+ layers: string[];
2196
+ tileLayers: string[];
2197
+ groups: number;
2198
+ features: number;
2199
+ themes: number;
2200
+ view?: ProjectView;
2201
+ /** Layers the file described but carried no data for */
2202
+ emptyLayers: string[];
2203
+ }
2204
+
2159
2205
  export declare function localName(name: string): string;
2160
2206
 
2161
2207
  export declare type LocationPredicate = 'intersects' | 'within' | 'contains' | 'disjoint' | 'touches' | 'crosses' | 'within-distance';
@@ -2171,6 +2217,12 @@ export declare function makeCurve(vertices: Coordinate[], bulges: number[], sagT
2171
2217
 
2172
2218
  export declare class MapboxAdapter extends BaseGLAdapter {
2173
2219
  readonly engineName: "mapbox";
2220
+ /**
2221
+ * Build a map in `container`.
2222
+ *
2223
+ * `mapbox-gl` is not imported by this package - see `resolve-engine.ts` - so pass the library in
2224
+ * as `options.mapboxgl`, or load it from a script tag.
2225
+ */
2174
2226
  init(container: HTMLElement | string, options?: Record<string, any>): Promise<void>;
2175
2227
  }
2176
2228
 
@@ -2189,6 +2241,22 @@ export declare interface MapClickEvent {
2189
2241
  originalEvent: MouseEvent;
2190
2242
  }
2191
2243
 
2244
+ /**
2245
+ * Finding the map engine an adapter should build its map with.
2246
+ *
2247
+ * Neither `maplibre-gl` nor `mapbox-gl` is imported anywhere in this package, and that is
2248
+ * deliberate. Both are optional peer dependencies, but a static `import` of one is a hard
2249
+ * dependency for every consumer regardless of what they actually use: the import sits in the
2250
+ * bundle, the bundler has to resolve it, and an application built on Mapbox that never installed
2251
+ * `maplibre-gl` fails at build time with "Can't resolve 'maplibre-gl'" before a line of code runs.
2252
+ * It failed even for applications that create their own map and hand it over with `attach()`, where
2253
+ * the engine is never needed at all.
2254
+ *
2255
+ * So the engine is handed to the adapter rather than imported: through `options`, or from the global
2256
+ * a CDN script tag leaves on `window`.
2257
+ */
2258
+ export declare type MapEngineGlobal = 'maplibregl' | 'mapboxgl';
2259
+
2192
2260
  export declare class MapEngineNotInitializedError extends OFSError {
2193
2261
  constructor(message?: string);
2194
2262
  }
@@ -2220,6 +2288,13 @@ export declare interface MapImage {
2220
2288
 
2221
2289
  export declare class MapLibreAdapter extends BaseGLAdapter {
2222
2290
  readonly engineName: "maplibre";
2291
+ /**
2292
+ * Build a map in `container`.
2293
+ *
2294
+ * `maplibre-gl` is not imported by this package - see `resolve-engine.ts` for why - so pass the
2295
+ * library in as `options.maplibregl`, or load it from a script tag. An application that creates
2296
+ * its own map does not come through here at all; it calls `attach()`.
2297
+ */
2223
2298
  init(container: HTMLElement | string, options?: Record<string, any>): Promise<void>;
2224
2299
  }
2225
2300
 
@@ -2608,6 +2683,14 @@ export declare interface OfsCurveMeta {
2608
2683
 
2609
2684
  export declare class OFSDataSource extends TypedEventEmitter<OFSDataSourceEvents> {
2610
2685
  private layers;
2686
+ /**
2687
+ * Tile layers are kept apart from feature layers on purpose. Everything that reads
2688
+ * `getAllLayers()` - snapping, topology, export, the attribute table, the AI context - wants
2689
+ * features, and a tile layer has none to give. Mixing them into that list would mean auditing
2690
+ * every one of those callers for a case that can only ever be "skip this". `getDisplayLayers()`
2691
+ * brings the two back together for the things that do treat them alike: the legend and the stack.
2692
+ */
2693
+ private tileLayers;
2611
2694
  private groups;
2612
2695
  private activeLayerId;
2613
2696
  private selectionStore;
@@ -2648,7 +2731,36 @@ export declare class OFSDataSource extends TypedEventEmitter<OFSDataSourceEvents
2648
2731
  */
2649
2732
  getAllLayers(): OFSLayerData[];
2650
2733
  /**
2651
- * Set layer visibility
2734
+ * Add a raster or vector tile layer.
2735
+ *
2736
+ * ```ts
2737
+ * ds.addTileLayer({ id: 'ortho', name: 'Aerial', kind: 'raster',
2738
+ * tiles: ['https://tiles.example/{z}/{x}/{y}.png'], tileSize: 256, zIndex: -1 });
2739
+ * ```
2740
+ *
2741
+ * A vector tile set also needs `styles`, naming which layer inside the tile to draw and how; a
2742
+ * vector tile carries no styling of its own, so there is nothing to fall back on.
2743
+ */
2744
+ addTileLayer(metadata: TileLayerMetadata): OFSTileLayer;
2745
+ getTileLayer(layerId: string): OFSTileLayer | undefined;
2746
+ /** Tile layers, lowest in the stack first */
2747
+ getTileLayers(): OFSTileLayer[];
2748
+ removeTileLayer(layerId: string): boolean;
2749
+ /** Replace the drawing instructions of a vector tile layer */
2750
+ setTileLayerStyles(layerId: string, styles: OFSTileLayer['styles']): void;
2751
+ /**
2752
+ * Every layer that appears on the map, tile and feature alike, lowest first.
2753
+ *
2754
+ * This is what the legend lists and what decides the drawing order, so a raster basemap at
2755
+ * zIndex -1 sits under the parcels without either side knowing about the other.
2756
+ */
2757
+ getDisplayLayers(): (OFSLayerData | OFSTileLayer)[];
2758
+ /** A layer of either kind, for the settings the two have in common */
2759
+ private findAnyLayer;
2760
+ /** Tell the right listeners that a layer changed, whichever kind it is */
2761
+ private emitLayerChanged;
2762
+ /**
2763
+ * Set layer visibility. Works on tile layers too, so one legend toggle drives both kinds.
2652
2764
  */
2653
2765
  setLayerVisibility(layerId: string, visible: boolean): void;
2654
2766
  /**
@@ -2691,7 +2803,7 @@ export declare class OFSDataSource extends TypedEventEmitter<OFSDataSourceEvents
2691
2803
  moveLayerToGroup(layerId: string, groupId: string | null): void;
2692
2804
  /** True when the layer is inside the group, directly or through sub-groups */
2693
2805
  isLayerInGroup(layerId: string, groupId: string): boolean;
2694
- /** A layer is drawn when it and all of its parent groups are visible */
2806
+ /** A layer of either kind is drawn when it and all of its parent groups are visible */
2695
2807
  isLayerRendered(layerId: string): boolean;
2696
2808
  setActiveLayer(layerId: string | null): void;
2697
2809
  getActiveLayerId(): string | null;
@@ -2816,6 +2928,15 @@ export declare interface OFSDataSourceEvents {
2816
2928
  'layer:updated': {
2817
2929
  layer: OFSLayerData;
2818
2930
  };
2931
+ 'tile-layer:added': {
2932
+ layer: OFSTileLayer;
2933
+ };
2934
+ 'tile-layer:removed': {
2935
+ layerId: string;
2936
+ };
2937
+ 'tile-layer:updated': {
2938
+ layer: OFSTileLayer;
2939
+ };
2819
2940
  'feature:added': {
2820
2941
  layerId: string;
2821
2942
  feature: Feature<Geometry, any>;
@@ -3465,7 +3586,25 @@ export declare interface OFSFeatureMetadata {
3465
3586
 
3466
3587
  export declare class OFSFileIO {
3467
3588
  private dataSource;
3468
- constructor(dataSource: OFSDataSource);
3589
+ private thematicEngine?;
3590
+ /**
3591
+ * `thematicEngine` is only needed for projects, which carry the styling of each layer. File
3592
+ * import and export never touch it, so it stays optional and `saveProject` says so if it is
3593
+ * missing rather than silently writing a project with no symbology in it.
3594
+ */
3595
+ constructor(dataSource: OFSDataSource, thematicEngine?: OFSThematicEngine | undefined);
3596
+ /** Attach the styling engine after construction, when the two are built in that order */
3597
+ useThematicEngine(engine: OFSThematicEngine): void;
3598
+ private requireThemes;
3599
+ /**
3600
+ * Write the whole map out as a project: the layers and their data, the tile layers, the groups,
3601
+ * the styling and the drawing order. See `project.ts`.
3602
+ */
3603
+ saveProject(options?: SaveProjectOptions): OFSProjectFile;
3604
+ /** Read a project back onto the map, from an object or from the text of a `.ofsproj` file */
3605
+ loadProject(project: OFSProjectFile | string, options?: LoadProjectOptions): LoadProjectResult;
3606
+ /** A project as a file ready to be downloaded or written to disk */
3607
+ exportProject(name?: string, options?: SaveProjectOptions): ExportedFile;
3469
3608
  /** Parse and add files as new layers */
3470
3609
  import(files: InputFile[], options?: ImportOptions): ImportToDataSourceResult;
3471
3610
  /** Browser File objects (from <input type="file"> or drag & drop) */
@@ -3795,6 +3934,8 @@ export declare class OFSObject {
3795
3934
  private dynamicInput;
3796
3935
  /** Map handed over by the host application (config.map), attached on init() */
3797
3936
  private readonly hostMap;
3937
+ /** Defaults for the map init() builds; the engine itself is usually passed here */
3938
+ private readonly mapOptions;
3798
3939
  constructor(config?: OFSObjectConfig);
3799
3940
  /**
3800
3941
  * Initialize map adapter and bind data source.
@@ -3816,12 +3957,29 @@ export declare class OFSObject {
3816
3957
  /**
3817
3958
  * Clean up all resources
3818
3959
  */
3960
+ /**
3961
+ * Save the whole map as a project: layers and their data, tile layers, groups, styling, drawing
3962
+ * order, and where the map is looking. `ofs.io.saveProject()` does the same without the view.
3963
+ */
3964
+ saveProject(options?: SaveProjectOptions): OFSProjectFile;
3965
+ /** Reopen a saved project, and move the map back to where it was looking */
3966
+ loadProject(project: OFSProjectFile | string, options?: LoadProjectOptions): LoadProjectResult;
3967
+ /** Where the map is looking, or undefined before there is a map to ask */
3968
+ private currentView;
3819
3969
  destroy(): void;
3820
3970
  }
3821
3971
 
3822
3972
  export declare interface OFSObjectConfig {
3823
3973
  mapAdapter?: IMapAdapter;
3824
3974
  container?: HTMLElement | string;
3975
+ /**
3976
+ * Options for the map `init()` builds, merged under the ones passed to `init()` itself.
3977
+ *
3978
+ * This is where the map engine goes. Neither `maplibre-gl` nor `mapbox-gl` is imported by this
3979
+ * package, so that an application using one need not install the other, which means the library
3980
+ * has to be handed in: `{ mapOptions: { maplibregl } }` after `import maplibregl from
3981
+ * 'maplibre-gl'`. A map the host creates itself needs none of this - pass it as `map` below.
3982
+ */
3825
3983
  mapOptions?: Record<string, any>;
3826
3984
  /**
3827
3985
  * A MapLibre / Mapbox map the host application created and keeps owning (Angular, React, Vue).
@@ -3839,6 +3997,23 @@ export declare interface OFSObjectConfig {
3839
3997
  console?: IOFSConsole;
3840
3998
  }
3841
3999
 
4000
+ export declare interface OFSProjectFile {
4001
+ format: typeof PROJECT_FORMAT;
4002
+ version: number;
4003
+ savedAt: string;
4004
+ /** False when the file describes the layers and their styling but carries no feature data */
4005
+ includesFeatures: boolean;
4006
+ activeLayerId: string | null;
4007
+ groups: LayerGroup[];
4008
+ layers: ProjectLayer[];
4009
+ /** Tile layers are always small - a URL and some numbers - so they are always written in full */
4010
+ tileLayers: TileLayerMetadata[];
4011
+ themes: ThematicDefinition[];
4012
+ view?: ProjectView;
4013
+ /** Anything the host application wants to keep with the map */
4014
+ extra?: Record<string, unknown>;
4015
+ }
4016
+
3842
4017
  declare class OFSSelectionOverlay {
3843
4018
  private dataSource;
3844
4019
  private mapAdapter;
@@ -3979,6 +4154,38 @@ export declare class OFSThematicEngine extends TypedEventEmitter<ThematicEvents>
3979
4154
  compile(layer: OFSLayerData): CompiledLayerStyle;
3980
4155
  }
3981
4156
 
4157
+ export declare class OFSTileLayer {
4158
+ readonly id: string;
4159
+ readonly kind: TileLayerKind;
4160
+ name: string;
4161
+ zIndex: number;
4162
+ visible: boolean;
4163
+ opacity: number;
4164
+ pinned: boolean;
4165
+ tag?: string;
4166
+ groupId?: string;
4167
+ readonly tiles: string[];
4168
+ readonly url?: string;
4169
+ readonly tileSize: number;
4170
+ readonly scheme: 'xyz' | 'tms';
4171
+ readonly minzoom?: number;
4172
+ readonly maxzoom?: number;
4173
+ readonly bounds?: BoundingBox;
4174
+ readonly attribution?: string;
4175
+ styles: VectorTileStyle[];
4176
+ readonly metadata: TileLayerMetadata;
4177
+ constructor(metadata: TileLayerMetadata);
4178
+ /** The source definition a GL map engine expects. Undefined keys are dropped, not sent as null. */
4179
+ toSourceSpec(): Record<string, any>;
4180
+ /**
4181
+ * The GL layers to draw this tile set with, bottom first.
4182
+ *
4183
+ * Opacity is folded into the paint properties rather than applied to the layer as a whole,
4184
+ * because GL styles have no layer-wide opacity: each type names its own.
4185
+ */
4186
+ toLayerSpecs(): Record<string, any>[];
4187
+ }
4188
+
3982
4189
  export declare class OFSTopologyPanel {
3983
4190
  private readonly dataSource;
3984
4191
  private readonly topology;
@@ -4265,6 +4472,9 @@ export declare function parseLngLat(raw: string): Coordinate | null;
4265
4472
  /** Parse a number, or null when the text is not a finite number */
4266
4473
  export declare function parseNumber(text: string): number | null;
4267
4474
 
4475
+ /** Parse a project from the text of a `.ofsproj` file */
4476
+ export declare function parseProject(text: string): OFSProjectFile;
4477
+
4268
4478
  export declare function parseWkb(bytes: Uint8Array, offset?: number): {
4269
4479
  geometry: Geometry;
4270
4480
  end: number;
@@ -4346,9 +4556,27 @@ export declare function printImportResult(consoleService: IOFSConsole, result: I
4346
4556
  /** Print an import / export report: summary lines, then issues grouped by code */
4347
4557
  export declare function printIssues(consoleService: IOFSConsole, issues: ImportIssue[], limit?: number): void;
4348
4558
 
4559
+ export declare const PROJECT_FORMAT = "ofs-project";
4560
+
4561
+ export declare const PROJECT_VERSION = 1;
4562
+
4349
4563
  /** Plane to do the arc maths on, centred on the curve so distances come out in ground meters */
4350
4564
  export declare function projectionForVertices(vertices: Coordinate[]): PlanarProjection;
4351
4565
 
4566
+ export declare interface ProjectLayer {
4567
+ metadata: LayerMetadata;
4568
+ /** Left out when the project was saved without feature data */
4569
+ features?: Feature<Geometry, any>[];
4570
+ }
4571
+
4572
+ /** Where the map was looking when the project was saved */
4573
+ export declare interface ProjectView {
4574
+ center: Coordinate;
4575
+ zoom: number;
4576
+ bearing?: number;
4577
+ pitch?: number;
4578
+ }
4579
+
4352
4580
  export declare class PromptBuilder {
4353
4581
  /**
4354
4582
  * Summarize current map state for LLM prompt context.
@@ -4425,6 +4653,9 @@ export declare function renderSymbolSwatchSvg(symbol: SymbolStyle, geomType: Lay
4425
4653
  /** Report an error that was caught and recovered from */
4426
4654
  export declare function reportInternalError(scope: string, error: unknown): void;
4427
4655
 
4656
+ /** The engine to build a map with, or an error saying how to supply one */
4657
+ export declare function requireMapEngine(global: MapEngineGlobal, options?: Record<string, any>): any;
4658
+
4428
4659
  /**
4429
4660
  * Resolves all selected features across all layers in the dataset,
4430
4661
  * prioritizing active layer features first, followed by other layers,
@@ -4548,6 +4779,21 @@ export declare function runTopologyChecks(features: AnyFeature[], layer: {
4548
4779
  name?: string;
4549
4780
  }, options?: TopologyOptions): Generator<TopologyProgress, TopologyReport, void>;
4550
4781
 
4782
+ /** Write the current map out as a project */
4783
+ export declare function saveProject(dataSource: OFSDataSource, thematicEngine: OFSThematicEngine, options?: SaveProjectOptions): OFSProjectFile;
4784
+
4785
+ export declare interface SaveProjectOptions {
4786
+ /**
4787
+ * Write the features into the file (default). Turning this off keeps the layers, their styling
4788
+ * and their order but leaves the data out, for a project meant to be reconnected to a database
4789
+ * or to files that live elsewhere - and for a project of 98,000 parcels, which is 60 MB of
4790
+ * GeoJSON nobody wants in version control.
4791
+ */
4792
+ features?: boolean;
4793
+ view?: ProjectView;
4794
+ extra?: Record<string, unknown>;
4795
+ }
4796
+
4551
4797
  export declare interface ScaleBarPlan {
4552
4798
  /** Ground distance the bar represents, in meters */
4553
4799
  meters: number;
@@ -5023,6 +5269,40 @@ export declare interface ThematicEvents {
5023
5269
  */
5024
5270
  export declare type ThematicType = 'simple' | 'categorical' | 'graduated';
5025
5271
 
5272
+ export declare type TileLayerKind = 'raster' | 'vector';
5273
+
5274
+ export declare interface TileLayerMetadata {
5275
+ id: string;
5276
+ name: string;
5277
+ kind: TileLayerKind;
5278
+ /**
5279
+ * Tile URL templates with `{z}`, `{x}` and `{y}` placeholders. Several entries are read as
5280
+ * mirrors of the same tile set, which is how a server spreads load across subdomains.
5281
+ */
5282
+ tiles?: string[];
5283
+ /** A TileJSON document describing the tile set, as an alternative to `tiles` */
5284
+ url?: string;
5285
+ /** Raster tile edge in pixels; 512 for the retina tiles many providers serve */
5286
+ tileSize?: number;
5287
+ /** `tms` counts rows from the bottom, the opposite of the usual `xyz` */
5288
+ scheme?: 'xyz' | 'tms';
5289
+ minzoom?: number;
5290
+ maxzoom?: number;
5291
+ /** Area the tile set covers; outside it the map does not ask for tiles */
5292
+ bounds?: BoundingBox;
5293
+ /** Credit line the provider requires; map engines show it in the corner */
5294
+ attribution?: string;
5295
+ /** Required for `vector`, meaningless for `raster` */
5296
+ styles?: VectorTileStyle[];
5297
+ zIndex?: number;
5298
+ visible?: boolean;
5299
+ opacity?: number;
5300
+ pinned?: boolean;
5301
+ tag?: string;
5302
+ groupId?: string;
5303
+ [key: string]: unknown;
5304
+ }
5305
+
5026
5306
  /** Split on whitespace, keeping "quoted text" together */
5027
5307
  export declare function tokenizeArgs(raw: string): string[];
5028
5308
 
@@ -5299,6 +5579,14 @@ export declare function validateFieldName(name: string, existing: FieldSchema[])
5299
5579
  */
5300
5580
  export declare function validateGeometry(geometry: Geometry | null | undefined, index: number, issues: ImportIssue[], ctx?: ValidationContext): Geometry | null;
5301
5581
 
5582
+ /**
5583
+ * Check that a value really is a project this build can open, and say what is wrong when it is not.
5584
+ *
5585
+ * A file from a newer release is refused rather than half-read: reading the parts that happen to
5586
+ * look familiar and quietly dropping the rest produces a map that looks plausible and is wrong.
5587
+ */
5588
+ export declare function validateProject(value: unknown): OFSProjectFile;
5589
+
5302
5590
  export declare interface ValidationContext {
5303
5591
  file?: string;
5304
5592
  closeRings?: boolean;
@@ -5306,6 +5594,27 @@ export declare interface ValidationContext {
5306
5594
  geographic?: boolean;
5307
5595
  }
5308
5596
 
5597
+ /**
5598
+ * How one named layer inside a vector tile should be drawn.
5599
+ *
5600
+ * Vector tiles carry several layers ("roads", "buildings", "parcels") and no instructions for
5601
+ * drawing any of them, so the caller says which to draw and how. Nothing is guessed: a tile set
5602
+ * whose styling is left out renders nothing, which is a silent blank map, so it is refused instead.
5603
+ */
5604
+ export declare interface VectorTileStyle {
5605
+ /** Name of the layer inside the tile, as the tile server publishes it */
5606
+ sourceLayer: string;
5607
+ type: 'fill' | 'line' | 'circle' | 'symbol';
5608
+ /** GL paint properties, passed through as given */
5609
+ paint?: Record<string, any>;
5610
+ layout?: Record<string, any>;
5611
+ filter?: any[];
5612
+ minzoom?: number;
5613
+ maxzoom?: number;
5614
+ /** Suffix for the GL layer id; needed when one source layer is drawn more than once */
5615
+ id?: string;
5616
+ }
5617
+
5309
5618
  export declare type WorkflowStatus = 'idle' | 'running' | 'waiting_input' | 'completed' | 'failed' | 'cancelled';
5310
5619
 
5311
5620
  /** Convert a world math angle (radians, CCW from East) to a user angle in the frame's convention [0, 360) */