-
-
Notifications
You must be signed in to change notification settings - Fork 653
Expand file tree
/
Copy pathtypes.ts
More file actions
999 lines (969 loc) · 45.9 KB
/
Copy pathtypes.ts
File metadata and controls
999 lines (969 loc) · 45.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
import type {
ExternalNativePaintBridge,
ExternalNativePaintMode,
GeoLibreLayer,
LayerStyle,
} from "@geolibre/core";
import type {
QueryGeometry as ZarrQueryGeometry,
QueryOptions as ZarrQueryOptions,
QueryResult as ZarrQueryResult,
Selector as ZarrSelector,
} from "@carbonplan/zarr-layer";
import type { Feature, FeatureCollection, Geometry } from "geojson";
import type { IControl, Map as MapLibreMap } from "maplibre-gl";
import type { OvertureTheme } from "maplibre-gl-overture-maps";
import type { TemporalLayerAdapter } from "./plugins/temporal-layers";
export type GeoLibreMapControlPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
export type GeoLibreBuiltInMapControl =
| "navigation"
| "fullscreen"
| "geolocate"
| "globe"
| "terrain"
| "scale"
| "attribution"
| "logo"
| "maptoolkit-logo"
| "layer-control";
export interface GeoLibreExternalNativeLayerRegistration {
id: string;
name: string;
/** Optional host layer-group id that should contain this layer. */
groupId?: string;
type?: GeoLibreLayer["type"];
source?: Record<string, unknown>;
geojson?: FeatureCollection;
nativeLayerIds: string[];
sourceIds?: string[];
sourceId?: string;
beforeId?: string;
opacity?: number;
style?: Partial<LayerStyle>;
metadata?: Record<string, unknown>;
sourcePath?: string;
/**
* Who paints the registered native layer(s). Default `"geolibre"`: the layer
* is an ordinary MapLibre layer, so the Style panel's paint editors apply to
* it through `setPaintProperty`.
*
* Pass `"plugin"` for a MapLibre `CustomLayerInterface` (WebGL) layer, or any
* layer whose pixels the plugin draws itself. Such a layer has no MapLibre
* paint properties, so GeoLibre hides the paint editors it cannot apply
* (raster brightness/saturation/contrast/hue, the vector paint controls)
* instead of offering inert sliders, and keeps only the generic operations:
* insert-below, zoom range, visibility, reorder, remove. Supply
* {@link paintBridge} to keep opacity live as well.
*/
paintMode?: ExternalNativePaintMode;
/**
* Setters that forward GeoLibre's generic controls to a `paintMode: "plugin"`
* layer's own API (e.g. `zarrLayer.setOpacity`). Supplying `setOpacity` keeps
* the Style and Layers panel Opacity sliders live for the layer; without it
* they are hidden. Implies `paintMode: "plugin"`.
*
* The setters are called on change only, not on every layer sync. They are
* held outside the layer record (functions cannot be serialized into a
* project file), so re-register the layer after a project is reloaded to
* restore the bridge, and call `unregisterExternalNativeLayer` on
* `deactivate` to drop it.
*/
paintBridge?: ExternalNativePaintBridge;
}
/**
* Options shared by the raster/tile registration helpers
* ({@link GeoLibreAppAPI.addTileLayer}, {@link GeoLibreAppAPI.addWmtsLayer},
* {@link GeoLibreAppAPI.addWmsLayer}). They mirror the MapLibre raster `source`
* fields a tile service typically advertises, so the layer renders with the
* right extent, zoom range, and attribution without the plugin touching the
* map.
*/
export interface GeoLibreTileLayerOptions {
/** Tile size in pixels (default 256). */
tileSize?: number;
/** Attribution string shown in the map's attribution control. */
attribution?: string;
/** Visible extent as `[west, south, east, north]` in WGS84 degrees. */
bounds?: [number, number, number, number];
/** Minimum zoom at which tiles are requested. */
minzoom?: number;
/** Maximum zoom at which tiles are requested. */
maxzoom?: number;
/** Tile y-axis scheme; `"tms"` flips the y origin. Defaults to `"xyz"`. */
scheme?: "xyz" | "tms";
/** Initial visibility (default true). */
visible?: boolean;
/** Initial opacity in [0, 1] (default 1). */
opacity?: number;
/** Insert the new layer directly beneath the layer with this id. */
beforeLayerId?: string;
}
/**
* Options for {@link GeoLibreAppAPI.addWmsLayer}. The GetMap tile URL is built
* from the service `url` plus `layers`, so the plugin passes the WMS request
* parameters instead of a tile URL template.
*/
export interface GeoLibreWmsLayerOptions extends GeoLibreTileLayerOptions {
/** WMS service endpoint (the GetMap base URL). */
url: string;
/** Comma-separated WMS layer name(s) to request. */
layers: string;
/** Comma-separated style name(s) (default empty: the server default). */
styles?: string;
/** Image format, e.g. `"image/png"` (default) or `"image/jpeg"`. */
format?: string;
/** Request transparent tiles (default true). */
transparent?: boolean;
/**
* WMS protocol version: `"1.1.1"` (default) or `"1.3.0"`. Version 1.3.0
* sends `CRS` instead of `SRS`; some servers accept only one version.
*/
version?: string;
}
/** Overture Maps themes available through the host's official PMTiles source. */
export type GeoLibreOvertureTheme = OvertureTheme;
/** Parameters for a bounded Overture Maps feature query. */
export interface GeoLibreOvertureQuery {
/** Overture theme archive to query. */
theme: GeoLibreOvertureTheme;
/** MVT source layer inside the theme, e.g. `building` or `segment`. */
sourceLayer: string;
/** Query bounds as `[west, south, east, north]` in WGS84 degrees. */
bbox: [number, number, number, number];
/** Preferred MVT zoom. Defaults to 12 and decreases when the tile cap requires it. */
zoom?: number;
/** Maximum PMTiles tiles to inspect. Defaults to 512. */
maxTiles?: number;
/** Maximum matching features returned. Defaults to 50,000. */
maxFeatures?: number;
/** Optional polygon filter applied before features are returned. */
filterGeometry?: Geometry | FeatureCollection;
/**
* Spatial predicate for `filterGeometry`. Defaults to `intersects`.
* `centroid-within` uses the arithmetic mean of geometry vertices.
*/
filterMode?: "centroid-within" | "intersects";
/** Optional request cancellation signal. */
signal?: AbortSignal;
}
/** Result of a bounded Overture Maps PMTiles feature query. */
export interface GeoLibreOvertureQueryResult {
data: FeatureCollection;
release: string;
theme: GeoLibreOvertureTheme;
sourceLayer: string;
zoom: number;
/** Number of PMTiles tile lookups completed before the query stopped. */
tilesRead: number;
/** Number of matching features included in `data`. */
matchedFeatureCount: number;
/** Whether at least one additional match was excluded by `maxFeatures`. */
truncated: boolean;
}
/**
* Options for {@link GeoLibreAppAPI.addCogLayer}: a native Cloud-Optimized
* GeoTIFF layer read directly from a URL and rendered client-side, with band
* selection, rescale, colormap, and nodata handling exposed in the Style/raster
* panel. All fields are optional; the renderer infers sensible defaults from
* the GeoTIFF when they are omitted.
*/
export type GeoLibreCogRenderEngine = "maplibre-gl-raster" | "cog-tiler-wasm" | "titiler";
export interface GeoLibreCogLayerOptions {
/** Renderer used for this COG. WASM is globe-compatible; the GPU renderer requires Mercator. */
engine?: GeoLibreCogRenderEngine;
/** Band selection, e.g. `"1"` (single band) or `"1,2,3"` (RGB). */
bands?: string;
/**
* Named colormap applied to a single-band COG (e.g. `"terrain"`,
* `"viridis"`). Deliberately typed as a loose `string` so external JS
* plugins are not forced to import the renderer's internal colormap union;
* an unrecognized name falls back to the renderer default rather than
* erroring.
*/
colormap?: string;
/** Lower bound of the value range mapped to the colormap/contrast stretch. */
rescaleMin?: number;
/** Upper bound of the value range mapped to the colormap/contrast stretch. */
rescaleMax?: number;
/** Pixel value rendered as transparent (overrides the file's NoData tag). */
nodata?: number;
/** Initial opacity in [0, 1] (default 1). */
opacity?: number;
/** Insert the new layer directly beneath the layer with this id. */
beforeLayerId?: string;
}
/**
* Options for {@link GeoLibreAppAPI.addZarrLayer}: a Zarr store rendered by the
* host's own `@carbonplan/zarr-layer` instance, mirrored into the Layers panel.
*
* Symmetric to {@link GeoLibreCogLayerOptions}, with the addition of
* `crs`/`proj4`: the renderer reprojects on the GPU, so a store on a projected
* national grid lands in the right place instead of being read as WGS84.
*/
export interface GeoLibreZarrLayerOptions {
/**
* Array/variable to render (e.g. `"tmax"`). Required: neither GeoLibre nor
* the renderer guesses which array of a store to draw.
*/
variable: string;
/** Dimension selector for the non-spatial dims, e.g. `{ time: 0 }`. */
selector?: Record<string, number | string>;
/** Color limits `[min, max]` mapped to the colormap. */
clim?: [number, number];
/**
* A named GeoLibre ramp (e.g. `"viridis"`) or an explicit list of hex colors.
* Typed loosely like {@link GeoLibreCogLayerOptions.colormap}: an
* unrecognized name falls back to the default ramp rather than erroring.
*/
colormap?: string | string[];
/** Initial opacity in [0, 1] (default: the renderer's default). */
opacity?: number;
/** Zarr metadata version. Omit to let the renderer detect it. */
zarrVersion?: 2 | 3;
/** CRS of the store, e.g. `"EPSG:32633"`. */
crs?: string;
/** proj4 definition string, for a CRS the renderer has no built-in for. */
proj4?: string;
/** Explicit spatial bounds `[xMin, yMin, xMax, yMax]` in the store's CRS. */
bounds?: [number, number, number, number];
/** Override the spatial dimension names when they are not lat/lon. */
spatialDimensions?: { lat?: string; lon?: string };
/** Request headers for an authenticated store. */
headers?: Record<string, string>;
/** Insert the new layer directly beneath the layer with this id. */
beforeLayerId?: string;
}
// The query surface of `GeoLibreAppAPI.queryZarrLayer`, aliased from the
// renderer's own types so a plugin can annotate its calls without adding
// @carbonplan/zarr-layer as a dependency of its own — and so a change to the
// renderer's contract fails the host build rather than drifting silently.
/** A WGS84 `Point` (click-to-value) or `Polygon`/`MultiPolygon` (region stats). */
export type GeoLibreZarrQueryGeometry = ZarrQueryGeometry;
/**
* Dimensions to read instead of the layer's current selector. Accepts a list per
* dimension (e.g. `{ month: [1, 7] }`), which nests the returned values by that
* dimension's values.
*/
export type GeoLibreZarrQuerySelector = ZarrSelector;
/** `signal` to cancel the read; `includeSpatialCoordinates` (default true). */
export type GeoLibreZarrQueryOptions = ZarrQueryOptions;
/**
* `{ [variable]: values, dimensions, coordinates }`, where `coordinates` are in
* the store's **source** CRS, not WGS84.
*/
export type GeoLibreZarrQueryResult = ZarrQueryResult;
/**
* Describes the file-type filter shown by the host's save/open dialog so
* plugins can label exports/imports (e.g. JSON, GeoJSON, CSV) without knowing
* whether they run under Tauri or in a browser.
*/
export interface GeoLibreFileDialogOptions {
/** Human-readable file-type label, e.g. "Bookmarks" or "GeoJSON". */
description?: string;
/** Allowed extensions without the leading dot, e.g. ["json"]. */
extensions?: string[];
/** MIME type used for the browser download blob. */
mimeType?: string;
/**
* For {@link GeoLibreAppAPI.exportTextFile}: when true, ask the user for a
* file name first in browsers that cannot show a native save picker (Firefox,
* Safari), where the export would otherwise download under a fixed name. Has
* no effect under Tauri or in browsers with the File System Access picker,
* which already let the user choose the name.
*/
promptName?: boolean;
}
/**
* GeoLibre's own deck.gl modules, handed to a plugin via
* {@link GeoLibreAppAPI.getDeckGL}. Lets an external plugin render deck.gl
* layers using the host's single deck.gl instance instead of bundling its own.
*/
export interface GeoLibreDeckGL {
/** `@deck.gl/core` (Deck, the Layer base classes, view/state helpers). */
core: typeof import("@deck.gl/core");
/** `@deck.gl/layers` (ArcLayer, ScatterplotLayer, GeoJsonLayer, ...). */
layers: typeof import("@deck.gl/layers");
/** `@deck.gl/aggregation-layers` (HexagonLayer, HeatmapLayer, GridLayer, ScreenGridLayer, ContourLayer). */
aggregationLayers: typeof import("@deck.gl/aggregation-layers");
/** `@deck.gl/geo-layers` (TileLayer, H3HexagonLayer, S2Layer, ...). */
geoLayers: typeof import("@deck.gl/geo-layers");
/** `@deck.gl/mesh-layers` (SimpleMeshLayer, ScenegraphLayer). */
meshLayers: typeof import("@deck.gl/mesh-layers");
/** `@deck.gl/mapbox` - use `mapbox.MapboxOverlay` for interleaved MapLibre rendering. */
mapbox: typeof import("@deck.gl/mapbox");
}
/**
* A vector file picked through {@link GeoLibreAppAPI.pickVectorFilesWithSidecars},
* with any shapefile sidecars the host found alongside it.
*/
export interface GeoLibrePickedVectorFile {
/** The main vector file (the `.shp` for a shapefile). */
file: File;
/**
* Shapefile sidecar files (`.shx`, `.dbf`, `.prj`, `.cpg`) discovered next to
* a `.shp`; empty for any other format. Pass these to a vector control's
* `addData(file, { companionFiles })` so a loose `.shp` loads as one layer.
*/
companionFiles: File[];
/**
* Absolute filesystem path the main file was read from, so the Add Vector
* Layer panel can persist it (`addData(file, { sourcePath })`) and re-read the
* file when a saved project reopens.
*/
sourcePath?: string;
/**
* FeatureCollection materialized by a desktop host's native vector reader.
* When present, the Add Vector Layer bridge can load it as GeoJSON while
* still persisting {@link sourcePath} for project restore.
*/
nativeData?: FeatureCollection;
}
export interface GeoLibreLayerSummary {
id: string;
name: string;
type: string;
visible: boolean;
opacity: number;
}
export interface GeoLibreSelection {
layerId: string | null;
features: Feature<Geometry | null>[];
}
export interface GeoLibreAppAPI {
setBasemap: (styleUrl: string) => void;
addGeoJsonLayer: (name: string, data: FeatureCollection, sourcePath?: string) => string;
listLayers?: () => GeoLibreLayerSummary[];
getLayerFeatures?: (layerId: string) => Feature<Geometry | null>[];
getSelectedFeatures?: () => Feature<Geometry | null>[];
getSelectedLayerId?: () => string | null;
getDrawnFeatures?: () => Feature<Geometry | null>[];
onSelectionChange?: (callback: (selection: GeoLibreSelection) => void) => () => void;
/**
* Add a native XYZ raster tile layer from a tile URL template (with
* `{x}`/`{y}`/`{z}` placeholders) and return its layer id. Unlike calling
* `getMap().addSource()/addLayer()` directly, the layer appears in the Layers
* panel with full opacity/reorder/styling support and persists with the
* project, matching {@link addGeoJsonLayer} for vector data. Typed optional
* for forward-compatibility with host variants, so call it with optional
* chaining.
*/
addTileLayer?: (name: string, url: string, options?: GeoLibreTileLayerOptions) => string;
/**
* Add a native WMTS raster tile layer from a WMTS tile URL template and return
* its layer id. Behaves like {@link addTileLayer} (the layer is a first-class
* panel entry that persists with the project); the separate name keeps WMTS
* layers labelled distinctly. Typed optional for forward-compatibility, so
* call it with optional chaining.
*/
addWmtsLayer?: (name: string, url: string, options?: GeoLibreTileLayerOptions) => string;
/**
* Add a native WMS raster layer and return its layer id. The host builds the
* GetMap tile URL from {@link GeoLibreWmsLayerOptions.url} and
* {@link GeoLibreWmsLayerOptions.layers}, so the plugin supplies the request
* parameters rather than a tile URL template. The layer persists with the
* project like {@link addTileLayer}. Typed optional for
* forward-compatibility, so call it with optional chaining.
*/
addWmsLayer?: (name: string, options: GeoLibreWmsLayerOptions) => string;
/**
* Add a native Cloud-Optimized GeoTIFF (COG) layer read directly from a URL
* and rendered client-side, returning a promise for the new layer's id.
* Unlike {@link addTileLayer} (which expects pre-rendered XYZ tiles), this
* loads the GeoTIFF itself and exposes band/rescale/colormap/nodata controls,
* matching the host's own COG raster layers. The layer appears in the Layers
* panel and persists with the project. Resolves once the layer is registered;
* rejects if the COG cannot be loaded. Typed optional for
* forward-compatibility, so call it with optional chaining.
*/
addCogLayer?: (name: string, url: string, options?: GeoLibreCogLayerOptions) => Promise<string>;
/**
* Add a Zarr layer rendered by the **host's own** `@carbonplan/zarr-layer`
* instance, returning a promise for the new layer's id. The Zarr counterpart
* of {@link addCogLayer}: it reads the store directly (Zarr v2/v3, Icechunk
* over HTTP), reprojects on the GPU when `crs`/`proj4` is given, and mirrors
* the layer into the Layers panel with working visibility, opacity, ordering,
* and removal.
*
* Prefer this over bundling `@carbonplan/zarr-layer` in the plugin and adding
* a raw MapLibre custom layer: a second copy ships a duplicate numcodecs WASM
* payload, and a custom layer added that way has no MapLibre paint properties
* for the Style panel to drive.
*
* Resolves once the layer is registered; rejects if `variable` is missing or
* the store cannot be read. Typed optional for forward-compatibility, so call
* it with optional chaining.
*/
addZarrLayer?: (name: string, url: string, options: GeoLibreZarrLayerOptions) => Promise<string>;
/**
* Re-select the non-spatial dimensions of a layer added by
* {@link addZarrLayer}, e.g. to drive a plugin's own time slider with
* `{ time: n }`. The renderer keeps its fetched chunks, so this is much
* cheaper than removing and re-adding the layer. Resolves to false when there
* is no live Zarr layer with that id.
*/
setZarrLayerSelector?: (
layerId: string,
selector: Record<string, number | string>,
) => Promise<boolean>;
/**
* Read the values of a layer added by {@link addZarrLayer} under a GeoJSON
* geometry: a `Point` for click-to-value (Identify), a `Polygon` /
* `MultiPolygon` for region statistics. The read counterpart of
* {@link setZarrLayerSelector}.
*
* The host's renderer already holds the store's grid, so it does the CRS
* reprojection and fill-value masking: pass a WGS84 `[lng, lat]` straight from
* a map click rather than reading the store again with your own zarrita
* point-read. Note the returned `coordinates` are in the store's **source**
* CRS, not WGS84.
*
* Pass `selector` to read a slice other than the one on screen (e.g. another
* `time`) — the layer keeps rendering the slice it is on, so a readout does not
* disturb the map — and `options.signal` to cancel a query the user has moved
* past. Values come back empty — not an error — for a geometry outside the
* store's grid, or for a layer whose first chunks have not loaded yet; an
* aborted query rejects. Resolves to null when there is no live Zarr layer
* with that id.
*
* Typed optional for forward-compatibility, so call it with optional chaining.
*/
queryZarrLayer?: (
layerId: string,
geometry: GeoLibreZarrQueryGeometry,
selector?: GeoLibreZarrQuerySelector,
options?: GeoLibreZarrQueryOptions,
) => Promise<GeoLibreZarrQueryResult | null>;
/**
* Declare that a layer's time is an **internal dimension** the Time Slider can
* drive, by registering a {@link TemporalLayerAdapter} for it. This is the
* third kind of temporal layer, alongside a vector layer filtered by a
* timestamp property and a raster time series of dated sources: the layer is
* one store and the timeline picks a slice inside it.
*
* Use it for a plugin's **own** custom layer (a data cube it renders itself, a
* frame animation). A Zarr layer added through {@link addZarrLayer} already
* registers one for its `time` axis, so it needs no call here.
*
* Registering makes the layer *bindable* — the Layers panel's "Bind to Time
* Slider" action appears for it. Pass `{ bind: true }` to bind it right away
* and open the dock, which is usually what a plugin that just loaded a cube
* wants. Call the returned function (or remove the layer) to unregister.
*
* Typed optional for forward-compatibility, so call it with optional chaining.
*/
registerTemporalLayer?: (
layerId: string,
adapter: TemporalLayerAdapter,
options?: { bind?: boolean },
) => () => void;
/**
* Drop a layer's temporal adapter registered with
* {@link registerTemporalLayer}. Removing the layer does this too.
*/
unregisterTemporalLayer?: (layerId: string) => void;
getActiveBasemap: () => string;
onBasemapChange: (callback: (styleUrl: string) => void) => () => void;
fetchArrayBuffer?: (url: string) => Promise<ArrayBuffer>;
/**
* Resolve a fetchable URL for an asset shipped alongside an external
* plugin's manifest (e.g. sample data bundled in the plugin folder). The
* host resolves `relativePath` against the plugin's own directory. Returns
* null for built-in plugins, for plugins installed from the desktop
* filesystem (which have no URL base), or when `relativePath` escapes the
* plugin directory. `pluginId` should be the calling plugin's own id; since
* all plugins share one JS context the host cannot verify the caller, so this
* is a convention rather than an enforced boundary. It is not a privilege
* boundary: the resolved URL grants no access a plugin does not already have,
* since any plugin can fetch any same-origin URL directly.
*/
resolvePluginAssetUrl?: (pluginId: string, relativePath: string) => string | null;
/**
* Activate an installed plugin and optionally apply a partial project-state
* patch after activation. Returns false when the plugin is unavailable,
* refuses activation, or rejects the state.
*/
activatePlugin?: (pluginId: string, state?: unknown) => Promise<boolean>;
/**
* Deactivate an installed plugin, the counterpart of {@link activatePlugin}.
* Lets a plugin that opened another plugin's panel close it again when the
* reason for opening it is gone — for example a plugin that bound its layer to
* the Time Slider and has since unbound the last one.
*
* A plugin may not deactivate **itself**: tearing a plugin down from inside
* its own callback would unmount the code that is still running. Such a call
* returns false, mirroring how `activatePlugin` refuses to reactivate the
* caller.
*
* Returns true when the plugin ended up inactive, false when it is unknown,
* was not active, is the caller, or threw while unmounting.
*/
deactivatePlugin?: (pluginId: string) => boolean;
/**
* Query a bounded set of features from GeoLibre's official Overture Maps
* PMTiles integration. The host enforces tile and feature limits.
*/
queryOvertureFeatures?: (query: GeoLibreOvertureQuery) => Promise<GeoLibreOvertureQueryResult>;
/**
* Create a named Layers-panel group, optionally assigning existing layer ids
* to it, and return the new group id.
*/
addLayerGroup?: (name?: string, layerIds?: string[]) => string;
/** Remove a Layers-panel group without removing its child layers. */
removeLayerGroup?: (id: string) => void;
fitBounds?: (bounds: [number, number, number, number]) => void;
getMap?: () => MapLibreMap | null;
/**
* Open an http(s) URL in the system browser. Needed because the Tauri
* desktop webview ignores `window.open`/`target="_blank"` and would open the
* link inside the app instead; the host routes through the opener plugin
* there and falls back to `window.open` on the web build. Non-http(s) URLs
* are ignored. Plugins should call this rather than `window.open` directly.
*/
openExternalUrl?: (url: string) => void;
pickLocalDirectoryFiles?: () => Promise<File[] | null>;
/**
* Prompt the user (desktop only) to pick one or more vector files via the
* native dialog, returning each with any shapefile sidecars discovered in the
* same directory and the absolute `sourcePath` it was read from. The sidecars
* let a host with filesystem access load a loose `.shp` without the user
* selecting every component (`.shx`, `.dbf`, ...); the path lets the Add
* Vector Layer panel persist it so the layer can be re-read on reopen. Present
* only on hosts with filesystem access (the desktop build); absent on the web
* (browsers cannot read sibling files or expose paths), so its presence
* doubles as a desktop capability check. Resolves to an empty array when the
* dialog is cancelled.
*/
pickVectorFilesWithSidecars?: () => Promise<GeoLibrePickedVectorFile[]>;
/**
* Desktop-native downloader for Add Vector Layer URL sources. The web app
* leaves this unset so the control uses ordinary browser networking.
*
* Must resolve to a `File` rather than a bare `Blob`, and to the *same* object
* for concurrent callers asking for one URL. The vector control keys its
* per-source caches (a KMZ's unzipped KML, a GeoPackage's bytes) on the source
* object and wraps a plain `Blob` in a fresh `File` per call, so returning a
* `Blob` would make every sibling layer of a multi-layer container re-unzip
* and re-register the same archive. The type says `File` so that contract is
* enforced rather than only documented.
*/
fetchVectorUrl?: (url: string) => Promise<File | null>;
/**
* Read a local vector file back into a File (with any shapefile sidecars) from
* the absolute path persisted on a layer's `sourcePath`, so the Add Vector
* Layer restore can reload a desktop local-file layer when a project reopens.
* Resolves to null off the desktop host, or when the file can no longer be
* read (moved or deleted).
*/
readLocalVectorFile?: (path: string) => Promise<{
file: File;
companionFiles: File[];
nativeData?: FeatureCollection;
} | null>;
/**
* Save text content to a file chosen by the user. The host handles the
* platform specifics (a native save dialog under Tauri, a browser download
* on the web), so plugins can export data without depending on the runtime.
* Pass `options` to control the file-type label/extensions (defaults to
* GeoJSON).
*/
exportTextFile?: (filename: string, content: string, options?: GeoLibreFileDialogOptions) => void;
/**
* Prompt the user to pick a text file and return its contents (a native open
* dialog under Tauri, a file input on the web). Resolves to null when the
* user cancels. Plugins can import data without depending on the runtime.
*
* Web-only caveat: browsers do not fire a cancel event for `<input
* type="file">`, so on the web the returned promise stays pending if the
* user dismisses the dialog without choosing a file. Under Tauri (the
* primary desktop target) cancel resolves to null as expected.
*/
importTextFile?: (options?: GeoLibreFileDialogOptions) => Promise<string | null>;
registerExternalNativeLayer?: (layer: GeoLibreExternalNativeLayerRegistration) => void;
unregisterExternalNativeLayer?: (id: string) => void;
addMapControl: (control: IControl, position?: GeoLibreMapControlPosition) => boolean;
removeMapControl: (control: IControl) => void;
setBuiltInMapControlVisible: (control: GeoLibreBuiltInMapControl, visible: boolean) => boolean;
getBuiltInMapControlPosition: (control: GeoLibreBuiltInMapControl) => GeoLibreMapControlPosition;
setBuiltInMapControlPosition: (
control: GeoLibreBuiltInMapControl,
position: GeoLibreMapControlPosition,
) => boolean;
/** Enable or disable GeoLibre's built-in DEM terrain without changing control visibility. */
setTerrainEnabled?: (enabled: boolean) => boolean;
/** Whether GeoLibre's built-in DEM terrain is currently active. */
isTerrainEnabled?: () => boolean;
/**
* Resolve GeoLibre's own deck.gl modules so an external plugin can render
* deck.gl layers (e.g. an `ArcLayer`) on the host's single deck.gl instance.
* Bundling a second copy is not viable: deck.gl and luma.gl throw on a
* version mismatch and share global singletons, so a plugin's own copy fails
* to render. Always present on the GeoLibre desktop and web hosts; typed
* optional for forward-compatibility with host variants that may not ship
* deck.gl, so plugins should still call it with optional chaining.
*/
getDeckGL?: () => Promise<GeoLibreDeckGL>;
/**
* Resolve GeoLibre's own `maplibre-gl-raster` module so an external plugin can
* render Cloud-Optimized GeoTIFFs through the host's instance instead of
* bundling its own. maplibre-gl-raster pulls in deck.gl and luma.gl, which
* throw on a second copy (luma.gl: "already initialized"); a bundled plugin
* copy therefore fails to activate. GeoLibre already ships maplibre-gl-raster
* (the built-in raster layer uses it), so it hands plugins the same instance.
* Always present on the GeoLibre desktop and web hosts; typed optional for
* forward-compatibility, so plugins should call it with optional chaining.
*/
getMaplibreGlRaster?: () => Promise<typeof import("maplibre-gl-raster")>;
/**
* Set the map projection preference (persisted in app state, so the host's
* projection enforcement keeps it). deck.gl-backed plugins call this with
* `"mercator"` because deck.gl's tiled rendering does not support globe view;
* a raw `map.setProjection` is reverted on the next idle by the host.
*
* There is no automatic rollback: a plugin that forces a projection on
* activate should save the user's choice with {@link getMapProjection} first
* and restore it on `deactivate`, otherwise the user is left in the forced
* projection after the plugin is turned off. Fall back when the getter is
* absent on an older host so `deactivate` never passes `undefined` (the
* runtime guard ignores it, stranding the user in the forced projection):
*
* ```ts
* const saved = app.getMapProjection?.() ?? "globe";
* app.setMapProjection?.("mercator");
* // on deactivate:
* app.setMapProjection?.(saved);
* ```
*/
setMapProjection?: (projection: "globe" | "mercator") => void;
/** Current map projection preference. */
getMapProjection?: () => "globe" | "mercator";
/**
* Register a plugin-owned right-sidebar panel that docks beside the built-in
* Style panel and behaves like a first-class part of the workspace. Returns
* an unregister function (call it from `deactivate`). The panel is not shown
* until `openRightPanel(panel.id)` runs. While a plugin panel is the active
* right-side workspace the host collapses the Style panel to its rail and
* restores it when the plugin panel closes. Typed optional for
* forward-compatibility with host variants without a right sidebar, so
* plugins should call it with optional chaining.
*/
registerRightPanel?: (panel: GeoLibreRightPanelRegistration) => () => void;
/** Remove a previously registered right panel (closing it if active). */
unregisterRightPanel?: (id: string) => void;
/**
* Make the panel the active right-side workspace and expand it. Returns false
* if no panel with that id is registered. Re-opening a collapsed panel
* expands it.
*/
openRightPanel?: (id: string) => boolean;
/** Collapse the active right panel to its rail without closing it. */
collapseRightPanel?: (id: string) => void;
/** Close the active right panel and restore the Style panel. */
closeRightPanel?: (id: string) => void;
/** Id of the active right-side workspace panel, or null when none is open. */
getActiveRightPanel?: () => string | null;
/**
* Dock the active panel at any dock, mirroring the user-facing controls so a
* plugin can reposition its own panel. The four positional docks behave like
* the move buttons; `replace-style` switches the panel into the shared Style
* rail (the inverse of detaching it back to a positional dock). No-op when no
* panel is active. See {@link GeoLibreRightPanelDock}.
*/
setActiveRightPanelDock?: (dock: GeoLibreRightPanelDock) => void;
/** Where the active panel docks, or null when none is open. */
getActiveRightPanelDock?: () => GeoLibreRightPanelDock | null;
/**
* Register a plugin-owned top-level toolbar menu shown in the GeoLibre banner
* beside the built-in menus, with nested submenus and action items. Returns
* an unregister function (call it from `deactivate`). Re-registering the same
* id replaces the menu. Typed optional for forward-compatibility with hosts
* that have no top toolbar, so call it with optional chaining.
*
* The host tracks which plugin owns each menu so the toolbar can place it by
* owner (e.g. external plugin menus after Help); that is injected internally
* by the PluginManager, so plugins never pass an owner here.
*/
registerToolbarMenu?: (menu: GeoLibreToolbarMenu) => () => void;
/** Remove a previously registered toolbar menu. */
unregisterToolbarMenu?: (id: string) => void;
/**
* Register a plugin-owned floating panel: a draggable, closeable card the
* host overlays on the map's top-left corner. Returns an unregister function
* (call it from `deactivate`). The panel is not shown until
* {@link openFloatingPanel} is called. Unlike a right panel, several floating
* panels can be open at once and they do not shrink the map.
*/
registerFloatingPanel?: (panel: GeoLibreFloatingPanelRegistration) => () => void;
/** Remove a registered floating panel (closing it if open). */
unregisterFloatingPanel?: (id: string) => void;
/** Open a floating panel (or bring an already-open one to the front). */
openFloatingPanel?: (id: string) => boolean;
/** Close an open floating panel. */
closeFloatingPanel?: (id: string) => void;
/** Ids of the currently open floating panels, in stacking order. */
getOpenFloatingPanels?: () => string[];
}
/**
* An action item in a plugin {@link GeoLibreToolbarMenu}. Selecting it runs
* {@link onSelect} (for example, to open a right panel or floating panel).
*/
export interface GeoLibreToolbarMenuAction {
/** Discriminator; defaults to "action" when omitted. */
type?: "action";
/** Stable id, unique within the menu. */
id: string;
/** Label shown in the menu. */
label: string;
/** Optional icon: a URL or `data:` URI rendered as an image. */
icon?: string;
/** When true, the item is shown disabled and cannot be selected. */
disabled?: boolean;
/** Invoked when the user selects the item. */
onSelect: () => void;
}
/** A nested submenu in a plugin {@link GeoLibreToolbarMenu}. */
export interface GeoLibreToolbarSubmenu {
type: "submenu";
/** Stable id, unique within the parent menu. */
id: string;
/** Label shown on the submenu trigger. */
label: string;
/** Optional icon: a URL or `data:` URI rendered as an image. */
icon?: string;
/** Child items (actions, separators, or further submenus). */
items: GeoLibreToolbarMenuItem[];
}
/** A divider between groups of items in a plugin toolbar menu. */
export interface GeoLibreToolbarSeparator {
type: "separator";
/** Optional id (only needed as a stable React key when you have many). */
id?: string;
}
/** One entry in a plugin toolbar menu: an action, a submenu, or a separator. */
export type GeoLibreToolbarMenuItem =
| GeoLibreToolbarMenuAction
| GeoLibreToolbarSubmenu
| GeoLibreToolbarSeparator;
/**
* A plugin-owned top-level toolbar menu. The host renders it as a dropdown
* button in the banner beside the built-in menus.
*/
export interface GeoLibreToolbarMenu {
/** Stable unique id used to unregister the menu. */
id: string;
/** Button label shown in the toolbar. */
label: string;
/** Optional icon: a URL or `data:` URI rendered as an image. */
icon?: string;
/** Top-level items (actions, separators, or submenus). */
items: GeoLibreToolbarMenuItem[];
}
/**
* A plugin-owned floating panel: a draggable, closeable card the host overlays
* on the map's top-left corner. The plugin owns only the body via {@link render}
* (plain DOM); the host provides the card chrome (a draggable title bar with a
* close button). Several floating panels can be open at once, and they do not
* shrink the map.
*/
export interface GeoLibreFloatingPanelRegistration {
/** Stable unique id used to open/close the panel. */
id: string;
/**
* Title shown in the card's title bar. Pass a getter function to make the
* title reactive: it is re-evaluated on every `getFloatingPanel` call, so it
* picks up a new language without re-registering the panel. Caveat: the
* registry itself does not subscribe to i18n events, so the getter is only
* re-run when a consumer re-reads the panel. Today every host component that
* displays a title also calls `useTranslation()`, whose `languageChanged`
* re-render re-reads the panel as a side effect; a host that reads a panel
* without that subscription would show a stale title after a language switch
* until the next registry mutation, and should re-read the panel itself on
* language change. A plain string is frozen at registration time.
*/
title: string | (() => string);
/** Optional icon: a URL or `data:` URI rendered in the title bar. */
icon?: string;
/** Preferred card width in px (the host clamps it to a sensible range). */
defaultWidth?: number;
/**
* Preferred card height in px. When set, the card opens at this height and its
* body fills it (so a `height:100%` plugin element grows with the card);
* omitted, the card sizes to its content. The host clamps it and lets the user
* resize from the corner handle.
*/
defaultHeight?: number;
/**
* Which corner of the map the card first opens at (default `top-left`). The
* card stays freely draggable/resizable afterwards; a plugin can move it
* between corners by re-registering with a new position (used to back the
* Plugins-menu position submenu).
*/
position?: GeoLibreMapControlPosition;
/**
* Populate the card body. Called once with an empty container the plugin
* fills with its own DOM. The container stays mounted while the card is open,
* so plugin state persists. May return a cleanup function the host runs when
* the panel closes or is unregistered.
*/
render: (container: HTMLElement) => void | (() => void);
/** Called after the panel opens. */
onOpen?: () => void;
/** Called after the panel closes. */
onClose?: () => void;
}
/**
* Where a plugin panel docks. Four are positional, left to right: `left-of-layers`
* (the far-left edge), `right-of-layers` (between the Layers panel and the map),
* `left-of-style` (between the map and the Style panel), or `right-of-style` (the
* far-right edge). The built-in panel on the docked side (Layers on the left,
* Style on the right) collapses to its rail while the plugin panel is expanded
* next to it.
*
* `replace-style` and `replace-layers` are non-positional **shared-rail** modes:
* the panel shares the Style (right) or Layers (left) panel's sidebar surface
* instead of sitting beside it as a separate rail. The host shows a single rail
* on that edge listing both the plugin panel and the built-in panel; selecting
* one expands it while the other stays as a rail entry, so a workbench-style
* plugin feels like a first-class sidebar workspace rather than a second rail.
* Unlike the positional docks, these modes are not part of the move-button step
* sequence; the host's merge/detach buttons switch a panel in and out of them.
*/
export type GeoLibreRightPanelDock =
| "left-of-layers"
| "right-of-layers"
| "left-of-style"
| "right-of-style"
| "replace-style"
| "replace-layers";
/**
* A plugin-owned dockable side panel. The host renders the registered panel in
* its own dock (one of three positions beside the Layers/Style panels), with a
* collapsible rail, a header (title plus move/collapse/close buttons), and a
* resize handle. The plugin owns only the content: `render` is called once with
* an empty container element the plugin fills with its own DOM (an external
* plugin cannot share GeoLibre's React, so the contract is plain DOM rather
* than a React node).
*/
export interface GeoLibreRightPanelRegistration {
/** Stable unique id used to open/collapse/close the panel. */
id: string;
/**
* Human-readable title shown in the panel header and collapsed rail.
* Pass a getter function to make the title reactive: it is re-evaluated on
* every `getRightPanel` call, so it picks up a new language without
* re-registering the panel. Caveat: the registry itself does not subscribe
* to i18n events, so the getter is only re-run when a consumer re-reads the
* panel. Today every host component that displays a title also calls
* `useTranslation()`, whose `languageChanged` re-render re-reads the panel
* as a side effect; a host that reads a panel without that subscription
* would show a stale title after a language switch until the next registry
* mutation, and should re-read the panel itself on language change. A plain
* string is frozen at registration time.
*/
title: string | (() => string);
/**
* Where the panel docks initially: one of the four positional docks
* (`left-of-layers`, `right-of-layers`, `left-of-style`, or `right-of-style`),
* or a shared-rail mode (`replace-style`, the default, or `replace-layers`).
* With a positional dock the built-in panel on the docked side (Layers on the
* left, Style on the right) collapses to its rail while the plugin panel is
* expanded next to it, and the user can move the panel between positions at
* runtime with the move buttons in its header (or a plugin via
* {@link GeoLibreAppAPI.setActiveRightPanelDock}). With a shared-rail mode the
* panel shares the Style or Layers sidebar's single rail instead and is not
* steppable (the header's merge/detach buttons switch it in and out).
*/
dock?: GeoLibreRightPanelDock;
/**
* Optional icon for the collapsed rail. A URL or `data:` URI is rendered as
* an image; any other value is ignored in favor of a default glyph.
*/
icon?: string;
/**
* Preferred width of the expanded panel in pixels (desktop only; the host
* clamps it to a sensible range). Defaults to the host's standard panel
* width.
*/
defaultWidth?: number;
/**
* Populate the panel body. Called once with an empty container when the panel
* first becomes active; the plugin appends its own DOM. The container is kept
* mounted across collapse so plugin state persists. May return a cleanup
* function invoked when the panel is closed or unregistered.
*/
render: (container: HTMLElement) => void | (() => void);
/** Called after the panel opens (becomes the active workspace). */
onOpen?: () => void;
/** Called after the panel collapses to its rail. */
onCollapse?: () => void;
/** Called after the panel closes (releases the workspace). */
onClose?: () => void;
}
export interface GeoLibrePlugin {
id: string;
name: string;
version: string;
activeByDefault?: boolean;
/** At least one name is required for handleUrlParameters to be called. */
urlParameterNames?: string[];
/**
* Activate the plugin. Return `false` to refuse activation. A plugin that
* mounts asynchronously (e.g. behind a dynamic import) may return a Promise
* that resolves to `false` (or rejects) when the mount ultimately fails; the
* host then rolls back the optimistic active state so the Plugins menu does
* not show a plugin that never came up.
*
* If a plugin auto-opens its control panel on activation, expand it with a
* `setTimeout(() => control.expand(), 0)` (the convention every built-in
* control follows). On a project restore the host re-collapses panels one
* tick after that expand so a loaded project does not bury the map (#952);
* deferring the expand by more than one tick would defeat that and leave the
* panel open after restore.
*/
activate: (app: GeoLibreAppAPI) => boolean | void | Promise<boolean | void>;
deactivate: (app: GeoLibreAppAPI) => void;
/**
* Called once per URL context after the map and plugins are ready.
* Requires urlParameterNames to be non-empty; otherwise this hook is never
* invoked. A handler that throws is not counted as handled, so a later
* dispatch for the same context retries it.
*/
handleUrlParameters?: (app: GeoLibreAppAPI, params: URLSearchParams) => void | Promise<void>;
getMapControlPosition?: () => GeoLibreMapControlPosition;
setMapControlPosition?: (
app: GeoLibreAppAPI,
position: GeoLibreMapControlPosition,
) => boolean | void;
getProjectState?: () => unknown;
applyProjectState?: (app: GeoLibreAppAPI, state: unknown) => boolean | void;
/**
* Set when the plugin persists its own panel open/collapsed state through
* getProjectState/applyProjectState.
*
* The project-restore pass collapses every control it adds so a loaded
* project does not bury the map under expanded panels (#952). That heuristic
* is right for panels which auto-expand on activation and carry no saved
* state, but wrong for a plugin whose collapsed state is part of the project:
* the blanket collapse overrides the user's saved `collapsed: false` and,
* because collapsing mutates the control's live state, a later re-save writes
* the collapsed state back into the project file. Declaring this opts out of
* the sweep — the restored config is then the only thing deciding whether the
* panel opens.
*/
restoresPanelCollapseState?: boolean;
}
export interface GeoLibreExternalPluginManifest {
id: string;
name: string;
version: string;
entry: string;
description?: string;
style?: string;
/**
* Activate the plugin on startup when no saved plugin state overrides it.
* Honored only for bundled drop-ins (public/plugins/<id>/), which are baked
* into the build by the deployer and therefore as trusted as built-ins.
* Ignored for plugins installed at runtime from zips or manifest URLs, so
* third-party plugins cannot force themselves active.
*/
activeByDefault?: boolean;
}