implement OGC EDR API (#658)

* implement OGC EDR API
* add docs/tests
* fix tests
This commit is contained in:
Tom Kralidis
2021-03-15 11:37:40 -04:00
committed by GitHub
parent 2063c69872
commit 951a1fb486
21 changed files with 830 additions and 22 deletions
+1 -1
View File
@@ -68,7 +68,7 @@ ARG ADD_DEB_PACKAGES="python3-gdal python3-psycopg2 python3-xarray python3-scipy
ENV TZ=${TIMEZONE} \
DEBIAN_FRONTEND="noninteractive" \
DEB_BUILD_DEPS="software-properties-common curl unzip" \
DEB_PACKAGES="python3-pip python3-setuptools python3-distutils python3-yaml python3-dateutil python3-tz python3-flask python3-flask-cors python3-unicodecsv python3-click python3-greenlet python3-gevent python3-wheel gunicorn libsqlite3-mod-spatialite ${ADD_DEB_PACKAGES}"
DEB_PACKAGES="python3-pip python3-setuptools python3-distutils python3-shapely python3-yaml python3-dateutil python3-tz python3-flask python3-flask-cors python3-unicodecsv python3-click python3-greenlet python3-gevent python3-wheel gunicorn libsqlite3-mod-spatialite ${ADD_DEB_PACKAGES}"
RUN mkdir -p /pygeoapi/pygeoapi
# Add files required for pip/setuptools
+1 -1
View File
@@ -170,7 +170,7 @@ default.
# see pygeoapi.plugin for supported providers
# for custom built plugins, use the import path (e.g. mypackage.provider.MyProvider)
# see Plugins section for more information
- type: feature # underlying data geospatial type: (allowed values are: feature, coverage, record, tile)
- type: feature # underlying data geospatial type: (allowed values are: feature, coverage, record, tile, edr)
default: true # optional: if not specified, the first provider definition is considered the default
name: CSV
data: tests/data/obs.csv # required: the data filesystem path or URL, depending on plugin setup
+1
View File
@@ -23,4 +23,5 @@ return back data to the pygeoapi API framework in a plug and play fashion.
ogcapi-tiles
ogcapi-processes
ogcapi-records
ogcapi-edr
stac
@@ -79,7 +79,7 @@ The `xarray`_ provider plugin reads and extracts `NetCDF`_ and `Zarr`_ data.
.. note::
`Zarr`_ files are directories with files and subdirectories. Therefore
a zip file is returned upon request for said format.
a zip file is returned upon request for said format.
Data access examples
--------------------
@@ -0,0 +1,84 @@
.. _ogcapi-edr:
Publishing data to OGC API - Environmental Data Retrieval
=========================================================
The `OGC Environmental Data Retrieval (EDR) (API)`_ provides a family of
lightweight query interfaces to access spatio-temporal data resources.
To add spatio-temporal data to pygeoapi for EDR query interfaces, you
can use the dataset example in :ref:`configuration` as a baseline and
modify accordingly.
Providers
---------
pygeoapi core EDR providers are listed below, along with a matrix of supported query
parameters.
.. csv-table::
:header: Provider, coords, parameter-name, datetime
:align: left
xarray-edr,✅,✅,✅
Below are specific connection examples based on supported providers.
Connection examples
-------------------
xarray-edr
^^^^^^^^^^
The `xarray-edr`_ provider plugin reads and extracts `NetCDF`_ and `Zarr`_ data via `xarray`_.
.. code-block:: yaml
providers:
- type: edr
name: xarray-edr
data: tests/data/coads_sst.nc
# optionally specify x/y/time fields, else provider will attempt
# to derive automagically
x_field: lat
x_field: lon
time_field: time
format:
name: netcdf
mimetype: application/x-netcdf
providers:
- type: edr
name: xarray-edr
data: tests/data/analysed_sst.zarr
format:
name: zarr
mimetype: application/zip
.. note::
`Zarr`_ files are directories with files and subdirectories. Therefore
a zip file is returned upon request for said format.
Data access examples
--------------------
- list all collections
- http://localhost:5000/collections
- overview of dataset
- http://localhost:5000/collections/foo
- dataset position query
- http://localhost:5000/collections/foo/position?coords=POINT(-75%2045)
- dataset position query for a specific parameter
- http://localhost:5000/collections/foo/position?coords=POINT(-75%2045)&parameter-name=SST
- dataset position query for a specific parameter and time step
- http://localhost:5000/collections/foo/position?coords=POINT(-75%2045)&parameter-name=SST&datetime=2000-01-16
.. _`xarray`: https://xarray.pydata.org
.. _`NetCDF`: https://en.wikipedia.org/wiki/NetCDF
.. _`Zarr`: https://zarr.readthedocs.io/en/stable
.. _`OGC Environmental Data Retrieval (EDR) (API)`: https://github.com/opengeospatial/ogcapi-coverages
+8 -1
View File
@@ -10,7 +10,12 @@ Features
- out of the box modern OGC API server
- certified OGC Compliant and Reference Implementation for OGC API - Features
- additionally implements OGC API - Coverages, OGC API - Tiles, OGC API - Processes, OGC API - Records and SpatioTemporal Asset Library
- additionally implements
- OGC API - Coverages
- OGC API - Tiles
- OGC API - Processes
- OGC API - Environmental Data Retrieval
- SpatioTemporal Asset Library
- out of the box data provider plugins for rasterio, GDAL/OGR, Elasticsearch, PostgreSQL/PostGIS
- easy to use OpenAPI / Swagger documentation for developers
- supports JSON, GeoJSON, HTML and CSV output
@@ -41,6 +46,7 @@ Standards are at the core of pygeoapi. Below is the project's standards support
`OGC API - Tiles`_,Implementing
`OGC API - Processes`_,Implementing
`OGC API - Records`_,Implementing
`OGC API - Environmental Data Retrieval`_,Implementing
`SpatioTemporal Asset Catalog`_,Implementing
@@ -51,4 +57,5 @@ Standards are at the core of pygeoapi. Below is the project's standards support
.. _`OGC API - Tiles`: https://github.com/opengeospatial/ogcapi-tiles
.. _`OGC API - Processes`: https://github.com/opengeospatial/ogcapi-processes
.. _`OGC API - Records`: https://github.com/opengeospatial/ogcapi-records
.. _`OGC API - Environmental Data Retrieval`: https://github.com/opengeospatial/ogcapi-environmental-data-retrieval
.. _`SpatioTemporal Asset Catalog`: https://stacspec.org
+20
View File
@@ -148,6 +148,7 @@ This page provides metadata catalogue search capabilities
.. seealso::
:ref:`ogcapi-records` for more OGC API - Records request examples.
Processes
---------
@@ -160,6 +161,25 @@ The processes page provides a list of process integrated onto the server, along
:ref:`ogcapi-processes` for more OGC API - Processes request examples.
Environmental data retrieval
----------------------------
http://localhost:5000/collections/edr-test
This page provides, in addition to a common collection description, specific
link relations for EDR queries if the collection has an EDR capability, as
well as supported parameter names to select.
http://localhost:5000/collections/edr-test/position?coords=POINT(111 13)&parameter-name=SST&f=json
This page executes a position query against a given parameter name, providing
a response in CoverageJSON.
.. seealso::
:ref:`ogcapi-edr` for more OGC API - EDR request examples.
SpatioTemporal Assets
---------------------
+26
View File
@@ -180,6 +180,32 @@ resources:
name: GRIB2
mimetype: application/x-grib2
icoads-sst:
type: collection
title: International Comprehensive Ocean-Atmosphere Data Set (ICOADS)
description: International Comprehensive Ocean-Atmosphere Data Set (ICOADS)
keywords:
- icoads
- sst
- air temperature
extents:
spatial:
bbox: [-180,-90,180,90]
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
links:
- type: text/html
rel: canonical
title: information
href: https://psl.noaa.gov/data/gridded/data.coads.1deg.html
hreflang: en-US
providers:
- type: edr
name: xarray-edr
data: tests/data/coads_sst.nc
format:
name: NetCDF
mimetype: application/x-netcdf
test-data:
type: stac-collection
title: pygeoapi test data
+186 -7
View File
@@ -43,6 +43,8 @@ import urllib.parse
from copy import deepcopy
from dateutil.parser import parse as dateparse
from shapely.wkt import loads as shapely_loads
from shapely.errors import WKTReadingError
import pytz
from pygeoapi import __version__
@@ -79,6 +81,8 @@ HEADERS = {
FORMATS = ['json', 'html', 'jsonld']
CONFORMANCE = [
'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core',
'http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections',
'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core',
'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30',
'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html',
@@ -88,13 +92,12 @@ CONFORMANCE = [
'http://www.opengis.net/spec/ogcapi-coverages-1/1.0/conf/html',
'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/core',
'http://www.opengis.net/spec/ogcapi-tiles-1/1.0/req/collections',
'http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core',
'http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections',
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/core',
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/sorting',
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/opensearch',
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/json',
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/html'
'http://www.opengis.net/spec/ogcapi-records-1/1.0/conf/html',
'http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/core'
]
OGC_RELTYPES_BASE = 'http://www.opengis.net/def/rel/ogc/1.0'
@@ -532,14 +535,15 @@ class API:
p = load_plugin('provider', get_provider_by_type(
self.config['resources'][k]['providers'],
'coverage'))
collection['crs'] = [p.crs]
collection['domainset'] = p.get_coverage_domainset()
collection['rangetype'] = p.get_coverage_rangetype()
except ProviderConnectionError:
msg = 'connection error (check logs)'
return self.get_exception(
500, headers_, format_, 'NoApplicableCode', msg)
collection['crs'] = [p.crs]
collection['domainset'] = p.get_coverage_domainset()
collection['rangetype'] = p.get_coverage_rangetype()
except ProviderTypeError:
pass
try:
tile = get_provider_by_type(v['providers'], 'tile')
@@ -563,6 +567,45 @@ class API:
self.config['server']['url'], k)
})
try:
edr = get_provider_by_type(v['providers'], 'edr')
except ProviderTypeError:
edr = None
if edr and dataset is not None:
LOGGER.debug('Adding EDR links')
try:
p = load_plugin('provider', get_provider_by_type(
self.config['resources'][dataset]['providers'],
'edr'))
parameters = p.get_fields()
if parameters:
collection['parameters'] = {}
for f in parameters['field']:
collection['parameters'][f['id']] = f
for qt in p.get_query_types():
collection['links'].append({
'type': 'text/json',
'rel': 'data',
'title': '{} query for this collection as JSON'.format(qt), # noqa
'href': '{}/collections/{}/{}?f=json'.format(
self.config['server']['url'], k, qt)
})
collection['links'].append({
'type': 'text/html',
'rel': 'data',
'title': '{} query for this collection as HTML'.format(qt), # noqa
'href': '{}/collections/{}/{}?f=html'.format(
self.config['server']['url'], k, qt)
})
except ProviderConnectionError:
msg = 'connection error (check logs)'
return self.get_exception(
500, headers_, format_, 'NoApplicableCode', msg)
except ProviderTypeError:
pass
if dataset is not None and k == dataset:
fcm = collection
break
@@ -2115,6 +2158,142 @@ tiles/{{{}}}/{{{}}}/{{{}}}/{{{}}}?f=mvt'
LOGGER.info(response)
return {}, http_status, response
def get_collection_edr_query(self, headers, args, dataset, instance,
query_type):
"""
Queries collection EDR
:param headers: dict of HTTP headers
:param args: dict of HTTP request parameters
:param dataset: dataset name
:param dataset: instance name
:param query_type: EDR query type
:returns: tuple of headers, status code, content
"""
headers_ = HEADERS.copy()
query_args = {}
formats = FORMATS
formats.extend(f.lower() for f in PLUGINS['formatter'].keys())
collections = filter_dict_by_key_value(self.config['resources'],
'type', 'collection')
format_ = check_format(args, headers)
if dataset not in collections.keys():
msg = 'Invalid collection'
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
if format_ is not None and format_ not in formats:
msg = 'Invalid format'
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
LOGGER.debug('Processing query parameters')
LOGGER.debug('Processing datetime parameter')
datetime_ = args.get('datetime')
try:
datetime_ = validate_datetime(collections[dataset]['extents'],
datetime_)
except ValueError as err:
msg = str(err)
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
LOGGER.debug('Processing parameter-name parameter')
parameternames = args.get('parameter-name', [])
if parameternames:
parameternames = parameternames.split(',')
LOGGER.debug('Processing coords parameter')
wkt = args.get('coords', None)
if wkt is None:
msg = 'missing coords parameter'
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
try:
wkt = shapely_loads(wkt)
except WKTReadingError:
msg = 'invalid coords parameter'
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
LOGGER.debug('Processing z parameter')
z = args.get('z')
LOGGER.debug('Loading provider')
try:
p = load_plugin('provider', get_provider_by_type(
collections[dataset]['providers'], 'edr'))
except ProviderTypeError:
msg = 'invalid provider type'
return self.get_exception(
500, headers_, format_, 'NoApplicableCode', msg)
except ProviderConnectionError:
msg = 'connection error (check logs)'
return self.get_exception(
500, headers_, format_, 'NoApplicableCode', msg)
except ProviderQueryError:
msg = 'query error (check logs)'
return self.get_exception(
500, headers_, format_, 'NoApplicableCode', msg)
if instance is not None and not p.get_instance(instance):
msg = 'Invalid instance identifier'
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
if query_type not in p.get_query_types():
msg = 'Unsupported query type'
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
parametername_matches = list(
filter(
lambda p: p['id'] in parameternames, p.get_fields()['field']
)
)
if len(parametername_matches) < len(parameternames):
msg = 'Invalid parameter-name'
return self.get_exception(
400, headers_, format_, 'InvalidParameterValue', msg)
query_args = dict(
query_type=query_type,
instance=instance,
format_=format_,
datetime_=datetime_,
select_properties=parameternames,
wkt=wkt,
z=z
)
try:
data = p.query(**query_args)
except ProviderNoDataError:
msg = 'No data found'
return self.get_exception(
204, headers_, format_, 'NoMatch', msg)
except ProviderQueryError:
msg = 'query error (check logs)'
return self.get_exception(
500, headers_, format_, 'NoApplicableCode', msg)
if format_ == 'html': # render
headers_['Content-Type'] = 'text/html'
content = render_j2_template(
self.config, 'collections/edr/query.html', data)
else:
content = to_json(data, self.pretty_print)
return headers_, 200, content
@pre_process
@jsonldify
def get_stac_root(self, headers_, format_):
+33
View File
@@ -465,6 +465,39 @@ def get_process_job_result_resource(process_id, job_id, resource):
return response
@BLUEPRINT.route('/collections/<collection_id>/position')
@BLUEPRINT.route('/collections/<collection_id>/area')
@BLUEPRINT.route('/collections/<collection_id>/cube')
@BLUEPRINT.route('/collections/<collection_id>/trajectory')
@BLUEPRINT.route('/collections/<collection_id>/corridor')
@BLUEPRINT.route('/collections/<collection_id>/instances/<instance_id>/position') # noqa
@BLUEPRINT.route('/collections/<collection_id>/instances/<instance_id>/area')
@BLUEPRINT.route('/collections/<collection_id>/instances/<instance_id>/cube')
@BLUEPRINT.route('/collections/<collection_id>/instances/<instance_id>/trajectory') # noqa
@BLUEPRINT.route('/collections/<collection_id>/instances/<instance_id>/corridor') # noqa
def get_collection_edr_query(collection_id, instance_id=None):
"""
OGC EDR API endpoints
:param collection_id: collection identifier
:param instance_id: instance identifier
:returns: HTTP response
"""
query_type = request.path.split('/')[-1]
headers, status_code, content = api_.get_collection_edr_query(
request.headers, request.args, collection_id, instance_id, query_type)
response = make_response(content, status_code)
if headers:
response.headers = headers
return response
@BLUEPRINT.route('/stac')
def stac_catalog_root():
"""
+52 -1
View File
@@ -48,7 +48,8 @@ OPENAPI_YAML = {
'oacov': 'https://raw.githubusercontent.com/tomkralidis/ogcapi-coverages-1/fix-cis/yaml-unresolved', # noqa
'oapit': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerhub/tiles.yaml', # noqa
'oapimt': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-tiles/master/openapi/swaggerhub/map-tiles.yaml', # noqa
'oapir': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi' # noqa
'oapir': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-records/master/core/openapi', # noqa
'oaedr': 'https://raw.githubusercontent.com/opengeospatial/ogcapi-environmental-data-retrieval/master/candidate-standard/openapi', # noqa
}
@@ -726,6 +727,56 @@ def get_oas_30(cfg):
}
}
LOGGER.debug('setting up tiles endpoints')
edr_extension = filter_providers_by_type(
collections[k]['providers'], 'edr')
if edr_extension:
ep = load_plugin('provider', edr_extension)
edr_query_endpoints = []
for qt in ep.get_query_types():
edr_query_endpoints.append({
'path': '{}/{}'.format(collection_name_path, qt),
'qt': qt,
'op_id': 'query{}{}'.format(qt.capitalize(), k.capitalize()) # noqa
})
if ep.instances:
edr_query_endpoints.append({
'path': '{}/instances/{{instanceId}}/{}'.format(collection_name_path, qt), # noqa
'qt': qt,
'op_id': 'query{}Instance{}'.format(qt.capitalize(), k.capitalize()) # noqa
})
for eqe in edr_query_endpoints:
paths[eqe['path']] = {
'get': {
'summary': 'query {} by {}'.format(v['description'], eqe['qt']), # noqa
'description': v['description'],
'tags': [k],
'operationId': eqe['op_id'],
'parameters': [
{'$ref': '{}/parameters/{}Coords.yaml'.format(OPENAPI_YAML['oaedr'], eqe['qt'])}, # noqa
{'$ref': '{}#/components/parameters/datetime'.format(OPENAPI_YAML['oapif'])}, # noqa
{'$ref': '{}/parameters/parameter-name.yaml'.format(OPENAPI_YAML['oaedr'])}, # noqa
{'$ref': '{}/parameters/z.yaml'.format(OPENAPI_YAML['oaedr'])}, # noqa
{'$ref': '#/components/parameters/f'}
],
'responses': {
'200': {
'description': 'Response',
'content': {
'application/prs.coverage+json': {
'schema': {
'$ref': '{}/schemas/coverageJSON.yaml'.format(OPENAPI_YAML['oaedr'])} # noqa
}
}
}
}
}
}
LOGGER.debug('setting up STAC')
stac_collections = filter_dict_by_key_value(cfg['resources'],
'type', 'stac-collection')
+2 -1
View File
@@ -49,7 +49,8 @@ PLUGINS = {
'rasterio': 'pygeoapi.provider.rasterio_.RasterioProvider',
'xarray': 'pygeoapi.provider.xarray_.XarrayProvider',
'MVT': 'pygeoapi.provider.mvt.MVTProvider',
'TinyDBCatalogue': 'pygeoapi.provider.tinydb_.TinyDBCatalogueProvider'
'TinyDBCatalogue': 'pygeoapi.provider.tinydb_.TinyDBCatalogueProvider',
'xarray-edr': 'pygeoapi.provider.xarray_edr.XarrayEDRProvider'
},
'formatter': {
'CSV': 'pygeoapi.formatter.csv_.CSVFormatter'
-2
View File
@@ -70,8 +70,6 @@ class FileSystemProvider(BaseProvider):
thispath = os.path.join(baseurl, urlpath)
print("THISPATH", thispath)
resource_type = None
root_link = None
child_links = []
+13 -4
View File
@@ -57,7 +57,7 @@ class XarrayProvider(BaseProvider):
super().__init__(provider_def)
try:
if provider_def['format']['name'] == 'zarr':
if provider_def['data'].endswith('.zarr'):
open_func = xarray.open_zarr
else:
open_func = xarray.open_dataset
@@ -312,10 +312,19 @@ class XarrayProvider(BaseProvider):
minx, miny, maxx, maxy = metadata['bbox']
mint, maxt = metadata['time']
if data.coords[self.y_field].values[0] > data.coords[self.y_field].values[-1]: # noqa
try:
tmp_min = data.coords[self.y_field].values[0]
except IndexError:
tmp_min = data.coords[self.y_field].values
try:
tmp_max = data.coords[self.y_field].values[-1]
except IndexError:
tmp_max = data.coords[self.y_field].values
if tmp_min > tmp_max:
LOGGER.debug('Reversing direction of {}'.format(self.y_field))
miny = data.coords[self.y_field].values[-1]
maxy = data.coords[self.y_field].values[0]
miny = tmp_max
maxy = tmp_min
cj = {
'type': 'Coverage',
+166
View File
@@ -0,0 +1,166 @@
# =================================================================
#
# Authors: Tom Kralidis <tomkralidis@gmail.com>
#
# Copyright (c) 2020 Tom Kralidis
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# =================================================================
import logging
from pygeoapi.provider.base import ProviderNoDataError
from pygeoapi.provider.xarray_ import _to_datetime_string, XarrayProvider
LOGGER = logging.getLogger(__name__)
class XarrayEDRProvider(XarrayProvider):
"""EDR Provider"""
def __init__(self, provider_def):
"""
Initialize object
:param provider_def: provider definition
:returns: pygeoapi.provider.rasterio_.RasterioProvider
"""
XarrayProvider.__init__(self, provider_def)
self.instances = []
def get_fields(self):
"""
Get provider field information (names, types)
:returns: dict of dicts of parameters
"""
return self.get_coverage_rangetype()
def get_instance(self, instance):
"""
Validate instance identifier
:returns: `bool` of whether instance is valid
"""
return NotImplementedError()
def get_query_types(self):
"""
Provide supported query types
:returns: list of EDR query types
"""
return ['position']
def query(self, **kwargs):
"""
Extract data from collection collection
:param query_type: query type
:param wkt: `shapely.geometry` WKT geometry
:param datetime_: temporal (datestamp or extent)
:param select_properties: list of parameters
:param z: vertical level(s)
:param format_: data format of output
:returns: coverage data as dict of CoverageJSON or native format
"""
query_params = {}
LOGGER.debug('Query parameters: {}'.format(kwargs))
LOGGER.debug('Query type: {}'.format(kwargs.get('query_type')))
wkt = kwargs.get('wkt')
if wkt is not None:
LOGGER.debug('Processing WKT')
LOGGER.debug('Geometry type: {}'.format(wkt.type))
if wkt.type == 'Point':
query_params[self._coverage_properties['x_axis_label']] = wkt.x
query_params[self._coverage_properties['y_axis_label']] = wkt.y
elif wkt.type == 'LineString':
query_params[self._coverage_properties['x_axis_label']] = wkt.xy[0] # noqa
query_params[self._coverage_properties['y_axis_label']] = wkt.xy[1] # noqa
elif wkt.type == 'Polygon':
query_params[self._coverage_properties['x_axis_label']] = slice(wkt.bounds[0], wkt.bounds[2]) # noqa
query_params[self._coverage_properties['y_axis_label']] = slice(wkt.bounds[1], wkt.bounds[3]) # noqa
pass
LOGGER.debug('Processing parameter-name')
select_properties = kwargs.get('select_properties')
# example of fetching instance passed
# TODO: apply accordingly
instance = kwargs.get('instance')
LOGGER.debug('instance: {}'.format(instance))
datetime_ = kwargs.get('datetime_')
if datetime_ is not None:
query_params[self._coverage_properties['time_axis_label']] = datetime_ # noqa
LOGGER.debug('query parameters: {}'.format(query_params))
try:
if select_properties:
self.fields = select_properties
data = self._data[[*select_properties]]
else:
data = self._data
data = data.sel(query_params, method='nearest')
except KeyError:
raise ProviderNoDataError()
if len(data.coords[self.time_field].values) < 1:
raise ProviderNoDataError()
try:
height = data.dims[self.y_field]
except KeyError:
height = 1
try:
width = data.dims[self.x_field]
except KeyError:
width = 1
bbox = wkt.bounds
out_meta = {
'bbox': [bbox[0], bbox[1], bbox[2], bbox[3]],
"time": [
_to_datetime_string(data.coords[self.time_field].values[0]),
_to_datetime_string(data.coords[self.time_field].values[-1])
],
"driver": "xarray",
"height": height,
"width": width,
"time_steps": data.dims[self.time_field],
"variables": {var_name: var.attrs
for var_name, var in data.variables.items()}
}
return self.gen_covjson(out_meta, data, self.fields)
+40
View File
@@ -467,6 +467,46 @@ async def get_process_job_result_resource(request: Request, process_id=None,
return response
@app.route('/collections/{collection_id}/position')
@app.route('/collections/{collection_id}/area')
@app.route('/collections/{collection_id}/cube')
@app.route('/collections/{collection_id}/trajectory')
@app.route('/collections/{collection_id}/corridor')
@app.route('/collections/{collection_id}/instances/{instance_id}/position')
@app.route('/collections/{collection_id}/instances/{instance_id}/area')
@app.route('/collections/{collection_id}/instances/{instance_id}/cube')
@app.route('/collections/{collection_id}/instances/{instance_id}/trajectory')
@app.route('/collections/{collection_id}/instances/{instance_id}/corridor')
async def get_collection_edr_query(request: Request, collection_id=None, instance_id=None): # noqa
"""
OGC EDR API endpoints
:param collection_id: collection identifier
:param instance_id: instance identifier
:returns: HTTP response
"""
if 'collection_id' in request.path_params:
collection_id = request.path_params['collection_id']
if 'instance_id' in request.path_params:
instance_id = request.path_params['instance_id']
query_type = request.path.split('/')[-1]
headers, status_code, content = api_.get_collection_edr_query(
request.headers, request.query_params, collection_id, instance_id,
query_type)
response = Response(content=content, status_code=status_code)
if headers:
response.headers.update(headers)
return response
@app.route('/stac')
async def stac_catalog_root(request: Request):
"""
@@ -0,0 +1,80 @@
{% extends "_base.html" %}
{% block title %}{{ super() }} {{ data['title'] }} {% endblock %}
{% block crumbs %}{{ super() }}
/ <a href="{{ data['collections_path'] }}">Collections</a>
{% for link in data['links'] %}
{% if link.rel == 'collection' %} /
<a href="{{ data['dataset_path'] }}">{{ link['title'] | truncate( 25 ) }}</a>
{% set col_title = link['title'] %}
{% endif %}
{% endfor %}
/ <a href="{{ data['items_path']}}">Items</a>
{% endblock %}
{% block extrahead %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css"/>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/leaflet-coverage@0.7/leaflet-coverage.css">
<script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
<script src="https://unpkg.com/covutils@0.6/covutils.min.js"></script>
<script src="https://unpkg.com/covjson-reader@0.16/covjson-reader.src.js"></script>
<script src="https://unpkg.com/leaflet-coverage@0.7/leaflet-coverage.min.js"></script>
{% endblock %}
{% block body %}
<section id="coverage">
<div id="items-map"></div>
</section>
{% endblock %}
{% block extrafoot %}
{% if data %}
<!--
<script>
var map = L.map('items-map').setView([{{ 45 }}, {{ -75 }}], 5);
map.addLayer(new L.TileLayer(
'{{ config['server']['map']['url'] }}', {
maxZoom: 18,
attribution: '{{ config['server']['map']['attribution'] }}'
}
));
var cov = CovJSON.read(JSON.parse('{{ data |to_json }}'), parameters: ['SST']);
//var cov = CovJSON.load('https://raw.githubusercontent.com/covjson/cookbook/master/examples/coverages/grid.covjson');
var layer = new C.dataLayer(cov, {parameter: 'SST'});
</script>
-->
<script>
var map = L.map('items-map').setView([{{ 45 }}, {{ -75 }}], 5);
map.addLayer(new L.TileLayer(
'{{ config['server']['map']['url'] }}', {
maxZoom: 18,
attribution: '{{ config['server']['map']['attribution'] }}'
}
));
var layers = L.control.layers(null, null, {collapsed: false}).addTo(map)
var layer
CovJSON.read(JSON.parse('{{ data | to_json }}')).then(function (coverage) {
layer = C.dataLayer(coverage, {parameter: 'SST'})
.on('afterAdd', function () {
C.legend(layer).addTo(map)
map.fitBounds(layer.getBounds())
})
.addTo(map)
layers.addOverlay(layer, 'Temperature')
map.setZoom(5)
})
map.on('click', function (e) {
new C.DraggableValuePopup({
layers: [layer]
}).setLatLng(e.latlng).openOn(map)
})
</script>
{% endif %}
{% endblock %}
+1
View File
@@ -5,4 +5,5 @@ python-dateutil
pytz
PyYAML
rasterio
shapely
unicodecsv
+26
View File
@@ -225,6 +225,32 @@ resources:
name: GRIB
mimetype: application/x-grib2
icoads-sst:
type: collection
title: International Comprehensive Ocean-Atmosphere Data Set (ICOADS)
description: International Comprehensive Ocean-Atmosphere Data Set (ICOADS)
keywords:
- icoads
- sst
- air temperature
extents:
spatial:
bbox: [-180,-90,180,90]
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
links:
- type: text/html
rel: canonical
title: information
href: https://psl.noaa.gov/data/gridded/data.coads.1deg.html
hreflang: en-US
providers:
- type: edr
name: xarray-edr
data: tests/data/coads_sst.nc
format:
name: NetCDF
mimetype: application/x-netcdf
hello-world:
type: process
processor:
+88 -2
View File
@@ -177,7 +177,7 @@ def test_conformance(config, api_):
assert isinstance(root, dict)
assert 'conformsTo' in root
assert len(root['conformsTo']) == 16
assert len(root['conformsTo']) == 17
rsp_headers, code, response = api_.conformance(req_headers, {'f': 'foo'})
assert code == 400
@@ -201,7 +201,7 @@ def test_describe_collections(config, api_):
collections = json.loads(response)
assert len(collections) == 2
assert len(collections['collections']) == 4
assert len(collections['collections']) == 5
assert len(collections['links']) == 3
rsp_headers, code, response = api_.describe_collections(
@@ -1116,6 +1116,92 @@ def test_delete_process_job(api_):
assert code == 404
def test_get_collection_edr_query(config, api_):
# no coords parameter
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {}, 'icoads-sst', instance=None, query_type='position')
assert code == 400
# bad query type
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {'coords': 'POINT(11 11)'}, 'icoads-sst', instance=None,
query_type='corridor')
assert code == 400
# bad coords parameter
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {'coords': 'gah'}, 'icoads-sst', instance=None,
query_type='position')
assert code == 400
# bad parameter-name parameter
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {'coords': 'POINT(11 11)', 'parameter-name': 'bad'},
'icoads-sst', instance=None, query_type='position')
assert code == 400
# all parameters
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {'coords': 'POINT(11 11)'}, 'icoads-sst', instance=None,
query_type='position')
assert code == 200
data = json.loads(response)
axes = list(data['domain']['axes'].keys())
axes.sort()
assert len(axes) == 3
assert axes == ['TIME', 'x', 'y']
assert data['domain']['axes']['x']['start'] == 11.0
assert data['domain']['axes']['x']['stop'] == 11.0
assert data['domain']['axes']['y']['start'] == 11.0
assert data['domain']['axes']['y']['stop'] == 11.0
parameters = list(data['parameters'].keys())
parameters.sort()
assert len(parameters) == 4
assert parameters == ['AIRT', 'SST', 'UWND', 'VWND']
# single parameter
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {'coords': 'POINT(11 11)', 'parameter-name': 'SST'},
'icoads-sst', instance=None, query_type='position')
assert code == 200
data = json.loads(response)
assert len(data['parameters'].keys()) == 1
assert list(data['parameters'].keys())[0] == 'SST'
# some data
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {'coords': 'POINT(11 11)', 'datetime': '2000-01-16'},
'icoads-sst', instance=None, query_type='position')
assert code == 200
# no data
req_headers = make_req_headers()
rsp_headers, code, response = api_.get_collection_edr_query(
req_headers, {'coords': 'POINT(11 11)', 'datetime': '2000-01-17'},
'icoads-sst', instance=None, query_type='position')
assert code == 204
# no data
# req_headers = make_req_headers()
# rsp_headers, code, response = api_.get_collection_edr_query(
# req_headers, {'coords': 'POINT(11 11)', 'datetime': '2000-01-15'},
# 'icoads-sst', instance=None, query_type='position')
# assert code == 204
def test_validate_bbox():
assert validate_bbox('1,2,3,4') == [1, 2, 3, 4]
assert validate_bbox('-142,42,-52,84') == [-142, 42, -52, 84]
+1 -1
View File
@@ -124,7 +124,7 @@ def test_filter_dict_by_key_value():
collections = util.filter_dict_by_key_value(d['resources'],
'type', 'collection')
assert len(collections) == 4
assert len(collections) == 5
notfound = util.filter_dict_by_key_value(d['resources'],
'type', 'foo')