Skip to content

Commit

Permalink
Merge pull request #20 from internetofwater/split-catchment
Browse files Browse the repository at this point in the history
Update Deployment Configuration
  • Loading branch information
webb-ben authored Sep 27, 2023
2 parents 47d66b8 + 0308ef6 commit 1d90e16
Show file tree
Hide file tree
Showing 9 changed files with 458 additions and 41 deletions.
86 changes: 79 additions & 7 deletions nldi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
'X-Powered-By': f'nldi {__version__}'
}
FORMAT_TYPES.move_to_end(F_JSON, last=False)
SPLIT_CATCHMENT_THRESHOLD = 200


def pre_process(func):
Expand Down Expand Up @@ -542,7 +543,7 @@ def get_basin(self, request: Union[APIRequest, Any],
if source_name != 'comid':
source = self.crawler_source.get(source_name)
plugin = self.load_plugin('FeatureLookup', source=source)
feature = next(plugin.get(identifier))
feature = plugin.get(identifier)
start_comid = int(feature['properties']['comid'])
isPoint = feature['geometry']['type'] == 'Point'
else:
Expand Down Expand Up @@ -572,12 +573,36 @@ def get_basin(self, request: Union[APIRequest, Any],
splitCatchment = _

if isPoint and splitCatchment:
LOGGER.debug('Split Catchment')
LOGGER.debug('Performing Split Catchment')
point = self.func.get_point(identifier, source_name)

if point is None:
distance = self.func.get_distance(identifier, source_name)
LOGGER.debug(distance)

if distance <= SPLIT_CATCHMENT_THRESHOLD:
point = self.func.get_closest(identifier, source_name)
else:
[lon, lat] = feature['geometry']['coordinates']
wkt_geom = f'POINT({lon} {lat})'
response = self.pygeoapi_lookup.get_hydrolocation(wkt_geom)
point = response['features'][0]['geometry']['coordinates']

if point is None:
msg = 'Unable to retrieve point on flowline for catchment splitting.' # noqa
return self.get_exception(
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
'NoApplicableCode', msg)

LOGGER.debug(f'Returning with simplified geometry: {simplified}')
content = self.func.get_basin(start_comid, simplified)
[lon, lat] = point
wkt_geom = f'POINT({lon} {lat})'
features = self.pygeoapi_lookup.get_split_catchment(wkt_geom)
else:
LOGGER.debug(f'Returning with simplified geometry: {simplified}')
features = self.func.get_basin(start_comid, simplified)

return headers, HTTPStatus.OK, to_json(content, self.pretty_print)
content = stream_j2_template('FeatureCollection.j2', features)
return headers, HTTPStatus.OK, content

@pre_process
def get_navigation_info(self, request: Union[APIRequest, Any],
Expand Down Expand Up @@ -734,8 +759,55 @@ def get_flowlines(self, request: Union[APIRequest, Any],
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
'NoApplicableCode', msg)

nav_results = self.func.get_navigation(nav_mode, start_comid, distance)
features = self.flowline_lookup.lookup_navigation(nav_results)
LOGGER.debug(f'Doing navigation {nav_mode} for {distance}')
nav = self.func.get_navigation(nav_mode, start_comid, distance)

trim_start = False
try:
trim_start = str(request.params['trimStart']).lower() == 'true'
except (KeyError, TypeError):
if request.params.get('trimStart'):
msg = 'Request parameter \'trimStart\' must be a boolean.'
return self.get_exception(
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
'NoApplicableCode', msg)

LOGGER.debug(trim_start)
if trim_start is True:
try:
trim_tolerance = request.params.get('trimTolerance', 0)
trim_tolerance = float(trim_tolerance)
except ValueError:
msg = 'Request parameter \'trimTolerance\' must be a number.'
return self.get_exception(
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
'NoApplicableCode', msg)

LOGGER.debug(f'Trimming flowline with tolerance: {trim_tolerance}')
try:
measure = feature['properties']['measure']
if not measure:
measure = \
self.func.estimate_measure(identifier, source_name)
measure = float(measure)
LOGGER.debug(f'measure {measure}')
except KeyError:
msg = 'Required field \'measure\' is not present.'
return self.get_exception(
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
'NoApplicableCode', msg)
except ValueError:
msg = 'Required field \'measure\' must be a number.'
return self.get_exception(
HTTPStatus.INTERNAL_SERVER_ERROR, headers, request.format,
'NoApplicableCode', msg)

trim_nav = self.func.trim_navigation(nav_mode, start_comid,
trim_tolerance, measure)
features = self.flowline_lookup.trim_navigation(nav, trim_nav)
else:
features = self.flowline_lookup.lookup_navigation(nav)

content = stream_j2_template('FeatureCollection.j2', features)

return headers, HTTPStatus.OK, content
Expand Down
123 changes: 107 additions & 16 deletions nldi/functions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@
from sqlalchemy.orm import Session

from nldi.functions.basin import get_basin
from nldi.functions.navigate import get_navigation
from nldi.functions.navigate import get_navigation, trim_navigation
from nldi.functions.lookup import (
get_point_on_flowline, estimate_measure,
get_closest_point_on_flowline,
get_distance_from_flowline)
from nldi.lookup import _ENGINE_STORE

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -80,17 +84,30 @@ def get_basin(self, comid: int, simplified: bool):
msg = f'No such item: {self.id_field}={comid}'
raise FunctionItemNotFoundError(msg)

return {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'geometry': json.loads(result.the_geom),
'properties': {}
}
]
yield {
'type': 'Feature',
'geometry': json.loads(result.the_geom),
'properties': {}
}

def trim_navigation(self, nav_mode: str, comid: int,
trim_tolerance: float, measure: float):
"""
Trim navigation
:param nav: navigation query
:param nav_mode: navigation mode
:param comid: start comid
:param trim_tolerance: trim tolerance
:param measure: calculated measure
:returns: string of trimmed navigation query
"""

trim = trim_navigation(nav_mode, comid, trim_tolerance, measure)
LOGGER.debug(trim.compile(self._engine))
return trim

def get_navigation(self, nav_mode: str, comid: int, distance: float):
"""
Perform navigation
Expand All @@ -99,7 +116,7 @@ def get_navigation(self, nav_mode: str, comid: int, distance: float):
:param comid: start comid
:param distance: distance to navigate
:returns: iterator of navigated comid
:returns: string of navigation query
"""
try:
distance = float(distance)
Expand All @@ -110,17 +127,91 @@ def get_navigation(self, nav_mode: str, comid: int, distance: float):

nav = get_navigation(nav_mode, comid, distance)
LOGGER.debug(nav.compile(self._engine))
return nav

def get_point(self, feature_id: str, feature_source: str):
"""
Perform flowline lookup
:param feature_id: Feature indentifier
:param feature_source: Feature source
:returns: Point on flowline
"""
point = get_point_on_flowline(feature_id, feature_source)
LOGGER.debug(point.compile(self._engine))

with Session(self._engine) as session:
# Retrieve data from database as feature
result = session.execute(point).fetchone()

if result is None or None in result:
LOGGER.warning('Not on flowline')
else:
return result

def estimate_measure(self, feature_id: str, feature_source: str):
"""
Perform flowline approximation
:param feature_id: Feature indentifier
:param feature_source: Feature source
:returns: Point on flowline
"""
measure = estimate_measure(feature_id, feature_source)
LOGGER.debug(measure.compile(self._engine))

with Session(self._engine) as session:
# Retrieve data from database as feature
result = session.execute(nav)
result = session.execute(measure).scalar()

if result is None:
msg = f'No such item: {self.id_field}={comid}'
raise FunctionItemNotFoundError(msg)
LOGGER.warning('Not on flowline')
else:
return result

def get_closest(self, feature_id: str, feature_source: str):
"""
Perform flowline approximation
:param feature_id: Feature indentifier
:param feature_source: Feature source
:returns: Point on flowline
"""
point = get_closest_point_on_flowline(feature_id, feature_source)
LOGGER.debug(point.compile(self._engine))

with Session(self._engine) as session:
# Retrieve data from database as feature
result = session.execute(point).fetchone()

for item in result.fetchall():
yield item.comid
if None in result:
LOGGER.warning('Not on flowline')
else:
return result

def get_distance(self, feature_id: str, feature_source: str):
"""
Perform flowline distance
:param feature_id: Feature indentifier
:param feature_source: Feature source
:returns: Distance from nearest flowline
"""
point = get_distance_from_flowline(feature_id, feature_source)
LOGGER.debug(point.compile(self._engine))

with Session(self._engine) as session:
# Retrieve data from database as feature
result = session.execute(point).scalar()

if result is None:
LOGGER.warning('Not on flowline')
else:
return result

def _store_db_parameters(self, parameters):
self.db_user = parameters.get('user')
Expand Down
Loading

0 comments on commit 1d90e16

Please sign in to comment.