Skip to content

Commit 865a489

Browse files
committed
update EDR and Maps to support MetOcean access and visualization workflows (#2213)
1 parent f98d5f9 commit 865a489

18 files changed

+320
-68
lines changed

docs/source/configuration.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ default.
225225
begin: 2000-10-30T18:24:39Z # start datetime in RFC3339
226226
end: 2007-10-30T08:57:29Z # end datetime in RFC3339
227227
trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian # TRS
228+
# additional extents can be added as desired (1..n)
229+
foo:
230+
url: https://example.org/def # required URL of the extent
231+
range: [0, 10] # required overall range/extent
232+
units: °C # optional units
233+
values: [0, 2, 5, 5, 10] # optional, enumeration of values
228234
providers: # list of 1..n required connections information
229235
- type: feature # underlying data geospatial type. Allowed values are: feature, coverage, record, tile, edr
230236
name: CSV # required: plugin name or import path. See Plugins section for more information.

docs/source/publishing/ogcapi-coverages.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,20 @@ The `Xarray`_ provider plugin reads and extracts `NetCDF`_ and `Zarr`_ data.
8989
format:
9090
name: zarr
9191
mimetype: application/zip
92+
options:
93+
zarr:
94+
consolidated: true
95+
squeeze: true
96+
9297
9398
.. note::
9499
`Zarr`_ files are directories with files and subdirectories. Therefore
95100
a zip file is returned upon request for said format.
96101

102+
.. note::
103+
104+
``options.zarr`` is a custom property that can be used to set `Zarr-specific open options`_.
105+
97106
.. note::
98107
When referencing `NetCDF`_ or `Zarr`_ data stored in an S3 bucket,
99108
be sure to provide the full S3 URL. Any parameters required to open the dataset
@@ -155,3 +164,4 @@ Data access examples
155164
.. _`Zarr`: https://zarr.readthedocs.io/en/stable
156165
.. _`GDAL raster driver short name`: https://gdal.org/drivers/raster/index.html
157166
.. _`pyproj.CRS.from_user_input`: https://pyproj4.github.io/pyproj/stable/api/crs/coordinate_system.html#pyproj.crs.CoordinateSystem.from_user_input
167+
.. _`Zarr-specific open options`: https://docs.xarray.dev/en/stable/generated/xarray.open_zarr.html

docs/source/publishing/ogcapi-edr.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,15 @@ The `xarray-edr`_ provider plugin reads and extracts `NetCDF`_ and `Zarr`_ data
9090
a zip file is returned upon request for said format.
9191

9292
.. note::
93+
94+
``options.zarr`` is a custom property that can be used to set `Zarr-specific open options`_.
95+
96+
.. note::
97+
9398
When referencing data stored in an S3 bucket, be sure to provide the full
9499
S3 URL. Any parameters required to open the dataset using fsspec can be added
95100
to the config file under `options` and `s3`, as shown above.
96101

97-
98102
SensorThingsEDR
99103
^^^^^^^^^^^^^^^
100104

@@ -143,3 +147,4 @@ Data access examples
143147
.. _`NetCDF`: https://en.wikipedia.org/wiki/NetCDF
144148
.. _`Zarr`: https://zarr.readthedocs.io/en/stable
145149
.. _`OGC Environmental Data Retrieval (EDR) (API)`: https://ogcapi.ogc.org/edr
150+
.. _`Zarr-specific open options`: https://docs.xarray.dev/en/stable/generated/xarray.open_zarr.html

docs/source/publishing/ogcapi-maps.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,5 +136,9 @@ Data visualization examples
136136

137137
* http://localhost:5000/collections/foo/map?bbox-crs=http%3A%2F%2Fwww.opengis.net%2Fdef%2Fcrs%2FEPSG%2F0%2F3857&bbox=4.022369384765626%2C50.690447870569436%2C4.681549072265626%2C51.00260125274477&width=800&height=600&transparent
138138

139+
* map with vertical subset (``extents.vertical`` must be set in resource level config)
140+
141+
* http://localhost:5000/collections/foo/map?bbox=-142,42,-52,84&subset=vertical(435)
142+
139143
.. _`OGC API - Maps`: https://ogcapi.ogc.org/maps
140144
.. _`see website`: https://mapserver.org/mapscript/index.html

pygeoapi/api/__init__.py

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Colin Blackburn <[email protected]>
88
# Ricardo Garcia Silva <[email protected]>
99
#
10-
# Copyright (c) 2025 Tom Kralidis
10+
# Copyright (c) 2026 Tom Kralidis
1111
# Copyright (c) 2025 Francesco Bartoli
1212
# Copyright (c) 2022 John A Stevenson and Colin Blackburn
1313
# Copyright (c) 2023 Ricardo Garcia Silva
@@ -588,6 +588,7 @@ def get_exception(self, status: int, headers: dict, format_: str | None,
588588
"""
589589

590590
exception_info = sys.exc_info()
591+
591592
LOGGER.error(
592593
description,
593594
exc_info=exception_info if exception_info[0] is not None else None
@@ -709,22 +710,22 @@ def landing_page(api: API,
709710
'title': l10n.translate('Collections', request.locale),
710711
'href': api.get_collections_url()
711712
}, {
712-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/processes',
713+
'rel': f'{OGC_RELTYPES_BASE}/processes',
713714
'type': FORMAT_TYPES[F_JSON],
714715
'title': l10n.translate('Processes', request.locale),
715716
'href': f"{api.base_url}/processes"
716717
}, {
717-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/job-list',
718+
'rel': f'{OGC_RELTYPES_BASE}/job-list',
718719
'type': FORMAT_TYPES[F_JSON],
719720
'title': l10n.translate('Jobs', request.locale),
720721
'href': f"{api.base_url}/jobs"
721722
}, {
722-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes',
723+
'rel': f'{OGC_RELTYPES_BASE}/tiling-schemes',
723724
'type': FORMAT_TYPES[F_JSON],
724725
'title': l10n.translate('The list of supported tiling schemes as JSON', request.locale), # noqa
725726
'href': f"{api.base_url}/TileMatrixSets?f=json"
726727
}, {
727-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes',
728+
'rel': f'{OGC_RELTYPES_BASE}/tiling-schemes',
728729
'type': FORMAT_TYPES[F_HTML],
729730
'title': l10n.translate('The list of supported tiling schemes as HTML', request.locale), # noqa
730731
'href': f"{api.base_url}/TileMatrixSets?f=html"
@@ -897,7 +898,10 @@ def describe_collections(api: API, request: APIRequest,
897898
'links': []
898899
}
899900

900-
bbox = v['extents']['spatial']['bbox']
901+
extents = deepcopy(v['extents'])
902+
903+
bbox = extents['spatial']['bbox']
904+
LOGGER.debug('Setting spatial extents from configuration')
901905
# The output should be an array of bbox, so if the user only
902906
# provided a single bbox, wrap it in a array.
903907
if not isinstance(bbox[0], list):
@@ -907,12 +911,13 @@ def describe_collections(api: API, request: APIRequest,
907911
'bbox': bbox
908912
}
909913
}
910-
if 'crs' in v['extents']['spatial']:
914+
if 'crs' in extents['spatial']:
911915
collection['extent']['spatial']['crs'] = \
912-
v['extents']['spatial']['crs']
916+
extents['spatial']['crs']
913917

914-
t_ext = v.get('extents', {}).get('temporal', {})
918+
t_ext = extents.get('temporal', {})
915919
if t_ext:
920+
LOGGER.debug('Setting temporal extents from configuration')
916921
begins = dategetter('begin', t_ext)
917922
ends = dategetter('end', t_ext)
918923
collection['extent']['temporal'] = {
@@ -921,6 +926,24 @@ def describe_collections(api: API, request: APIRequest,
921926
if 'trs' in t_ext:
922927
collection['extent']['temporal']['trs'] = t_ext['trs']
923928

929+
_ = extents.pop('spatial', None)
930+
_ = extents.pop('temporal', None)
931+
932+
for ek, ev in extents.items():
933+
LOGGER.debug(f'Adding extent {ek}')
934+
collection['extent'][ek] = {
935+
'definition': ev['url'],
936+
'interval': [ev['range']]
937+
}
938+
if 'units' in ev:
939+
collection['extent'][ek]['unit'] = ev['units']
940+
941+
if 'values' in ev:
942+
collection['extent'][ek]['grid'] = {
943+
'cellsCount': len(ev['values']),
944+
'coordinates': ev['values']
945+
}
946+
924947
LOGGER.debug('Processing configured collection links')
925948
for link in l10n.translate(v.get('links', []), request.locale):
926949
lnk = {
@@ -990,13 +1013,13 @@ def describe_collections(api: API, request: APIRequest,
9901013
if collection_data_type == 'record':
9911014
collection['links'].append({
9921015
'type': FORMAT_TYPES[F_JSON],
993-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/ogc-catalog',
1016+
'rel': f'{OGC_RELTYPES_BASE}/ogc-catalog',
9941017
'title': l10n.translate('Record catalogue as JSON', request.locale), # noqa
9951018
'href': f'{api.get_collections_url()}/{k}?f={F_JSON}'
9961019
})
9971020
collection['links'].append({
9981021
'type': FORMAT_TYPES[F_HTML],
999-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/ogc-catalog',
1022+
'rel': f'{OGC_RELTYPES_BASE}/ogc-catalog',
10001023
'title': l10n.translate('Record catalogue as HTML', request.locale), # noqa
10011024
'href': f'{api.get_collections_url()}/{k}?f={F_HTML}'
10021025
})
@@ -1021,13 +1044,13 @@ def describe_collections(api: API, request: APIRequest,
10211044
LOGGER.debug('Adding feature/record based links')
10221045
collection['links'].append({
10231046
'type': 'application/schema+json',
1024-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/queryables',
1047+
'rel': f'{OGC_RELTYPES_BASE}/queryables',
10251048
'title': l10n.translate('Queryables for this collection as JSON', request.locale), # noqa
10261049
'href': f'{api.get_collections_url()}/{k}/queryables?f={F_JSON}' # noqa
10271050
})
10281051
collection['links'].append({
10291052
'type': FORMAT_TYPES[F_HTML],
1030-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/queryables',
1053+
'rel': f'{OGC_RELTYPES_BASE}/queryables',
10311054
'title': l10n.translate('Queryables for this collection as HTML', request.locale), # noqa
10321055
'href': f'{api.get_collections_url()}/{k}/queryables?f={F_HTML}' # noqa
10331056
})
@@ -1135,19 +1158,20 @@ def describe_collections(api: API, request: APIRequest,
11351158
LOGGER.debug('Adding tile links')
11361159
collection['links'].append({
11371160
'type': FORMAT_TYPES[F_JSON],
1138-
'rel': f'http://www.opengis.net/def/rel/ogc/1.0/tilesets-{p.tile_type}', # noqa
1161+
'rel': f'{OGC_RELTYPES_BASE}/tilesets-{p.tile_type}',
11391162
'title': l10n.translate('Tiles as JSON', request.locale),
1140-
'href': f'{api.get_collections_url()}/{k}/tiles?f={F_JSON}' # noqa
1163+
'href': f'{api.get_collections_url()}/{k}/tiles?f={F_JSON}'
11411164
})
11421165
collection['links'].append({
11431166
'type': FORMAT_TYPES[F_HTML],
1144-
'rel': f'http://www.opengis.net/def/rel/ogc/1.0/tilesets-{p.tile_type}', # noqa
1167+
'rel': f'{OGC_RELTYPES_BASE}/tilesets-{p.tile_type}',
11451168
'title': l10n.translate('Tiles as HTML', request.locale),
1146-
'href': f'{api.get_collections_url()}/{k}/tiles?f={F_HTML}' # noqa
1169+
'href': f'{api.get_collections_url()}/{k}/tiles?f={F_HTML}'
11471170
})
11481171

11491172
try:
11501173
map_ = get_provider_by_type(v['providers'], 'map')
1174+
p = load_plugin('provider', map_)
11511175
except ProviderTypeError:
11521176
map_ = None
11531177

@@ -1158,15 +1182,36 @@ def describe_collections(api: API, request: APIRequest,
11581182
map_format = map_['format']['name']
11591183

11601184
title_ = l10n.translate('Map as', request.locale)
1161-
title_ = f"{title_} {map_format}"
1185+
title_ = f'{title_} {map_format}'
11621186

11631187
collection['links'].append({
11641188
'type': map_mimetype,
1165-
'rel': 'http://www.opengis.net/def/rel/ogc/1.0/map',
1189+
'rel': f'{OGC_RELTYPES_BASE}/map',
11661190
'title': title_,
1167-
'href': f"{api.get_collections_url()}/{k}/map?f={map_format}" # noqa
1191+
'href': f'{api.get_collections_url()}/{k}/map?f={map_format}'
11681192
})
11691193

1194+
if p._fields:
1195+
schema_reltype = f'{OGC_RELTYPES_BASE}/schema',
1196+
schema_links = [s for s in collection['links'] if
1197+
schema_reltype in s]
1198+
1199+
if not schema_links:
1200+
title_ = l10n.translate('Schema of collection in JSON', request.locale) # noqa
1201+
collection['links'].append({
1202+
'type': 'application/schema+json',
1203+
'rel': f'{OGC_RELTYPES_BASE}/schema',
1204+
'title': title_,
1205+
'href': f'{api.get_collections_url()}/{k}/schema?f=json' # noqa
1206+
})
1207+
title_ = l10n.translate('Schema of collection in HTML', request.locale) # noqa
1208+
collection['links'].append({
1209+
'type': 'text/html',
1210+
'rel': f'{OGC_RELTYPES_BASE}/schema',
1211+
'title': title_,
1212+
'href': f'{api.get_collections_url()}/{k}/schema?f=html' # noqa
1213+
})
1214+
11701215
try:
11711216
edr = get_provider_by_type(v['providers'], 'edr')
11721217
p = load_plugin('provider', edr)
@@ -1217,6 +1262,10 @@ def describe_collections(api: API, request: APIRequest,
12171262
}
12181263
}
12191264
}
1265+
1266+
if request.format is not None and request.format == 'json':
1267+
data_query['link']['type'] = 'application/vnd.cov+json'
1268+
12201269
collection['data_queries'][qt] = data_query
12211270

12221271
title1 = l10n.translate('query for this collection as JSON', request.locale) # noqa
@@ -1334,9 +1383,14 @@ def get_collection_schema(api: API, request: Union[APIRequest, Any],
13341383
p = load_plugin('provider', get_provider_by_type(
13351384
api.config['resources'][dataset]['providers'], 'coverage')) # noqa
13361385
except ProviderTypeError:
1337-
LOGGER.debug('Loading record provider')
1338-
p = load_plugin('provider', get_provider_by_type(
1339-
api.config['resources'][dataset]['providers'], 'record'))
1386+
try:
1387+
LOGGER.debug('Loading record provider')
1388+
p = load_plugin('provider', get_provider_by_type(
1389+
api.config['resources'][dataset]['providers'], 'record'))
1390+
except ProviderTypeError:
1391+
LOGGER.debug('Loading edr provider')
1392+
p = load_plugin('provider', get_provider_by_type(
1393+
api.config['resources'][dataset]['providers'], 'edr'))
13401394
except ProviderGenericError as err:
13411395
LOGGER.error(err)
13421396
return api.get_exception(

pygeoapi/api/coverages.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# Ricardo Garcia Silva <[email protected]>
99
# Bernhard Mallinger <[email protected]>
1010
#
11-
# Copyright (c) 2024 Tom Kralidis
11+
# Copyright (c) 2026 Tom Kralidis
1212
# Copyright (c) 2025 Francesco Bartoli
1313
# Copyright (c) 2022 John A Stevenson and Colin Blackburn
1414
# Copyright (c) 2023 Ricardo Garcia Silva
@@ -37,12 +37,13 @@
3737
#
3838
# =================================================================
3939

40-
40+
from copy import deepcopy
4141
import logging
4242
from http import HTTPStatus
4343
from typing import Tuple
4444

4545
from pygeoapi import l10n
46+
from pygeoapi.openapi import get_oas_30_parameters
4647
from pygeoapi.plugin import load_plugin
4748
from pygeoapi.provider.base import ProviderGenericError, ProviderTypeError
4849
from pygeoapi.util import (
@@ -216,8 +217,8 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
216217

217218
for k, v in get_visible_collections(cfg).items():
218219
try:
219-
load_plugin('provider', get_provider_by_type(
220-
collections[k]['providers'], 'coverage'))
220+
p = load_plugin('provider', get_provider_by_type(
221+
collections[k]['providers'], 'coverage'))
221222
except ProviderTypeError:
222223
LOGGER.debug('collection is not coverage based')
223224
continue
@@ -226,6 +227,11 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
226227
title = l10n.translate(v['title'], locale)
227228
description = l10n.translate(v['description'], locale)
228229

230+
parameters = get_oas_30_parameters(cfg, locale)
231+
232+
coll_properties = deepcopy(parameters)['properties']
233+
coll_properties['schema']['items']['enum'] = list(p.fields.keys())
234+
229235
paths[coverage_path] = {
230236
'get': {
231237
'summary': f'Get {title} coverage',
@@ -236,7 +242,9 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
236242
{'$ref': '#/components/parameters/lang'},
237243
{'$ref': '#/components/parameters/f'},
238244
{'$ref': '#/components/parameters/bbox'},
239-
{'$ref': '#/components/parameters/bbox-crs'}
245+
{'$ref': '#/components/parameters/bbox-crs'},
246+
{'$ref': f"{OPENAPI_YAML['oacov']}#/components/parameters/subset"}, # noqa
247+
coll_properties
240248
],
241249
'responses': {
242250
'200': {'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/responses/Features"}, # noqa

0 commit comments

Comments
 (0)