diff --git a/climatic_zones/serializers.py b/climatic_zones/serializers.py index 6b43a3df90dda4809ef1ad94dfc27832b5d70da9..7c1cef64342d0c392eef831c6d7606a15dd86f37 100644 --- a/climatic_zones/serializers.py +++ b/climatic_zones/serializers.py @@ -20,6 +20,7 @@ def get_provenance(obj): Tel. + 44 (0)1248 374518 (direct)\n Tel. + 44 (0)1248 374500 (reception)\n E-mail: katshar@ceh.ac.uk\n\nThe climate zones are as follows:\n + 0 Residual class (water?) 1 Warm Temperate Moist\n 2 Warm Temperate Dry\n 3 Cool Temperate Moist\n @@ -59,18 +60,29 @@ class AggSerializer(BaseSerializer): except TypeError: vlength = 1 # TODO: check units below + for k, v in obj.items(): + print(k, v) if vlength > 1: - properties = OrderedDict([ - ('agg_function', agg_function), - ('many', True), - (agg_function, OrderedDict([ - (d, v) for d, v in zip(obj['direction'], val) - ])), - ('units', '?'), - ('radius', obj['radius']), - ]) if obj['direction'] is None: - properties.pop('direction') + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(range(vlength), val) + ])), + ('units', '?'), + ('radius', obj['radius']), + ]) + else: + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(obj['direction'], val) + ])), + ('units', '?'), + ('radius', obj['radius']), + ]) else: properties = OrderedDict([ ('agg_function', agg_function), diff --git a/climatic_zones/views.py b/climatic_zones/views.py index db68ef26e5753240412b35db65761f7b6ef79e5e..d0ec24a8ccdf99ad6b0beb68c560869c33228eb9 100644 --- a/climatic_zones/views.py +++ b/climatic_zones/views.py @@ -7,9 +7,11 @@ from rest_framework.response import Response from toar_location_services.settings import DEBUG from .climatic_zones_file_extraction import read_proxydata from .serializers import AggSerializer -from utils.views_commons import get_query_params, get_agg_function +# from utils.views_commons import get_query_params, get_agg_function +from utils.views_commons import CommonViews from utils.geoutils import Directions from utils.extraction_tools import extract_value, extract_value_stats +from utils.statistics import most_common_value, relative_frequency FILENAME = "climate_ascii.txt" @@ -19,13 +21,19 @@ if DEBUG: print("File %s successfully loaded" % FILENAME, datainfo) -class ClimateView(APIView): +class ClimateView(APIView, CommonViews): + + def __init__(self): + CommonViews.__init__(self) + self.keep_only_given_agg_allowed(dict(maxclass=most_common_value, + frequency=relative_frequency), + frequency=dict(bins=13)) def _extract(self, lat, lon, radius, agg, direction, by_direction): """perform actual extraction of desired quantity""" print('**by_direction:', by_direction, '** radius, agg = ', radius, agg) if agg is not None and radius is not None and radius > 0.: - agg_function = get_agg_function(agg) + agg_function = self.get_agg_function(agg) min_angle = None max_angle = None if by_direction: @@ -36,7 +44,7 @@ class ClimateView(APIView): # ToDo: once we serve the data via rasdaman the calls with directions should use # a polygon query for efficiency reasons. result[i] = extract_value_stats(lonvec, latvec, data, lon, lat, default_value=-999., - out_of_bounds_value=0., min_valid=0., max_valid=2.e6, + out_of_bounds_value=0., min_valid=0, max_valid=12, radius=radius, min_angle=min_angle, max_angle=max_angle, agg=agg_function) else: @@ -45,13 +53,13 @@ class ClimateView(APIView): # ToDo: once we serve the data via rasdaman the calls with directions should use # a polygon query for efficiency reasons. result = extract_value_stats(lonvec, latvec, data, lon, lat, default_value=-999., - out_of_bounds_value=0., min_valid=0., max_valid=2.e6, + out_of_bounds_value=0., min_valid=0, max_valid=12, radius=radius, min_angle=min_angle, max_angle=max_angle, agg=agg_function) else: agg = 'value' result = extract_value(lonvec, latvec, data, lon, lat, default_value=-999., - out_of_bounds_value=0., min_valid=0., max_valid=2.e6) + out_of_bounds_value=0., min_valid=0, max_valid=12) # return data, also return agg and direction as they may have been overwritten return result, agg, direction @@ -77,7 +85,7 @@ class ClimateView(APIView): by_direction: if True, data are returned as vector with one value aggregated over each of 16 wind directions. """ - lat, lon, radius, agg, direction, by_direction = get_query_params(request.query_params, + lat, lon, radius, agg, direction, by_direction = self.get_query_params(request.query_params, ['lat', 'lon', 'radius', 'agg', 'direction', 'by_direction']) result, agg, direction = self._extract(lat, lon, radius, agg, direction, by_direction) diff --git a/major-roads/views.py b/major-roads/views.py index b4433f6b6142ffbab5dee2b8ea26ab945aacace6..bc73eb500c7f1772747c1485b46582f21256c5e5 100644 --- a/major-roads/views.py +++ b/major-roads/views.py @@ -4,13 +4,15 @@ from collections import OrderedDict from rest_framework.views import APIView from rest_framework.response import Response -from utils.views_commons import get_query_params +from utils.views_commons import CommonViews from .overpass_api import OverpassRoadAPI from .serializers import NearestSerializer, NearestDirectionSerializer, NearestByDirectionSerializer -class MajorRoadsView(APIView): +class MajorRoadsView(APIView, CommonViews): + def __init__(self): + CommonViews.__init__(self) def get(self, request, format=None): """process GET requests for major-roads app @@ -34,7 +36,7 @@ class MajorRoadsView(APIView): Default is motorway,trunk,primary,secondary. For additional options see https://wiki.openstreetmap.org/wiki/Key:highway """ - lat, lon, radius, direction, by_direction, highway_types = get_query_params( + lat, lon, radius, direction, by_direction, highway_types = self.get_query_params( request.query_params, ['lat', 'lon', 'radius', 'direction', 'by_direction', 'highway_types'], add_agg=False diff --git a/population-density/serializers.py b/population-density/serializers.py index a1f998d06ffe2e09a863bcbb97986f33b3886301..90593515cd72e85fc2ef43a8acd4cd0ef32101c8 100644 --- a/population-density/serializers.py +++ b/population-density/serializers.py @@ -28,6 +28,7 @@ Center (SEDAC), Columbia University. """), # serializer classes + class AggSerializer(BaseSerializer): """ see http://www.django-rest-framework.org/api-guide/serializers/#baseserializer """ @@ -45,23 +46,32 @@ class AggSerializer(BaseSerializer): except TypeError: vlength = 1 if vlength > 1: - properties = OrderedDict([ - ('agg_function', agg_function), - ('many', True), - (agg_function, OrderedDict([ - (d, v) for d, v in zip(obj['direction'], val) - ])), - ('units', 'km-2'), - ('radius', obj['radius']), - ]) if obj['direction'] is None: - properties.pop('direction') + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(range(vlength), val) + ])), + ('units', ''), + ('radius', obj['radius']), + ]) + else: + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(obj['direction'], val) + ])), + ('units', ''), + ('radius', obj['radius']), + ]) else: properties = OrderedDict([ ('agg_function', agg_function), ('many', False), (agg_function, val), - ('units', 'km-2'), + ('units', ''), ('radius', obj['radius']), ('direction', obj['direction']), ]) diff --git a/population-density/views.py b/population-density/views.py index 01c2097ed19bf879df53ccd96b391934ee2a1cd7..0550a836d3df2a6224a674b844a42735161393f4 100644 --- a/population-density/views.py +++ b/population-density/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from toar_location_services.settings import DEBUG from .population_file_extraction import read_proxydata from .serializers import AggSerializer -from utils.views_commons import get_query_params, get_agg_function +from utils.views_commons import CommonViews from utils.geoutils import Directions from utils.extraction_tools import extract_value, extract_value_stats @@ -20,13 +20,17 @@ lonvec, latvec, data, datainfo = read_proxydata(FILENAME) if DEBUG: print("File %s successfully loaded" % (FILENAME), datainfo) -class PopulationView(APIView): + +class PopulationView(APIView, CommonViews): + + def __init__(self): + CommonViews.__init__(self) def _extract(self, lat, lon, radius, agg, direction, by_direction): """perform actual extraction of desired quantity""" print('**by_direction:', by_direction, '** radius, agg = ', radius, agg) if agg is not None and radius is not None and radius > 0.: - agg_function = get_agg_function(agg) + agg_function = self.get_agg_function(agg) min_angle = None max_angle = None if by_direction: @@ -56,7 +60,6 @@ class PopulationView(APIView): # return data, also return agg and direction as they may have been overwritten return result, agg, direction - def get(self, request, format=None): """process GET requests for population-density app @@ -79,7 +82,7 @@ class PopulationView(APIView): by_direction: if True, data are returned as vector with one value aggregated over each of 16 wind directions. """ - lat, lon, radius, agg, direction, by_direction = get_query_params(request.query_params, + lat, lon, radius, agg, direction, by_direction = self.get_query_params(request.query_params, ['lat', 'lon', 'radius', 'agg', 'direction', 'by_direction']) result, agg, direction = self._extract(lat, lon, radius, agg, direction, by_direction) diff --git a/rice_production/serializers.py b/rice_production/serializers.py index e42616294c8b10833b18703afaf65815fc230a8c..ceac3b4cdaae1523197762d7e98ef460b6fd6cea 100644 --- a/rice_production/serializers.py +++ b/rice_production/serializers.py @@ -46,17 +46,26 @@ class AggSerializer(BaseSerializer): vlength = 1 # TODO: check units below if vlength > 1: - properties = OrderedDict([ - ('agg_function', agg_function), - ('many', True), - (agg_function, OrderedDict([ - (d, v) for d, v in zip(obj['direction'], val) - ])), - ('units', 'kt'), - ('radius', obj['radius']), - ]) if obj['direction'] is None: - properties.pop('direction') + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(range(vlength), val) + ])), + ('units', 'kt'), + ('radius', obj['radius']), + ]) + else: + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(obj['direction'], val) + ])), + ('units', 'kt'), + ('radius', obj['radius']), + ]) else: properties = OrderedDict([ ('agg_function', agg_function), diff --git a/rice_production/views.py b/rice_production/views.py index ba4686d9963b82448d64ab8cf789f2cf56f3fe36..9a2ade4400f3a90be39b4719c0d9ec77380ba8f8 100644 --- a/rice_production/views.py +++ b/rice_production/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from toar_location_services.settings import DEBUG from .rice_file_extraction import read_proxydata from .serializers import AggSerializer -from utils.views_commons import get_query_params, get_agg_function +from utils.views_commons import CommonViews from utils.geoutils import Directions from utils.extraction_tools import extract_value, extract_value_stats @@ -19,13 +19,16 @@ if DEBUG: print("File %s successfully loaded" % FILENAME, datainfo) -class RiceView(APIView): +class RiceView(APIView, CommonViews): + + def __init__(self): + CommonViews.__init__(self) def _extract(self, lat, lon, radius, agg, direction, by_direction): """perform actual extraction of desired quantity""" print('**by_direction:', by_direction, '** radius, agg = ', radius, agg) if agg is not None and radius is not None and radius > 0.: - agg_function = get_agg_function(agg) + agg_function = self.get_agg_function(agg) min_angle = None max_angle = None if by_direction: @@ -77,7 +80,7 @@ class RiceView(APIView): by_direction: if True, data are returned as vector with one value aggregated over each of 16 wind directions. """ - lat, lon, radius, agg, direction, by_direction = get_query_params(request.query_params, + lat, lon, radius, agg, direction, by_direction = self.get_query_params(request.query_params, ['lat', 'lon', 'radius', 'agg', 'direction', 'by_direction']) result, agg, direction = self._extract(lat, lon, radius, agg, direction, by_direction) diff --git a/stable_night_lights/serializers.py b/stable_night_lights/serializers.py index 54d16e819d2dd827bced2f933bbd492441b9b9af..89de8507ce6621d1ae497ce0b20caa80c9d9bd26 100644 --- a/stable_night_lights/serializers.py +++ b/stable_night_lights/serializers.py @@ -42,23 +42,32 @@ class AggSerializer(BaseSerializer): vlength = 1 # TODO: check units if vlength > 1: - properties = OrderedDict([ - ('agg_function', agg_function), - ('many', True), - (agg_function, OrderedDict([ - (d, v) for d, v in zip(obj['direction'], val) - ])), - ('units', 'm'), - ('radius', obj['radius']), - ]) if obj['direction'] is None: - properties.pop('direction') + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(range(vlength), val) + ])), + ('units', ''), + ('radius', obj['radius']), + ]) + else: + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(obj['direction'], val) + ])), + ('units', ''), + ('radius', obj['radius']), + ]) else: properties = OrderedDict([ ('agg_function', agg_function), ('many', False), (agg_function, val), - ('units', 'm'), + ('units', ''), ('radius', obj['radius']), ('direction', obj['direction']), ]) diff --git a/stable_night_lights/views.py b/stable_night_lights/views.py index 0016499cb04a6a28a9c3f45bd468a38c9dd3619b..56a917f7de4c66b5e96839b2e8043db5324ef0aa 100644 --- a/stable_night_lights/views.py +++ b/stable_night_lights/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from toar_location_services.settings import DEBUG from .nightlights_file_extraction import read_proxydata from .serializers import AggSerializer -from utils.views_commons import get_query_params, get_agg_function +from utils.views_commons import CommonViews from utils.geoutils import Directions from utils.extraction_tools import extract_value, extract_value_stats @@ -19,13 +19,16 @@ if DEBUG: print("File %s successfully loaded" % FILENAME, datainfo) -class NightTimeLightsView(APIView): +class NightTimeLightsView(APIView, CommonViews): + + def __init__(self): + CommonViews.__init__(self) def _extract(self, lat, lon, radius, agg, direction, by_direction): """perform actual extraction of desired quantity""" print('**by_direction:', by_direction, '** radius, agg = ', radius, agg) if agg is not None and radius is not None and radius > 0.: - agg_function = get_agg_function(agg) + agg_function = self.get_agg_function(agg) min_angle = None max_angle = None if by_direction: @@ -77,7 +80,7 @@ class NightTimeLightsView(APIView): by_direction: if True, data are returned as vector with one value aggregated over each of 16 wind directions. """ - lat, lon, radius, agg, direction, by_direction = get_query_params(request.query_params, + lat, lon, radius, agg, direction, by_direction = self.get_query_params(request.query_params, ['lat', 'lon', 'radius', 'agg', 'direction', 'by_direction']) result, agg, direction = self._extract(lat, lon, radius, agg, direction, by_direction) diff --git a/toar_location_services/settings.py b/toar_location_services/settings.py index 13b562f56f1daff6c2024b1cb98fa85e97fddbde..7b99bb19d36645086cabce641438ca3d3b4a19fe 100644 --- a/toar_location_services/settings.py +++ b/toar_location_services/settings.py @@ -145,7 +145,7 @@ DEFAULT_LON = 4.9003 # DEBUG settings # These only take effect if DEBUG==True USE_LOCAL_OVERPASS_DATA = False -USE_DUMMY_POPULATION_DATA = True +USE_DUMMY_POPULATION_DATA = False USE_DUMMY_TOPOGRAPHY_TANDEM_DATA = False USE_DUMMY_STABLE_NIGHT_LIGHTS_DATA = False USE_DUMMY_WHEAT_DATA = False diff --git a/topography-tandem-x/serializers.py b/topography-tandem-x/serializers.py index 5886e6d7017b62698407823488c167154497f9b4..8adab2bba2086475b2686c74869a78396d188115 100644 --- a/topography-tandem-x/serializers.py +++ b/topography-tandem-x/serializers.py @@ -46,17 +46,26 @@ class AggSerializer(BaseSerializer): except TypeError: vlength = 1 if vlength > 1: - properties = OrderedDict([ - ('agg_function', agg_function), - ('many', True), - (agg_function, OrderedDict([ - (d, v) for d, v in zip(obj['direction'], val) - ])), - ('units', 'm'), - ('radius', obj['radius']), - ]) if obj['direction'] is None: - properties.pop('direction') + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(range(vlength), val) + ])), + ('units', 'm'), + ('radius', obj['radius']), + ]) + else: + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(obj['direction'], val) + ])), + ('units', 'm'), + ('radius', obj['radius']), + ]) else: properties = OrderedDict([ ('agg_function', agg_function), diff --git a/topography-tandem-x/views.py b/topography-tandem-x/views.py index a064804cc485e1e9d7c3bc1a97118c90c6b9aa99..7c308e0e3618e2d688569a40ce8f7de73398f191 100644 --- a/topography-tandem-x/views.py +++ b/topography-tandem-x/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from toar_location_services.settings import DEBUG from .topography_file_extraction import read_proxydata from .serializers import AggSerializer -from utils.views_commons import get_query_params, get_agg_function +from utils.views_commons import CommonViews from utils.geoutils import Directions from utils.extraction_tools import extract_value, extract_value_stats @@ -19,13 +19,17 @@ if DEBUG: print("File %s successfully loaded" % (FILENAME), datainfo) -class TopographyView(APIView): +class TopographyView(APIView, CommonViews): + + def __init__(self): + CommonViews.__init__(self) + self.remove_agg_allowed('sum') def _extract(self, lat, lon, radius, agg, direction, by_direction): """perform actual extraction of desired quantity""" print('**by_direction:', by_direction, '** radius, agg = ', radius, agg) if agg is not None and radius is not None and radius > 0.: - agg_function = get_agg_function(agg) + agg_function = self.get_agg_function(agg) min_angle = None max_angle = None if by_direction: @@ -36,7 +40,7 @@ class TopographyView(APIView): # ToDo: once we serve the data via rasdaman the calls with directions should use # a polygon query for efficiency reasons. result[i] = extract_value_stats(lonvec, latvec, data, lon, lat, default_value=-999., - out_of_bounds_value=0., min_valid=0., max_valid=2.e6, + out_of_bounds_value=0., min_valid=-500., max_valid=9000, radius=radius, min_angle=min_angle, max_angle=max_angle, agg=agg_function) else: @@ -45,13 +49,13 @@ class TopographyView(APIView): # ToDo: once we serve the data via rasdaman the calls with directions should use # a polygon query for efficiency reasons. result = extract_value_stats(lonvec, latvec, data, lon, lat, default_value=-999., - out_of_bounds_value=0., min_valid=0., max_valid=2.e6, + out_of_bounds_value=-666, min_valid=-500, max_valid=9000, radius=radius, min_angle=min_angle, max_angle=max_angle, agg=agg_function) else: agg = 'value' result = extract_value(lonvec, latvec, data, lon, lat, default_value=-999., - out_of_bounds_value=0., min_valid=0., max_valid=2.e6) + out_of_bounds_value=0., min_valid=-500, max_valid=9000) # return data, also return agg and direction as they may have been overwritten return result, agg, direction @@ -78,7 +82,7 @@ class TopographyView(APIView): by_direction: if True, data are returned as vector with one value aggregated over each of 16 wind directions. """ - lat, lon, radius, agg, direction, by_direction = get_query_params(request.query_params, + lat, lon, radius, agg, direction, by_direction = self.get_query_params(request.query_params, ['lat', 'lon', 'radius', 'agg', 'direction', 'by_direction']) result, agg, direction = self._extract(lat, lon, radius, agg, direction, by_direction) diff --git a/utils/statistics.py b/utils/statistics.py new file mode 100644 index 0000000000000000000000000000000000000000..8b52c4014ed7e08ff28bdddea5b835d1e5eb9ed7 --- /dev/null +++ b/utils/statistics.py @@ -0,0 +1,15 @@ +import numpy as np + + +def most_common_value(values): + """Get most common value from array""" + return np.bincount(values.astype(int)).argmax() + + +def relative_frequency(values, decimals=6, bins=0): + """Get the relative frequency of each possible class""" + if bins > 0: + f = np.round(np.bincount(values.astype(int), minlength=int(bins)) / len(values), decimals) + else: + f = np.round(np.bincount(values.astype(int)) / len(values), decimals) + return f diff --git a/utils/views_commons.py b/utils/views_commons.py index 08bf33f43ddc5ed0b10485c020eb837f51f64940..d470304880ee94a9e2413da51ea4633bace1dc64 100644 --- a/utils/views_commons.py +++ b/utils/views_commons.py @@ -23,225 +23,262 @@ import numpy as np from functools import partial import toar_location_services.settings as settings from utils.geoutils import Directions - - -# obtain default key settings from settings.py -DEFAULT_KEYS = [x for x in dir(settings) if x.startswith('DEFAULT_')] - -# define allowed statistics in agg -# special handling for percentiles -STATS = {'mean': np.mean, 'min': np.amin, 'max': np.amax, 'median': np.median} -AGG_ALLOWED = list(STATS.keys()) + ['NN%-ile', 'NN-percentile'] - - -def get_agg_function(function_name): - """given a string with a function name, return the respective one-argument function pointer - Validity of function_name must have been checked before (see get_agg_param) - NN-percentile is possible besides any function defined in STATS""" - if 'percentile' in function_name: - pval = float(function_name[:2]) - return partial(np.percentile, q=[pval]) # Note: numpy q is range 0..100 - else: - return STATS[function_name] - - -def get_latitude_param(params): - """This is a mandatory parameter""" - val = params.get('lat') - # try alternative(s) - if val is None: - val = params.get('latitude') - # if still not found, raise error - if val is None: - if settings.DEBUG: - val = settings.DEFAULT_LAT +import inspect + + +class CommonViews: + + def __init__(self): + + # obtain default key settings from settings.py + self.DEFAULT_KEYS = [x for x in dir(settings) if x.startswith('DEFAULT_')] + + # define allowed statistics in agg + # special handling for percentiles + self.STATS = {'mean': np.mean, 'min': np.amin, 'max': np.amax, 'median': np.median, 'percentile': np.percentile, + 'sum': np.sum} + self.AGG_ALLOWED = list(self.STATS.keys()) + self.update_agg_allowed() + + def update_agg_allowed(self): + self.AGG_ALLOWED = list(self.STATS.keys()) + + def load_kwargs(self, args_dict, func): + # check kwargs for usage + args = {} + for k, v in args_dict.items(): + if inspect.getargspec(func): + args[k] = v + + def add_agg_allowed(self, name, func, update=True, **kwargs): + # add only if not already present + if str(name) not in self.STATS.keys(): + if name not in kwargs.keys(): + self.STATS[str(name)] = func + else: + self.STATS[str(name)] = partial(func, **kwargs[name]) + if update: + self.update_agg_allowed() + + def remove_agg_allowed(self, name, update=True): + if str(name) in self.STATS.keys(): + self.STATS.pop(str(name)) + if update: + self.update_agg_allowed() + + def keep_only_given_agg_allowed(self, agg_dict, **kwargs): + for name in list(self.STATS.keys()): + if name not in agg_dict.keys(): + self.remove_agg_allowed(name, update=False) + for name, func in agg_dict.items(): + self.add_agg_allowed(name, func, update=False, **kwargs) + self.update_agg_allowed() + + def get_agg_param(self, params): + """extract desired aggregation statistics + Allowed values are those defined in STATS and any NN%-ile or NN-percentile""" + val = params.get('agg') + if val is not None: + is_perc = ('%-ile' in val or 'percentile' in val) and 'percentile' in self.STATS.keys() + if not (val in self.STATS.keys() or is_perc): + raise ValueError('query argument for agg {} not allowed.\nAllowed values: {}' \ + .format(val, self.AGG_ALLOWED)) + # check validity of percentile value and normalize writing + if is_perc: + numval = int(val[:2]) # fails if not a valid int + val = '{:02d}-percentile'.format(numval) + return val + + def get_agg_function(self, function_name): + """given a string with a function name, return the respective one-argument function pointer + Validity of function_name must have been checked before (see get_agg_param) + NN-percentile is possible besides any function defined in STATS""" + if 'percentile' in function_name: + pval = float(function_name[:2]) + return partial(np.percentile, q=[pval]) # Note: numpy q is range 0..100 else: - raise KeyError('lat is a mandatory query argument') - # check validity of lat value - val = float(val) - if val < -90. or val > 90.: - raise ValueError('query argument for lat (%f) outside allowed range (-90..+90)' % (val)) - return val - - -def get_longitude_param(params): - """This is a mandatory parameter""" - val = params.get('lon') - # try alternative(s) - if val is None: - val = params.get('long') - if val is None: - val = params.get('longitude') - # if still not found, raise error - if val is None: - if settings.DEBUG: - val = settings.DEFAULT_LON - else: - raise KeyError('lon is a mandatory query argument') - # check validity of lon value and convert to range -180..+180 - val = float(val) - if val < -180. or val > 360.: - raise ValueError('query argument for lon (%f) outside allowed range (-180..+360)' % (val)) - if val > 180.: - val -= 360. - return val - - -def get_radius_param(params, radius_none_by_default=True): - """check if radius is given in params and return radius value or default - For standard gridpoint data services, the default radius is 0 if 'agg' is not - present in params, and DEFAULT_RADIUS otherwise. Some services which don't use 'agg' - always return the DEFAULT_RADIUS. - radius_none_by_default (True): set standard behaviour""" - val = params.get('radius') - if val is None: - if not radius_none_by_default: - val = settings.DEFAULT_RADIUS - # check valid range - else: + return self.STATS[function_name] + + @staticmethod + def get_latitude_param(params): + """This is a mandatory parameter""" + val = params.get('lat') + # try alternative(s) + if val is None: + val = params.get('latitude') + # if still not found, raise error + if val is None: + if settings.DEBUG: + val = settings.DEFAULT_LAT + else: + raise KeyError('lat is a mandatory query argument') + # check validity of lat value val = float(val) - if val < 0. or val > settings.MAX_ALLOWED_RADIUS: - raise ValueError('query argument for radius {:f} outside allowed range (0..{:.0f})'\ - .format(val, settings.MAX_ALLOWED_RADIUS)) - return val - - -def get_agg_param(params): - """extract desired aggregation statistics - Allowed values are those defined in STATS and any NN%-ile or NN-percentile""" - val = params.get('agg') - if val is not None: - is_perc = '%-ile' in val or 'percentile' in val - if not (val in STATS.keys() or is_perc): - raise ValueError('query argument for agg {} not allowed.\nAllowed values: {}'\ - .format(val, AGG_ALLOWED)) - # check validity of percentile value and normalize writing - if is_perc: - numval = int(val[:2]) # fails if not a valid int - val = '{:02d}-percentile'.format(numval) - return val - - -def get_direction_param(params): - """look for 'direction' argument and make sure a valid direction is given - We accept northbased azimuth angles, e.g. N, NW, SSE, etc. - This argument is always optional, therefore we don't look for a default - and None can be returned.""" - val = params.get('direction') - if val is not None: - # check validity of argument value - labels = Directions.LABELS - if not val.upper() in labels: - raise ValueError('query argument for direction {} not valid. Allowed values: {}'\ - .format(val, ', '.join([x for x in labels]))) - return val - - -def get_bydirection_param(params): - """look for boolean by_direction argument - defaults to False - Accepted values for True are true, 'true', 1, 'yes', 'y' - """ - val = params.get('by_direction') - if val is None: - val = False - if isinstance(val, str): - if val.lower() in ['true', 'yes', 'y']: - val = True - else: - try: - if val == 1: - val = True - except: + if val < -90. or val > 90.: + raise ValueError('query argument for lat (%f) outside allowed range (-90..+90)' % (val)) + return val + + @staticmethod + def get_longitude_param(params): + """This is a mandatory parameter""" + val = params.get('lon') + # try alternative(s) + if val is None: + val = params.get('long') + if val is None: + val = params.get('longitude') + # if still not found, raise error + if val is None: + if settings.DEBUG: + val = settings.DEFAULT_LON + else: + raise KeyError('lon is a mandatory query argument') + # check validity of lon value and convert to range -180..+180 + val = float(val) + if val < -180. or val > 360.: + raise ValueError('query argument for lon (%f) outside allowed range (-180..+360)' % (val)) + if val > 180.: + val -= 360. + return val + + @staticmethod + def get_radius_param(params, radius_none_by_default=True): + """check if radius is given in params and return radius value or default + For standard gridpoint data services, the default radius is 0 if 'agg' is not + present in params, and DEFAULT_RADIUS otherwise. Some services which don't use 'agg' + always return the DEFAULT_RADIUS. + radius_none_by_default (True): set standard behaviour""" + val = params.get('radius') + if val is None: + if not radius_none_by_default: + val = settings.DEFAULT_RADIUS + # check valid range + else: + val = float(val) + if val < 0. or val > settings.MAX_ALLOWED_RADIUS: + raise ValueError('query argument for radius {:f} outside allowed range (0..{:.0f})'\ + .format(val, settings.MAX_ALLOWED_RADIUS)) + return val + + @staticmethod + def get_direction_param(params): + """look for 'direction' argument and make sure a valid direction is given + We accept northbased azimuth angles, e.g. N, NW, SSE, etc. + This argument is always optional, therefore we don't look for a default + and None can be returned.""" + val = params.get('direction') + if val is not None: + # check validity of argument value + labels = Directions.LABELS + if not val.upper() in labels: + raise ValueError('query argument for direction {} not valid. Allowed values: {}'\ + .format(val, ', '.join([x for x in labels]))) + return val + + @staticmethod + def get_bydirection_param(params): + """look for boolean by_direction argument + defaults to False + Accepted values for True are true, 'true', 1, 'yes', 'y' + """ + val = params.get('by_direction') + if val is None: val = False - return val - - -def get_generic_param(params, keystring): - """ - search for and process any service-specific query arguments - Values will be converted to integer or float where appropriate and necessary - """ - val = params.get(keystring) - # check for default value in settings - if val is None: - test = 'DEFAULT_' + keystring.upper() - if test in DEFAULT_KEYS: - val = getattr(settings, test) - elif settings.DEBUG: - print('# DEBUG: No default found for {} in settings.'.format(keystring)) - # convert val to numeric if possible and necessary - if isinstance(val, str): - # check for int, then float - # Note: these tests don't capture all cases of 'weird pseudo-numbers' - # e.g. '123-456' will pass as numeric. See discussion at - # http://nbviewer.jupyter.org/github/rasbt/One-Python-benchmark-per-day/blob/master/ipython_nbs/day6_string_is_number.ipynb?create=1 - # Note: we should re-think if it is a good strategy to try conversion for all params generically. - # Perhaps better to limit this to the known arguments, i.e. lon, lat, radius, and leave conversion of - # all other params to the respective view. - test = val.replace('-', '', 1) - if test.isdigit(): - try: - val = int(val) - except: - pass - elif test.replace('.', '', 1).isdigit(): + if isinstance(val, str): + if val.lower() in ['true', 'yes', 'y']: + val = True + else: try: - val = float(val) + if val == 1: + val = True except: - pass - return val - - -def get_query_params(params, param_keys, add_agg=True): - """Standardized extraction of variables from the query parameters for all locations services - including (some) validation - - params: query parameters as obtained in APIView, i.e. a Django QueryDict - param_keys: a list of accepted query arguments - - Example: - import views_commons as commons - lat, lon, radius = commons.get_query_params(params, ['lat', 'lon', 'radius']) - - Note: special handling for lat and lon also looks for 'long', 'longitude', and 'latitude' - ToDo: also accept OGC compliant syntax '&SUBSET=Lat(y)&SUBSET=Long(x)' - ToDo: improve error handling - write ServiceError class and pass information back to user - Note: be careful about QueryDict properties - default behaviour is to return last occurence with get! - """ - tmp = OrderedDict() - for k in param_keys: - k = k.lower() - if k == 'lat': - val = get_latitude_param(params) - tmp['lat'] = val - elif k == 'lon': - val = get_longitude_param(params) - tmp['lon'] = val - elif k == 'radius': - radius_none_by_default = ('agg' in param_keys) - val = get_radius_param(params, radius_none_by_default) - tmp['radius'] = val - elif k == 'agg': - val = get_agg_param(params) - tmp['agg'] = val - elif k == 'direction': - val = get_direction_param(params) - tmp['direction'] = val - elif k == 'by_direction': - val = get_bydirection_param(params) - tmp['by_direction'] = val - # parse any other service-specific query arguments - else: - val = get_generic_param(params, k) - tmp[k] = val - # use DEFAULT_AGG from settings if radius is > 0 and agg not given - r = tmp.get('radius', 0.) - if add_agg and r is not None and r > 0. and tmp.get('agg') is None: - tmp['agg'] = settings.DEFAULT_AGG - # return values as tuple for unpacking - print(tmp) - return tuple(tmp.values()) + val = False + return val + + def get_generic_param(self, params, keystring): + """ + search for and process any service-specific query arguments + Values will be converted to integer or float where appropriate and necessary + """ + val = params.get(keystring) + # check for default value in settings + if val is None: + test = 'DEFAULT_' + keystring.upper() + if test in self.DEFAULT_KEYS: + val = getattr(settings, test) + elif settings.DEBUG: + print('# DEBUG: No default found for {} in settings.'.format(keystring)) + # convert val to numeric if possible and necessary + if isinstance(val, str): + # check for int, then float + # Note: these tests don't capture all cases of 'weird pseudo-numbers' + # e.g. '123-456' will pass as numeric. See discussion at + # http://nbviewer.jupyter.org/github/rasbt/One-Python-benchmark-per-day/blob/master/ipython_nbs/day6_string_is_number.ipynb?create=1 + # Note: we should re-think if it is a good strategy to try conversion for all params generically. + # Perhaps better to limit this to the known arguments, i.e. lon, lat, radius, and leave conversion of + # all other params to the respective view. + test = val.replace('-', '', 1) + if test.isdigit(): + try: + val = int(val) + except: + pass + elif test.replace('.', '', 1).isdigit(): + try: + val = float(val) + except: + pass + return val + + def get_query_params(self, params, param_keys, add_agg=True): + """Standardized extraction of variables from the query parameters for all locations services + including (some) validation + + params: query parameters as obtained in APIView, i.e. a Django QueryDict + param_keys: a list of accepted query arguments + + Example: + import views_commons as commons + lat, lon, radius = commons.get_query_params(params, ['lat', 'lon', 'radius']) + + Note: special handling for lat and lon also looks for 'long', 'longitude', and 'latitude' + ToDo: also accept OGC compliant syntax '&SUBSET=Lat(y)&SUBSET=Long(x)' + ToDo: improve error handling - write ServiceError class and pass information back to user + Note: be careful about QueryDict properties - default behaviour is to return last occurence with get! + """ + tmp = OrderedDict() + for k in param_keys: + k = k.lower() + if k == 'lat': + val = self.get_latitude_param(params) + tmp['lat'] = val + elif k == 'lon': + val = self.get_longitude_param(params) + tmp['lon'] = val + elif k == 'radius': + radius_none_by_default = not ('agg' in param_keys) + val = self.get_radius_param(params, radius_none_by_default) + tmp['radius'] = val + elif k == 'agg': + val = self.get_agg_param(params) + tmp['agg'] = val + elif k == 'direction': + val = self.get_direction_param(params) + tmp['direction'] = val + elif k == 'by_direction': + val = self.get_bydirection_param(params) + tmp['by_direction'] = val + # parse any other service-specific query arguments + else: + val = self.get_generic_param(params, k) + tmp[k] = val + # use DEFAULT_AGG from settings if radius is > 0 and agg not given + r = tmp.get('radius', 0.) + if add_agg and r is not None and r > 0. and tmp.get('agg') is None: + tmp['agg'] = settings.DEFAULT_AGG + # return values as tuple for unpacking + print(tmp) + return tuple(tmp.values()) diff --git a/wheat_production/serializers.py b/wheat_production/serializers.py index 2ddacd511ee7215e83c93b6aaf067c51f97f919d..a2484581a6033f19adc3376739b76cc14de0e0e2 100644 --- a/wheat_production/serializers.py +++ b/wheat_production/serializers.py @@ -46,17 +46,26 @@ class AggSerializer(BaseSerializer): vlength = 1 # TODO: check units below if vlength > 1: - properties = OrderedDict([ - ('agg_function', agg_function), - ('many', True), - (agg_function, OrderedDict([ - (d, v) for d, v in zip(obj['direction'], val) - ])), - ('units', 'kt'), - ('radius', obj['radius']), - ]) if obj['direction'] is None: - properties.pop('direction') + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(range(vlength), val) + ])), + ('units', 'kt'), + ('radius', obj['radius']), + ]) + else: + properties = OrderedDict([ + ('agg_function', agg_function), + ('many', True), + (agg_function, OrderedDict([ + (d, v) for d, v in zip(obj['direction'], val) + ])), + ('units', 'kt'), + ('radius', obj['radius']), + ]) else: properties = OrderedDict([ ('agg_function', agg_function), diff --git a/wheat_production/views.py b/wheat_production/views.py index 7134e695b5d2bd3b7b802ecf9141b4f79cf12147..24b88217f97cbce109bb28cdb7fe6876a3f911f5 100644 --- a/wheat_production/views.py +++ b/wheat_production/views.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from toar_location_services.settings import DEBUG from .wheat_file_extraction import read_proxydata from .serializers import AggSerializer -from utils.views_commons import get_query_params, get_agg_function +from utils.views_commons import CommonViews from utils.geoutils import Directions from utils.extraction_tools import extract_value, extract_value_stats @@ -19,13 +19,16 @@ if DEBUG: print("File %s successfully loaded" % FILENAME, datainfo) -class WheatView(APIView): +class WheatView(APIView, CommonViews): + + def __init__(self): + CommonViews.__init__(self) def _extract(self, lat, lon, radius, agg, direction, by_direction): """perform actual extraction of desired quantity""" print('**by_direction:', by_direction, '** radius, agg = ', radius, agg) if agg is not None and radius is not None and radius > 0.: - agg_function = get_agg_function(agg) + agg_function = self.get_agg_function(agg) min_angle = None max_angle = None if by_direction: @@ -77,7 +80,7 @@ class WheatView(APIView): by_direction: if True, data are returned as vector with one value aggregated over each of 16 wind directions. """ - lat, lon, radius, agg, direction, by_direction = get_query_params(request.query_params, + lat, lon, radius, agg, direction, by_direction = self.get_query_params(request.query_params, ['lat', 'lon', 'radius', 'agg', 'direction', 'by_direction']) result, agg, direction = self._extract(lat, lon, radius, agg, direction, by_direction)