diff --git a/pygeoapi/Dockerfile b/pygeoapi/Dockerfile index 2e8c3d3..d8c417b 100644 --- a/pygeoapi/Dockerfile +++ b/pygeoapi/Dockerfile @@ -1,4 +1,4 @@ -FROM webbben/pygeoapi-river-runner +FROM geopython/pygeoapi:latest #Add data directory RUN mkdir /data @@ -10,3 +10,7 @@ ADD https://www.hydroshare.org/resource/4a22e88e689949afa1cf71ae009eaf1b/data/co COPY ./pygeoapi.config.yml /pygeoapi/local.config.yml COPY ./schemas.opengis.net /opt/schemas.opengis.net COPY ./pygeoapi-skin-dashboard /skin-dashboard + +#Add river runner plugin +COPY ./plugin.py /pygeoapi/pygeoapi/plugin.py +COPY ./river_runner.py /pygeoapi/pygeoapi/process/river_runner.py diff --git a/pygeoapi/plugin.py b/pygeoapi/plugin.py new file mode 100644 index 0000000..9db51ab --- /dev/null +++ b/pygeoapi/plugin.py @@ -0,0 +1,113 @@ +# ================================================================= +# +# Authors: Tom Kralidis +# +# Copyright (c) 2021 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. +# +# ================================================================= +"""Plugin loader""" + +import importlib +import logging + +LOGGER = logging.getLogger(__name__) + +#: Loads provider plugins to be used by pygeoapi,\ +#: formatters and processes available +PLUGINS = { + 'provider': { + 'CSV': 'pygeoapi.provider.csv_.CSVProvider', + 'Elasticsearch': 'pygeoapi.provider.elasticsearch_.ElasticsearchProvider', # noqa + 'ElasticsearchCatalogue': 'pygeoapi.provider.elasticsearch_.ElasticsearchCatalogueProvider', # noqa + 'GeoJSON': 'pygeoapi.provider.geojson.GeoJSONProvider', + 'OGR': 'pygeoapi.provider.ogr.OGRProvider', + 'PostgreSQL': 'pygeoapi.provider.postgresql.PostgreSQLProvider', + 'SQLiteGPKG': 'pygeoapi.provider.sqlite.SQLiteGPKGProvider', + 'MongoDB': 'pygeoapi.provider.mongo.MongoProvider', + 'FileSystem': 'pygeoapi.provider.filesystem.FileSystemProvider', + 'rasterio': 'pygeoapi.provider.rasterio_.RasterioProvider', + 'xarray': 'pygeoapi.provider.xarray_.XarrayProvider', + 'MVT': 'pygeoapi.provider.mvt.MVTProvider', + 'TinyDBCatalogue': 'pygeoapi.provider.tinydb_.TinyDBCatalogueProvider', + 'SensorThings': 'pygeoapi.provider.sensorthings.SensorThingsProvider', + 'xarray-edr': 'pygeoapi.provider.xarray_edr.XarrayEDRProvider' + }, + 'formatter': { + 'CSV': 'pygeoapi.formatter.csv_.CSVFormatter' + }, + 'process': { + 'RiverRunner': 'pygeoapi.process.river_runner.RiverRunnerProcessor', + 'HelloWorld': 'pygeoapi.process.hello_world.HelloWorldProcessor' + }, + 'process_manager': { + 'Dummy': 'pygeoapi.process.manager.dummy.DummyManager', + 'TinyDB': 'pygeoapi.process.manager.tinydb_.TinyDBManager' + } +} + + +def load_plugin(plugin_type, plugin_def): + """ + loads plugin by name + + :param plugin_type: type of plugin (provider, formatter) + :param plugin_def: plugin definition + + :returns: plugin object + """ + + name = plugin_def['name'] + + if plugin_type not in PLUGINS.keys(): + msg = 'Plugin type {} not found'.format(plugin_type) + LOGGER.exception(msg) + raise InvalidPluginError(msg) + + plugin_list = PLUGINS[plugin_type] + + LOGGER.debug('Plugins: {}'.format(plugin_list)) + + if '.' not in name and name not in plugin_list.keys(): + msg = 'Plugin {} not found'.format(name) + LOGGER.exception(msg) + raise InvalidPluginError(msg) + + if '.' in name: # dotted path + packagename, classname = name.rsplit('.', 1) + else: # core formatter + packagename, classname = plugin_list[name].rsplit('.', 1) + + LOGGER.debug('package name: {}'.format(packagename)) + LOGGER.debug('class name: {}'.format(classname)) + + module = importlib.import_module(packagename) + class_ = getattr(module, classname) + plugin = class_(plugin_def) + + return plugin + + +class InvalidPluginError(Exception): + """Invalid plugin""" + pass diff --git a/pygeoapi/river_runner.py b/pygeoapi/river_runner.py new file mode 100644 index 0000000..9c4a175 --- /dev/null +++ b/pygeoapi/river_runner.py @@ -0,0 +1,200 @@ +# ================================================================= +# +# Authors: Benjamin Webb +# +# Copyright (c) 2021 Benjamin Webb +# +# 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 os +import logging + +from pygeoapi.util import yaml_load +from pygeoapi.plugin import load_plugin +from pygeoapi.process.base import BaseProcessor, ProcessorExecuteError + + +LOGGER = logging.getLogger(__name__) +CONFIG_ = '' + +with open(os.getenv('PYGEOAPI_CONFIG'), encoding='utf8') as fh: + CONFIG_ = yaml_load(fh) + +PROVIDER_DEF = CONFIG_['resources']['merit']['providers'][0] +P = 'properties' +#: Process metadata and description +PROCESS_METADATA = { + 'version': '0.1.0', + 'id': 'river-runner', + 'title': { + 'en': 'River Runner' + }, + 'description': { + 'en': 'A simple process that takes a lat/long as input, and returns ' + 'it back as output. Intended to demonstrate a simple ' + 'process with a single literal input.' + }, + 'keywords': ['river runner', 'rivers'], + 'links': [{ + 'type': 'text/html', + 'rel': 'canonical', + 'title': 'information', + 'href': 'https://example.org/process', + 'hreflang': 'en-US' + }], + 'inputs': { + 'bbox': { + 'title': 'Boundary Box', + 'description': 'A set of four coordinates', + 'schema': { + 'type': 'object', + }, + 'minOccurs': 0, + 'maxOccurs': 1, + 'metadata': None, # TODO how to use? + 'keywords': ['coordinates', 'geography'] + }, + 'lat': { + 'title': 'Latitude', + 'description': 'Latitude of a point', + 'schema': { + 'type': 'number', + }, + 'minOccurs': 0, + 'maxOccurs': 1, + 'metadata': None, # TODO how to use? + 'keywords': ['coordinates', 'longitude'] + }, + 'long': { + 'title': 'Longitude', + 'description': 'Longitude of a point', + 'schema': { + 'type': 'number', + }, + 'minOccurs': 0, + 'maxOccurs': 1, + 'metadata': None, # TODO how to use? + 'keywords': ['coordinates', 'latitude'] + } + }, + 'outputs': { + 'echo': { + 'title': 'Feature Collection', + 'description': 'A geoJSON Feature Collection of River Runner', + 'schema': { + 'type': 'Object', + 'contentMediaType': 'application/json' + } + } + }, + 'example': { + 'inputs': { + 'bbox': [-86.2, 39.7, -86.15, 39.75] + } + } +} + + +class RiverRunnerProcessor(BaseProcessor): + """River Runner Processor example""" + + def __init__(self, processor_def): + """ + Initialize object + + :param processor_def: provider definition + + :returns: pygeoapi.process.river_runner.RiverRunnerProcessor + """ + self.p = load_plugin('provider', PROVIDER_DEF) + super().__init__(processor_def, PROCESS_METADATA) + + def execute(self, data): + mimetype = 'application/json' + if len(data.get('bbox', [])) != 4 and \ + not data.get('lat', '') and \ + not data.get('long', ''): + raise ProcessorExecuteError(f'Invalid input: { {{data.items()}} }') + + if data.get('bbox', []): + bbox = data['bbox'] + else: + bbox = self._expand_bbox((data['long'], data['lat'])*2) + + value = self.p.query(bbox=bbox) + i = 1 + while len(value['features']) < 1: + LOGGER.debug(f'No features in bbox {bbox}, expanding') + bbox = self._expand_bbox(bbox, e=0.125*i) + value = self.p.query(bbox=bbox) + i = i + 1 + + LOGGER.debug('fetching downstream features') + mh = self._compare(value, 'hydroseq', min) + out, trim = [], [] + for i in (mh[P]['levelpathi'], + *mh[P]['down_levelpaths'].split(',')): + try: + i = int(float(i)) + except ValueError: + LOGGER.error(f'No Downstem Rivers found {i}') + continue + + down = self.p.query( + properties=[('levelpathi', i), ], limit=2000 + ) + + out.extend(down['features']) + m = self._compare(down, 'hydroseq', min) + trim.append((m[P]['dnlevelpat'], m[P]['dnhydroseq'])) + + LOGGER.debug('keeping only mainstem flowpath') + trim.append((mh[P]['levelpathi'], mh[P]['hydroseq'])) + outm = [] + for seg in out: + for i in trim: + if seg[P]['levelpathi'] == i[0] and \ + seg[P]['hydroseq'] <= i[1]: + outm.append(seg) + + value['features'] = outm + outputs = { + 'id': 'echo', + 'value': value + } + return mimetype, outputs + + def _compare(self, fc, prop, dir): + val = fc['features'][0] + for f in fc['features']: + if dir(f[P][prop], val[P][prop]) != val[P][prop]: + val = f + return val + + def _expand_bbox(self, bbox, e=0.125): + return [b + e if i < 2 else b - e + for (i, b) in enumerate(bbox)] + + def __repr__(self): + return ' {}'.format(self.name)