diff --git a/pygeoapi/api.py b/pygeoapi/api.py index 4400a29..76870e4 100644 --- a/pygeoapi/api.py +++ b/pygeoapi/api.py @@ -34,8 +34,9 @@ import os from jinja2 import Environment, FileSystemLoader +from pygeoapi import __version__ from pygeoapi.provider import load_provider -from pygeoapi.provider.base import ProviderConnectionError +from pygeoapi.provider.base import ProviderConnectionError, ProviderQueryError LOGGER = logging.getLogger(__name__) @@ -106,14 +107,13 @@ class API(object): 'rel': 'self', 'type': 'text/html', 'title': 'the OpenAPI definition as HTML', - 'href': '{}?f=html'.format(self.config['server']['url']) + 'href': '{}api?f=html'.format(self.config['server']['url']) } ] if format_ == 'html': # render headers_['Content-type'] = 'text/html' - content = _render_j2_template(self.config, 'service.html', fcm) - + content = _render_j2_template(self.config, 'root.html', fcm) return headers_, 200, content return headers_, 200, json.dumps(fcm) @@ -149,6 +149,17 @@ class API(object): 'Content-type': 'application/json' } + formats = ['json', 'html'] + + format_ = args.get('f') + if format_ is not None and format_ not in formats: + exception = { + 'code': 'InvalidParameterValue', + 'description': 'Invalid format' + } + LOGGER.error(exception) + return headers_, 400, json.dumps(exception) + conformance = { 'conformsTo': [ 'http://www.opengis.net/spec/wfs-1/3.0/req/core', @@ -158,6 +169,12 @@ class API(object): ] } + if format_ == 'html': # render + headers_['Content-type'] = 'text/html' + content = _render_j2_template(self.config, 'conformance.html', + conformance) + return headers_, 200, content + return headers_, 200, json.dumps(conformance) def describe_collections(self, headers, args, dataset=None): @@ -219,13 +236,19 @@ class API(object): collection['links'].append(lnk) if dataset is not None and k == dataset: - return headers_, 200, json.dumps(collection) + fcm = collection + break fcm['collections'].append(collection) if format_ == 'html': # render headers_['Content-type'] = 'text/html' - content = _render_j2_template(self.config, 'service.html', fcm) + if dataset is not None: + content = _render_j2_template(self.config, 'collection.html', + fcm) + else: + content = _render_j2_template(self.config, 'collections.html', + fcm) return headers_, 200, content @@ -242,10 +265,12 @@ class API(object): :returns: tuple of headers, status code, content """ + print(args) headers_ = { 'Content-type': 'application/json' } + LOGGER.debug('Processing query parameters') try: startindex = int(args.get('startindex')) except TypeError: @@ -256,6 +281,8 @@ class API(object): limit = self.config['server']['limit'] resulttype = args.get('resulttype') or 'results' + bbox = args.get('bbox') + time = args.get('time') if dataset not in self.config['datasets'].keys(): exception = { @@ -274,8 +301,15 @@ class API(object): try: content = p.query(startindex=int(startindex), limit=int(limit), - resulttype=resulttype) + resulttype=resulttype, bbox=bbox, time=time) except ProviderConnectionError: + exception = { + 'code': 'NoApplicableCode', + 'description': 'connection error (check logs)' + } + LOGGER.error(exception) + return headers_, 500, json.dumps(exception) + except ProviderQueryError: exception = { 'code': 'NoApplicableCode', 'description': 'query error (check logs)' @@ -371,4 +405,4 @@ def _render_j2_template(config, template, data): env = Environment(loader=FileSystemLoader(TEMPLATES)) print(TEMPLATES) template = env.get_template(template) - return template.render(config=config, data=data) + return template.render(config=config, data=data, version=__version__) diff --git a/pygeoapi/provider/base.py b/pygeoapi/provider/base.py index e0b7ddc..8d64a21 100644 --- a/pygeoapi/provider/base.py +++ b/pygeoapi/provider/base.py @@ -47,6 +47,7 @@ class BaseProvider(object): self.name = provider_def['name'] self.data = provider_def['data'] self.id_field = provider_def['id_field'] + self.time_field = provider_def.get('time_field') def query(self): """ @@ -97,3 +98,8 @@ class BaseProvider(object): class ProviderConnectionError(Exception): """query / backend error""" pass + + +class ProviderQueryError(Exception): + """query / backend error""" + pass diff --git a/pygeoapi/provider/elasticsearch_.py b/pygeoapi/provider/elasticsearch_.py index b580cc6..b0388c1 100644 --- a/pygeoapi/provider/elasticsearch_.py +++ b/pygeoapi/provider/elasticsearch_.py @@ -31,7 +31,8 @@ import logging from elasticsearch import Elasticsearch, exceptions -from pygeoapi.provider.base import BaseProvider, ProviderConnectionError +from pygeoapi.provider.base import (BaseProvider, ProviderConnectionError, + ProviderQueryError) LOGGER = logging.getLogger(__name__) @@ -63,33 +64,82 @@ class ElasticsearchProvider(BaseProvider): LOGGER.debug('Connecting to Elasticsearch') self.es = Elasticsearch(self.es_host) - def query(self, startindex=0, limit=10, resulttype='results'): + def query(self, startindex=0, limit=10, resulttype='results', + bbox=None, time=None): """ query Elasticsearch index :param startindex: starting record to return (default 0) :param limit: number of records to return (default 10) :param resulttype: return results or hit limit (default results) + :param bbox: bounding box (minx,miny,maxx,maxy) + :param time: temporal (datestamp or extent) :returns: dict of 0..n GeoJSON features """ + query = {'query': {'bool': {'filter': []}}} + filter_ = [] + feature_collection = { 'type': 'FeatureCollection', 'features': [] } - LOGGER.debug('Querying Elasticsearch') if resulttype == 'hits': LOGGER.debug('hits only specified') limit = 0 + if bbox is not None: + LOGGER.debug('processing bbox parameter') + minx, miny, maxx, maxy = bbox.split(',') + bbox_filter = { + 'geo_shape': { + 'geometry': { + 'shape': { + 'type': 'envelope', + 'coordinates': [[minx, miny], [maxx, maxy]] + }, + 'relation': 'intersects' + } + } + } + + query['query']['bool']['filter'].append(bbox_filter) + + if time is not None: + LOGGER.debug('processing time parameter') + if self.time_field is None: + LOGGER.error('time_field not enabled for collection') + raise ProviderQueryError() + + time_field = 'properties.{}'.format(self.time_field) + + if '/' in time: # envelope + range_ = {'range': time_field} + time_begin, time_end = time.split('/') + + if time_begin == '': # until + range_['range'][time_field]['lte'] = time_end + if time_end == '': # from + range_['range'][time_field]['gte'] = time_begin + filter_.append(range_) + + else: # time instant + filter_.append({'match': {time_field: time}}) + + query['query']['bool']['filter'].append(filter_) + try: + LOGGER.debug('Querying Elasticsearch') results = self.es.search(index=self.index_name, from_=startindex, - size=limit) + size=limit, body=query) except exceptions.ConnectionError as err: LOGGER.error(err) raise ProviderConnectionError() + except exceptions.RequestError as err: + LOGGER.error(err) + raise ProviderQueryError() feature_collection['numberMatched'] = results['hits']['total'] diff --git a/pygeoapi/templates/collection.html b/pygeoapi/templates/collection.html new file mode 100644 index 0000000..d298a6d --- /dev/null +++ b/pygeoapi/templates/collection.html @@ -0,0 +1,35 @@ + + + + + {{ config['metadata']['identification']['title'] }} - data['title'] + + + + + + + + + + +
+

{{ config['metadata']['identification']['title'] }}

+ {{ config['metadata']['identification']['description'] }} +
+
+

{{ data['title'] }}

+ {{ data['description'] }} +

Links

+ +
+
+ + + diff --git a/pygeoapi/templates/collections.html b/pygeoapi/templates/collections.html new file mode 100644 index 0000000..8640edd --- /dev/null +++ b/pygeoapi/templates/collections.html @@ -0,0 +1,33 @@ + + + + + {{ config['metadata']['identification']['title'] }} - Collections + + + + + + + + + + +
+

{{ config['metadata']['identification']['title'] }}

+ {{ config['metadata']['identification']['description'] }} +
+
+

Collections

+ +
+
+ + + diff --git a/pygeoapi/templates/conformance.html b/pygeoapi/templates/conformance.html new file mode 100644 index 0000000..8721258 --- /dev/null +++ b/pygeoapi/templates/conformance.html @@ -0,0 +1,33 @@ + + + + + {{ config['metadata']['identification']['title'] }} - Conformance + + + + + + + + + + +
+

{{ config['metadata']['identification']['title'] }}

+ {{ config['metadata']['identification']['description'] }} +
+
+

Conformance

+ +
+
+ + + diff --git a/pygeoapi/templates/service.html b/pygeoapi/templates/root.html similarity index 73% rename from pygeoapi/templates/service.html rename to pygeoapi/templates/root.html index 01f3a7d..d2ec0d0 100644 --- a/pygeoapi/templates/service.html +++ b/pygeoapi/templates/root.html @@ -16,7 +16,7 @@
-

{{ config['metadata']['identification']['title'] }}

+

{{ config['metadata']['identification']['title'] }}

{{ config['metadata']['identification']['description'] }}
@@ -28,11 +28,17 @@
-
-

Datasets

+
+

Conformance

+
+
+

Collections

+
+