Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/geolibre-desktop/src/hooks/usePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
setEffectsSettings,
type EffectsSettings,
maplibreEarthdataGisPlugin,
SKETCHES_SOURCE_KIND,
setEarthdataCogSaver,
maplibreEnviroAtlasPlugin,
maplibreEsriWaybackPlugin,
Expand Down Expand Up @@ -93,6 +94,7 @@ import type {
GeoLibreExternalNativeLayerRegistration,
GeoLibreFileDialogOptions,
GeoLibreMapControlPosition,
GeoLibreSelection,
GeoLibreTileLayerOptions,
GeoLibreWmsLayerOptions,
GeoLibreZarrLayerOptions,
Expand Down Expand Up @@ -829,6 +831,19 @@ export function useTimeSliderAutoClose(mapControllerRef: RefObject<MapController
}, [mapControllerRef]);
}

function readPluginSelection(): GeoLibreSelection {
const state = useAppStore.getState();
const layer = state.layers.find((item) => item.id === state.selectedLayerId);
if (!layer || state.selectedFeatureIds.length === 0) {
return { layerId: state.selectedLayerId, features: [] };
}
const selected = new Set(state.selectedFeatureIds);
const features = (layer.geojson?.features ?? []).filter((feature, index) =>
selected.has(String(feature.id ?? index)),
);
return { layerId: state.selectedLayerId, features };
}

export function createAppAPI(mapControllerRef?: RefObject<MapController | null>) {
const store = useAppStore.getState();
// Captured so methods that delegate to plugin helpers taking the AppAPI
Expand All @@ -840,6 +855,36 @@ export function createAppAPI(mapControllerRef?: RefObject<MapController | null>)
const id = store.addGeoJsonLayer(name, data, sourcePath);
return id;
},
listLayers: () =>
useAppStore.getState().layers.map(({ id, name, type, visible, opacity }) => ({
id,
name,
type,
visible,
opacity,
})),
getLayerFeatures: (layerId: string) => {
const layer = useAppStore.getState().layers.find((item) => item.id === layerId);
if (!layer) throw new Error(`No layer with id "${layerId}"`);
return layer.geojson?.features ?? [];
},
getSelectedFeatures: () => readPluginSelection().features,
getSelectedLayerId: () => useAppStore.getState().selectedLayerId,
getDrawnFeatures: () =>
useAppStore
.getState()
.layers.flatMap((layer) =>
layer.metadata.sourceKind === SKETCHES_SOURCE_KIND ? (layer.geojson?.features ?? []) : [],
),
onSelectionChange: (callback: (selection: GeoLibreSelection) => void) =>
useAppStore.subscribe((state, previous) => {
if (
state.selectedLayerId !== previous.selectedLayerId ||
state.selectedFeatureIds !== previous.selectedFeatureIds
) {
callback(readPluginSelection());
}
}),
addTileLayer: (name: string, url: string, options?: GeoLibreTileLayerOptions) =>
store.addTileLayer(
name,
Expand Down
65 changes: 64 additions & 1 deletion docs/plugin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Interface

```typescript
import type { FeatureCollection } from "geojson";
import type { Feature, FeatureCollection } from "geojson";
import type { IControl } from "maplibre-gl";

export type GeoLibreMapControlPosition =
Expand Down Expand Up @@ -55,13 +55,34 @@ export interface GeoLibreDeckGL {
mapbox: typeof import("@deck.gl/mapbox");
}

export interface GeoLibreLayerSummary {
id: string;
name: string;
type: string;
visible: boolean;
opacity: number;
}

export interface GeoLibreSelection {
layerId: string | null;
features: Feature[];
}

export interface GeoLibreAppAPI {
setBasemap: (styleUrl: string) => void;
addGeoJsonLayer: (
name: string,
data: FeatureCollection,
sourcePath?: string,
) => string;
listLayers?: () => GeoLibreLayerSummary[];
getLayerFeatures?: (layerId: string) => Feature[];
getSelectedFeatures?: () => Feature[];
getSelectedLayerId?: () => string | null;
getDrawnFeatures?: () => Feature[];
onSelectionChange?: (
callback: (selection: GeoLibreSelection) => void,
) => () => void;
// Native raster/tile layers (see "Raster and tile layers" below). Each
// returns the new layer's id and the layer appears in the Layers panel and
// persists with the project, like addGeoJsonLayer does for vector data.
Expand Down Expand Up @@ -416,6 +437,48 @@ https://web.geolibre.app/?url=https://example.com/project.geolibre.json&exampleG

A URL parameter activates only an already-registered (installed) plugin that owns it; it never loads a plugin from the URL. For external plugins, include the plugin manifest URL in the project `plugins` state (so the plugin is registered) before relying on its URL handler — the matching parameter then activates and dispatches it even if it is not in the active set.

## Read-only layer and feature queries

External plugins can inspect the current layer list, a layer's GeoJSON features,
the current feature selection, and features in GeoEditor's Sketches layers. The
methods are optional so the same plugin remains compatible with older hosts.

```typescript
const layers = app.listLayers?.() ?? [];
const selectedLayerId = app.getSelectedLayerId?.() ?? null;
const selectedFeatures = app.getSelectedFeatures?.() ?? [];

if (selectedLayerId && app.getLayerFeatures) {
const layerFeatures = app.getLayerFeatures(selectedLayerId);
console.log(layers, layerFeatures, selectedFeatures);
}

const drawnFeatures = app.getDrawnFeatures?.() ?? [];
```

`getSelectedFeatures` returns every selected feature in the selected layer, not
only the most recently selected feature. Features without a GeoJSON `id` are
matched using their zero-based array index converted to a string. An empty
selection returns an empty array. `getLayerFeatures` throws when the layer id is
unknown and returns an empty array for a layer that has no GeoJSON features.

Selection subscriptions fire after the selected layer or selected feature-id
array changes. Keep and call the returned unsubscribe function during plugin
deactivation:

```typescript
const unsubscribe = app.onSelectionChange?.(({ layerId, features }) => {
console.log(layerId, features);
});

// In deactivate or another cleanup path:
unsubscribe?.();
```

These methods are a read-only query surface: calling them does not change the
GeoLibre store. Plugins must also treat returned GeoJSON features as read-only
and use host APIs such as `addGeoJsonLayer` when they need to add data.

## Raster and tile layers

`addGeoJsonLayer` registers vector data as a native layer. For raster and tile data there are three matching helpers — `addTileLayer` (XYZ), `addWmtsLayer` (WMTS), and `addWmsLayer` (WMS). Each returns the new layer's id, and the layer appears in the Layers panel with full opacity, reorder, and styling support and persists with the project, so a plugin no longer has to call `getMap().addSource()/addLayer()` directly (which leaves the layer invisible to GeoLibre's layer store).
Expand Down
21 changes: 20 additions & 1 deletion packages/plugins/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
QueryResult as ZarrQueryResult,
Selector as ZarrSelector,
} from "@carbonplan/zarr-layer";
import type { FeatureCollection, Geometry } from "geojson";
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";
Expand Down Expand Up @@ -331,9 +331,28 @@ export interface GeoLibrePickedVectorFile {
nativeData?: FeatureCollection;
}

export interface GeoLibreLayerSummary {
id: string;
name: string;
type: string;
visible: boolean;
opacity: number;
}

export interface GeoLibreSelection {
layerId: string | null;
features: Feature[];
}

export interface GeoLibreAppAPI {
setBasemap: (styleUrl: string) => void;
addGeoJsonLayer: (name: string, data: FeatureCollection, sourcePath?: string) => string;
listLayers?: () => GeoLibreLayerSummary[];
getLayerFeatures?: (layerId: string) => Feature[];
getSelectedFeatures?: () => Feature[];
getSelectedLayerId?: () => string | null;
getDrawnFeatures?: () => Feature[];
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
Expand Down
Loading