Skip to content

Commit 8792133

Browse files
authored
feat(add-data): add OGC API - Features collections as vector layers (#1449)
* feat(add-data): add OGC API - Features collections as vector layers GeoLibre could reach OGC feature services only through WFS, so a modern OGC API - Features endpoint (the WFS successor: no capabilities document, JSON-native, paged) had no way in. Add an "OGC API - Features" source to the Add Data menu: - `lib/ogc-api-features.ts` owns the client. `parseOgcFeaturesUrl` accepts whatever URL the user has in hand -- a landing page, `/collections`, a collection, or the full `/collections/{id}/items` URL a service's own HTML browser puts in the address bar -- and splits it into a base URL plus collection id, keeping non-OGC query params (an API key) and dropping the request params the builder sets itself, so a pasted `f=html&limit=10` cannot fight the request built on top of it. - The items fetch follows the service's `next` links until the requested feature count is reached. This is the part a single request gets wrong: pygeoapi's demo caps `limit` at 10, so a naive fetch yields 10 of 25 features. Paging stops on an empty page (many services advertise `next` even on the last one), refuses a `next` that leaves the service's origin, and shares one deadline across every page. - `OgcFeaturesSource` retrieves the collection list, preselects the one a pasted URL named, and offers optional bbox and datetime filters. - The layer persists its request, so `Refresh` replays the same paged walk instead of silently shrinking to the stored first page. The JSON fetch that works around missing CORS headers (Tauri native HTTP / dev proxy / direct) moves to `lib/ogc-json.ts` and is now shared with the OGC API - Tiles client, which had the only copy; it also surfaces a service's JSON problem-document description alongside the status on a failed request. Fixes #1446 * Address CodeRabbit review feedback - Propagate the refreshed paging metadata for OGC API - Features layers. `refreshGeoJsonLayer` now returns an optional `metadata` patch, and the OGC branch fills it with the `numberMatched`/`truncated` from the fetch it just ran; LayerPanel merges it alongside `featureCount`, so a refreshed layer no longer reports the counts it was originally added with. The legacy single-page fallback deliberately sends no patch: it has no `numberMatched` to compare against, so leaving the stored values alone beats overwriting them with a guess. - Spell the default layer name "OGC API - Features Layer", matching the hyphenated official name every other new string already uses.
1 parent 2b70fec commit 8792133

14 files changed

Lines changed: 1470 additions & 74 deletions

File tree

apps/geolibre-desktop/src/components/layout/AddDataDialog.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { GdbSource } from "./add-data/sources/GdbSource";
1414
import { GeoRssSource } from "./add-data/sources/GeoRssSource";
1515
import { GpxSource } from "./add-data/sources/GpxSource";
1616
import { MbtilesSource } from "./add-data/sources/MbtilesSource";
17+
import { OgcFeaturesSource } from "./add-data/sources/OgcFeaturesSource";
1718
import { OgcVectorTilesSource } from "./add-data/sources/OgcVectorTilesSource";
1819
import { PhotosSource } from "./add-data/sources/PhotosSource";
1920
import { PostgresSource } from "./add-data/sources/PostgresSource";
@@ -64,6 +65,8 @@ function renderSource(
6465
return <WfsSource />;
6566
case "wmts":
6667
return <WmtsSource />;
68+
case "ogc-features":
69+
return <OgcFeaturesSource />;
6770
case "ogc-vector-tiles":
6871
return <OgcVectorTilesSource />;
6972
case "gpx":

apps/geolibre-desktop/src/components/layout/add-data/apply-service.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type { MapController } from "@geolibre/map";
2626
import type { ArcGISLayerType, ArcGISSourceType } from "@geolibre/plugins";
2727
import type { FeatureCollection } from "geojson";
2828
import type { RefObject } from "react";
29+
import { OGC_FEATURES_SOURCE_KIND } from "../../../lib/ogc-api-features";
2930
import type { ResolvedXyzTileUrl } from "../../../lib/xyz-url";
3031
import {
3132
attributionForTileUrl,
@@ -295,6 +296,63 @@ export function buildWfsGeoJsonLayer(params: WfsLayerParams): GeoLibreLayer {
295296
};
296297
}
297298

299+
// --- OGC API - Features ----------------------------------------------------
300+
301+
export interface OgcFeaturesLayerParams {
302+
name: string;
303+
/** The first page's `/items` request URL, replayed on refresh. */
304+
itemsUrl: string;
305+
data: FeatureCollection;
306+
baseUrl: string;
307+
collectionId: string;
308+
maxFeatures: number;
309+
bbox?: string;
310+
datetime?: string;
311+
extraQuery?: string;
312+
/** The server's `numberMatched`, when it advertised one. */
313+
numberMatched?: number;
314+
/** True when the collection holds more features than were loaded. */
315+
truncated: boolean;
316+
}
317+
318+
/**
319+
* Builds a GeoJSON layer from fetched OGC API - Features items. The request
320+
* parameters are persisted alongside the data so a refresh can replay the same
321+
* paged walk rather than re-reading only the first page.
322+
*
323+
* @param params - The layer name, items URL, fetched data, and request metadata.
324+
* @returns The constructed GeoJSON layer.
325+
*/
326+
export function buildOgcFeaturesLayer(params: OgcFeaturesLayerParams): GeoLibreLayer {
327+
return {
328+
...createBaseLayer(
329+
params.name,
330+
"geojson",
331+
{
332+
type: "geojson",
333+
url: params.itemsUrl,
334+
service: "ogc-features",
335+
baseUrl: params.baseUrl,
336+
collectionId: params.collectionId,
337+
maxFeatures: params.maxFeatures,
338+
bbox: params.bbox || undefined,
339+
datetime: params.datetime || undefined,
340+
extraQuery: params.extraQuery || undefined,
341+
},
342+
{
343+
featureCount: params.data.features.length,
344+
service: "ogc-features",
345+
sourceKind: OGC_FEATURES_SOURCE_KIND,
346+
collectionId: params.collectionId,
347+
...(params.numberMatched !== undefined ? { numberMatched: params.numberMatched } : {}),
348+
truncated: params.truncated,
349+
},
350+
),
351+
geojson: params.data,
352+
sourcePath: params.itemsUrl,
353+
};
354+
}
355+
298356
// --- ArcGIS ----------------------------------------------------------------
299357

300358
export interface ArcGISOptions {

apps/geolibre-desktop/src/components/layout/add-data/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type KindI18nKey =
1616
| "wms"
1717
| "wfs"
1818
| "wmts"
19+
| "ogcFeatures"
1920
| "ogcVectorTiles"
2021
| "gpx"
2122
| "georss"
@@ -39,6 +40,7 @@ export const KIND_I18N_KEY: Record<AddDataKind, KindI18nKey> = {
3940
wms: "wms",
4041
wfs: "wfs",
4142
wmts: "wmts",
43+
"ogc-features": "ogcFeatures",
4244
"ogc-vector-tiles": "ogcVectorTiles",
4345
gpx: "gpx",
4446
georss: "georss",
@@ -87,6 +89,11 @@ export const DEFAULT_WMTS_URL =
8789
// this to any EOX tile layer, however it was added.
8890
export const EOX_S2CLOUDLESS_ATTRIBUTION =
8991
'Sentinel-2 cloudless 2025 by <a href="https://s2maps.eu" target="_blank" rel="noreferrer">EOX IT Services GmbH</a> (contains modified Copernicus Sentinel data 2025)';
92+
// pygeoapi's public demo, the reference OGC API - Features implementation. Its
93+
// `lakes` collection is a small global polygon set, and the server caps `limit`
94+
// at 10 per page, so the sample also exercises the `next`-link paging walk.
95+
export const DEFAULT_OGC_FEATURES_ENDPOINT = "https://demo.pygeoapi.io/master";
96+
export const DEFAULT_OGC_FEATURES_COLLECTION = "lakes";
9097
// PDOK BGT (Dutch large-scale base map) served as OGC API - Tiles vector tiles.
9198
// The style document carries the source-layer names the TileJSON omits; both
9299
// are prefilled so the sample works out of the box (zoom into the Netherlands).

0 commit comments

Comments
 (0)