Skip to content

Commit 734dabe

Browse files
authored
update EDR and Maps to support MetOcean access and visualization workflows (geopython#2213) (geopython#2214)
* update EDR and Maps to support MetOcean access and visualization workflows (geopython#2213) * add EDR plugin documentation (geopython#2162)
1 parent f98d5f9 commit 734dabe

19 files changed

+374
-74
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/plugins.rst

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,15 @@ The core pygeoapi plugin registry can be found in ``pygeoapi.plugin.PLUGINS``.
3030

3131
Each plugin type implements its relevant base class as the API contract:
3232

33-
* data providers: ``pygeoapi.provider.base``
34-
* output formats: ``pygeoapi.formatter.base``
35-
* processes: ``pygeoapi.process.base``
36-
* process_manager: ``pygeoapi.process.manager.base``
33+
* data providers:
34+
35+
* features/records/maps: ``pygeoapi.provider.base.BaseProvider``
36+
* edr: ``pygeoapi.provider.base_edr.BaseEDRProvider``
37+
* tiles: ``pygeoapi.provider.tile.BaseTileProvider``
38+
39+
* output formats: ``pygeoapi.formatter.base.BaseFormatter``
40+
* processes: ``pygeoapi.process.base.BaseProcessor``
41+
* process_manager: ``pygeoapi.process.manager.base.BaseManager``
3742

3843
.. todo:: link PLUGINS to API doc
3944

@@ -150,7 +155,7 @@ option 2 above).
150155
Example: custom pygeoapi vector data provider
151156
---------------------------------------------
152157

153-
Lets consider the steps for a vector data provider plugin:
158+
Let's consider the steps for a vector data provider plugin:
154159

155160
Python code
156161
^^^^^^^^^^^
@@ -223,7 +228,7 @@ Each base class documents the functions, arguments and return types required for
223228
Example: custom pygeoapi raster data provider
224229
---------------------------------------------
225230

226-
Lets consider the steps for a raster data provider plugin:
231+
Let's consider the steps for a raster data provider plugin:
227232

228233
Python code
229234
^^^^^^^^^^^
@@ -278,6 +283,51 @@ Each base class documents the functions, arguments and return types required for
278283

279284
.. _example-custom-pygeoapi-processing-plugin:
280285

286+
Example: custom pygeoapi EDR data provider
287+
------------------------------------------
288+
289+
Let's consider the steps for an EDR data provider plugin:
290+
291+
Python code
292+
^^^^^^^^^^^
293+
294+
The below template provides a minimal example (let's call the file ``mycooledrdata.py``:
295+
296+
.. code-block:: python
297+
298+
from pygeoapi.provider.base_edr import BaseEDRProvider
299+
300+
class MyCoolEDRDataProvider(BaseEDRProvider):
301+
302+
def __init__(self, provider_def):
303+
"""Inherit from the parent class"""
304+
305+
super().__init__(provider_def)
306+
307+
self.covjson = {...}
308+
309+
def get_instances(self):
310+
return ['foo', 'bar']
311+
312+
def get_instance(self, instance):
313+
return instance in get_instances()
314+
315+
def position(self, **kwargs):
316+
return self.covjson
317+
318+
def trajectory(self, **kwargs):
319+
return self.covjson
320+
321+
322+
For brevity, the ``position`` function returns ``self.covjson`` which is a
323+
dictionary of a CoverageJSON representation. ``get_instances`` returns a list
324+
of instances associated with the collection/plugin, and ``get_instance`` returns
325+
a boolean of whether a given instance exists/is valid. EDR query types are subject
326+
to the query functions defined in the plugin. In the example above, the plugin
327+
implements ``position`` and ``trajectory`` queries, which will be advertised as
328+
supported query types.
329+
330+
281331
Example: custom pygeoapi processing plugin
282332
------------------------------------------
283333

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 <colb@bgs.ac.uk>
88
# Ricardo Garcia Silva <ricardo.garcia.silva@geobeyond.it>
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(

0 commit comments

Comments
 (0)