diff --git a/climatic_zones/__init__.py b/climatic_zones/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/climatic_zones/admin.py b/climatic_zones/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/climatic_zones/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/climatic_zones/apps.py b/climatic_zones/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..a9fa3e119d1d9c05c5d92fb4ec82268c196dd494 --- /dev/null +++ b/climatic_zones/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ClimaticZonesConfig(AppConfig): + name = 'climatic_zones' diff --git a/climatic_zones/climatic_zones_file_extraction.py b/climatic_zones/climatic_zones_file_extraction.py new file mode 100644 index 0000000000000000000000000000000000000000..768bead809fc92a802215f7d4d69bce568436114 --- /dev/null +++ b/climatic_zones/climatic_zones_file_extraction.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +""" +Load stable night lights data from file or use dummy data. + +..todo: implementation of RASDAMAN access + +Author: Lukas Leufen, FZ Juelich (4th June 2019) +""" +import numpy as np +import os +from toar_location_services.settings import DATA_DIR, DEBUG, USE_DUMMY_CLIMATIC_ZONES_DATA, PLOT_DATA +from django.contrib.gis.gdal import GDALRaster +import matplotlib.pyplot as plt +# import gdal +# from gdalconst import GA_ReadOnly + + +def read_proxydata(filename, dummy=DEBUG and USE_DUMMY_CLIMATIC_ZONES_DATA, plot=False): + """ + PLACEHOLDER + expand this routine, if you want to use real and not dummy data + """ + if dummy: + return create_dummy_data() + + CLIMATIC_ZONES_FILE = os.path.join(DATA_DIR, filename) + + if DEBUG: + print("DATA_DIR = ", DATA_DIR, "...") + print("Opening ", CLIMATIC_ZONES_FILE, "...") + + with open(CLIMATIC_ZONES_FILE, "r") as dataset: + tok, cols = dataset.readline().split() + tok, rows = dataset.readline().split() + cols = int(cols) + rows = int(rows) + tok, lon0 = dataset.readline().split() + tok, lat0 = dataset.readline().split() + tok, dlon = dataset.readline().split() + lon0 = float(lon0) + lat0 = float(lat0) + dlon = float(dlon) + dlat = dlon + tok, missval = dataset.readline().split() + + # construct data array and lonvec, latvec + data = np.zeros((rows, cols), dtype='f2') + lonvec = np.linspace(lon0, lon0+cols*dlon, cols) + latvec = np.linspace(lat0, lat0+rows*dlat, rows) + + # data are flipped, therefore reverse latitudes + # trick from http://stackoverflow.com/questions/6771428/most-efficient-way-to-reverse-a-numpy-array + latvec = np.fliplr(np.atleast_2d((latvec)))[0] + + # read actual data + for i, line in enumerate(dataset): + row = np.array([int(x) for x in line.split()], dtype='f2') + data[i, :] = row + + # correct missing values + data[data == float(missval)] = np.nan + + if (DEBUG and PLOT_DATA) or plot: + fig, ax = plt.subplots() + cmap = plt.cm.get_cmap('tab20') + cax = ax.contourf(lonvec, latvec, data, levels=np.arange(-.5, 13).tolist(), cmap=cmap) + cbar = fig.colorbar(cax, ticks=np.arange(0, 13).tolist()) + cbar.ax.set_yticklabels(["Water", "Warm Temperate Moist", "Warm Temperate Dry", "Cool Temperate Moist", + "Cool Temperate Dry", "Polar Moist", "Polar Dry", "Boreal Moist", "Boreal Dry", + "Tropical Montane", "Tropical Wet", "Tropical Moist", "Tropical Dry"]) + plt.savefig('../plots/global_climatic_zone.png') + plt.close() + + + # set metadata + boundingbox = [lon0, lat0, lonvec.max(), latvec.max()] + + datainfo = {'size': (rows, cols), 'resolution': (np.abs(dlon), np.abs(dlat)), 'boundingbox': boundingbox} + + return lonvec, latvec, data, datainfo + + +def create_dummy_data(): + """generate some small dummy data set for testing of other services + This avoids loading of a large file + Eventually this should become obsolete when we have rasdaman running...""" + lonvec = np.array([4., 5., 6.]) + latvec = np.array([52., 53., 54.]) + data = np.array([[100., 100., 105.], [200., 250., 300.], [600., 700., 800.]]) + datainfo = {'size': (3, 3), 'resolution': (1., 1.), + 'boundingbox': [4., 52., 6., 54.]} + msg = "#DEBUG: Using dummy data for stable night lights!" + if DEBUG: + print(msg) + else: + raise UserWarning(msg) + return lonvec, latvec, data, datainfo diff --git a/climatic_zones/migrations/__init__.py b/climatic_zones/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/climatic_zones/models.py b/climatic_zones/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/climatic_zones/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/climatic_zones/serializers.py b/climatic_zones/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..7c1cef64342d0c392eef831c6d7606a15dd86f37 --- /dev/null +++ b/climatic_zones/serializers.py @@ -0,0 +1,108 @@ +"""serializer for climate zones +""" + +from collections import OrderedDict +import datetime as dt +from rest_framework.serializers import BaseSerializer + + +# helper functions + +def get_provenance(obj): + """construct provenance information on climatic zones dataset""" + # TODO: Complete provenance information + prov = OrderedDict([ + ('dataset_name', '?'), + ('dataset_description', """RClimatic zones in ~9 km resolution were obtained from the eusoils project.\n + The data were output to ascii text format by:\n + Dr Katrina Sharps\n + Centre for Ecology & Hydrology, Environment Centre Wales, UK\n + 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 + 4 Cool Temperate Dry\n + 5 Polar Moist\n + 6 Polar Dry\n + 7 Boreal Moist\n + 8 Boreal Dry\n + 9 Tropical Montane\n + 10 Tropical Wet\n + 11 Tropical Moist\n + 12 Tropical Dry"""), + ('data_source', """Dr Katrina Sharps\nCentre for Ecology & Hydrology, Environment Centre Wales, UK\n + Tel. + 44 (0)1248 374518 (direct)\nTel. + 44 (0)1248 374500 (reception)\nE-mail: katshar@ceh.ac.uk"""), + ('datacenter_url', 'http://eusoils.jrc.ec.europa.eu/projects/RenewableEnergy/'), + ('download_date', '?'), + ('timestamp', dt.datetime.now().isoformat()) + ]) + return prov + + +# serializer classes +class AggSerializer(BaseSerializer): + """ see http://www.django-rest-framework.org/api-guide/serializers/#baseserializer """ + + def to_representation(self, obj): + """takes dictionary-like obj and returns geojson compliant structure""" + agg_function = obj['agg_function'] + val = obj[agg_function] + + # build GeoJSON response with 'provenance' extension + # ToDo (probably in views): change content-type to application/vnd.geo+json + # ToDo: enable support for different output formats + # format properties depending on 'by_direction' (vector or not) + try: + vlength = len(val) + except TypeError: + vlength = 1 + # TODO: check units below + for k, v in obj.items(): + print(k, v) + if vlength > 1: + if obj['direction'] is None: + 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', '?'), + ('radius', obj['radius']), + ('direction', obj['direction']), + ]) + if obj['direction'] is None: + properties.pop('direction') + + response = OrderedDict([ + ('type', 'Feature'), + ('geometry', OrderedDict([ + ('type', 'Point'), + ('coordinates', [obj['lon'], obj['lat']]), + ])), + ('properties', properties), + ('provenance', get_provenance(obj)), + ]) + + return response diff --git a/climatic_zones/tests.py b/climatic_zones/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..c202a939aa4fe176061f3d4f7375aae8d9f652d6 --- /dev/null +++ b/climatic_zones/tests.py @@ -0,0 +1,12 @@ +from django.test import TestCase + +# Create your tests here. + +from climatic_zones_file_extraction import read_proxydata +from toar_location_services import settings + + +FILENAME = "climate_ascii.txt" +settings.PLOT_DATA = True + +read_proxydata(FILENAME, dummy=False, plot=True) diff --git a/climatic_zones/urls.py b/climatic_zones/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..1dbf414dcd5d252a5d14c48f7ec018a8e4a12ade --- /dev/null +++ b/climatic_zones/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url +from .views import ClimateView + +urlpatterns = [ + url(r'^$', ClimateView.as_view()), + ] diff --git a/climatic_zones/views.py b/climatic_zones/views.py new file mode 100644 index 0000000000000000000000000000000000000000..d0ec24a8ccdf99ad6b0beb68c560869c33228eb9 --- /dev/null +++ b/climatic_zones/views.py @@ -0,0 +1,102 @@ +import datetime as dt +import numpy as np +from collections import OrderedDict +from rest_framework.views import APIView +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 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" + +lonvec, latvec, data, datainfo = read_proxydata(FILENAME) + +if DEBUG: + print("File %s successfully loaded" % FILENAME, datainfo) + + +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 = self.get_agg_function(agg) + min_angle = None + max_angle = None + if by_direction: + result = np.zeros((16,)) + direction = Directions.LABELS + for i, d in enumerate(Directions.LABELS): + min_angle, max_angle = Directions.edges(d) + # 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=12, + radius=radius, min_angle=min_angle, max_angle=max_angle, + agg=agg_function) + else: + if direction is not None: + min_angle, max_angle = Directions.edges(direction) + # 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=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=12) + # 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 rice_production app + + returns a Geo-JSON response with information about the rice production at or + around a point location. + + required arguments: + lat: latitude in degrees_north + lng: longitude in degrees_east (can be either -180 to 180 or 0 to 360) + + optional arguments: + radius: search radius in m. See settings.py for default and max allowed values. + Without 'agg', the radius defaults to None and the rice production at the + point location is returned. + agg: method of aggregation for data around point location. See settings.py for + default method. Only evaluated if radius > 0. Allowed methods are mean, + min, max, median, and NN-percentile (see views_commons.py) + direction: return data aggregation in one direction (wind sector) only. + Direction must be given as wind sector (e.g. 'N', 'NNE', 'NE', etc.). + 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 = 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) + + rawdata = OrderedDict([ + ("lat", lat), + ("lon", lon), + ("radius", radius), + ("direction", direction), + ("agg_function", agg), + (agg, result), + ]) + response = AggSerializer(rawdata).data + return Response(response) + 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/__init__.py b/rice_production/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/rice_production/admin.py b/rice_production/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/rice_production/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/rice_production/apps.py b/rice_production/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..a37fc3b5b2d12c854bd13a94a0992f6eb2dfbd0a --- /dev/null +++ b/rice_production/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class RiceProductionConfig(AppConfig): + name = 'rice_production' diff --git a/rice_production/migrations/__init__.py b/rice_production/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/rice_production/models.py b/rice_production/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/rice_production/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/rice_production/rice_file_extraction.py b/rice_production/rice_file_extraction.py new file mode 100644 index 0000000000000000000000000000000000000000..8b64276d80a812e5e60f0e5a3c8c66eb20d9c2ad --- /dev/null +++ b/rice_production/rice_file_extraction.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +""" +Import rice production from .txt-file + +Author: Lukas Leufen, FZ Juelich (28th May 2019) +""" + +import numpy as np +import os +from toar_location_services.settings import DATA_DIR, DEBUG, USE_DUMMY_RICE_DATA, PLOT_DATA +import matplotlib.pyplot as plt + + +def read_proxydata(filename, dummy=DEBUG and USE_DUMMY_RICE_DATA, plot=False): + """Read the ascii file and return the data array together with + some dataset properties for use in the extraction routines. + + filename: name of data file (rice.txt) + dummy: if true a small set of dummy data are returned to speed up development of other services + """ + + if dummy: + return create_dummy_data() + + if DEBUG: + print("DATA_DIR = ", DATA_DIR, "...") + print("Opening ", os.path.join(DATA_DIR, filename), "...") + + with open(os.path.join(DATA_DIR, filename), "r") as dataset: + tok, cols = dataset.readline().split() + tok, rows = dataset.readline().split() + cols = int(cols) + rows = int(rows) + tok, lon0 = dataset.readline().split() + tok, lat0 = dataset.readline().split() + tok, dlon = dataset.readline().split() + lon0 = float(lon0) + lat0 = float(lat0) + dlon = float(dlon) + dlat = dlon + tok, missval = dataset.readline().split() + + # construct data array and lonvec, latvec + data = np.zeros((rows, cols), dtype='f4') + lonvec = np.linspace(lon0, lon0 + cols * dlon, cols) + latvec = np.linspace(lat0, lat0 + rows * dlat, rows) + + # data are flipped, therefore reverse latitudes + # trick from http://stackoverflow.com/questions/6771428/most-efficient-way-to-reverse-a-numpy-array + latvec = np.fliplr(np.atleast_2d((latvec)))[0] + + # read actual data + for i, line in enumerate(dataset): + row = np.array([float(x) for x in line.split()], dtype='f4') + data[i, :] = row + + # correct missing values + data[data == float(missval)] = np.nan + + logdata = data.copy() + logdata[logdata <= 1.e-4] = 1.e-4 + + if (DEBUG and PLOT_DATA) or plot: + plt.contourf(lonvec, latvec, np.log10(logdata)) + plt.savefig('../plots/global_rice_production.png') + plt.close() + + # set metadata + boundingbox = [lon0, lat0, lonvec.max(), latvec.max()] + + datainfo = {'size': (rows, cols), 'resolution': (np.abs(dlon), np.abs(dlat)), + 'boundingbox': boundingbox} + return lonvec, latvec, data, datainfo + + +def create_dummy_data(): + """generate some small dummy data set for testing of other services + This avoids loading of a large file + Eventually this should become obsolete when we have rasdaman running...""" + lonvec = np.array([4., 5., 6.]) + latvec = np.array([52., 53., 54.]) + data = np.array([[1., 10., 11.], [0., 2.5, 3.], [6., 7., 8.]]) + datainfo = {'size': (3, 3), 'resolution': (1., 1.), + 'boundingbox': [4., 52., 6., 54.]} + msg = "#DEBUG: Using dummy data for rice production" + if DEBUG: + print(msg) + else: + raise UserWarning(msg) + return lonvec, latvec, data, datainfo diff --git a/rice_production/serializers.py b/rice_production/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..ceac3b4cdaae1523197762d7e98ef460b6fd6cea --- /dev/null +++ b/rice_production/serializers.py @@ -0,0 +1,91 @@ +"""serializer for rice production data +""" + +from collections import OrderedDict +import datetime as dt +from rest_framework.serializers import BaseSerializer + + +# helper functions + +def get_provenance(obj): + """construct provenance information on rice production dataset""" + # TODO: Fill right provenance information + prov = OrderedDict([ + ('dataset_name', '?'), + ('dataset_description', """Rice production values for the globe at 5 arc minute resolution. The data are in + units of production (irrigated + non-irrigated) in thousand tonnes. \nThe data were downloaded from the GAEZ + data portal ( http://gaez.fao.org/Main.html# ) and then output to ascii text format by:\nDr Katrina Sharps\n + Centre for Ecology & Hydrology, Environment Centre Wales, UK\nTel. + 44 (0)1248 374518 (direct)\n + Tel. + 44 (0)1248 374500 (reception)\nE-mail: katshar@ceh.ac.uk"""), + ('data_source', """Dr Katrina Sharps\nCentre for Ecology & Hydrology, Environment Centre Wales, UK\n + Tel. + 44 (0)1248 374518 (direct)\nTel. + 44 (0)1248 374500 (reception)\nE-mail: katshar@ceh.ac.uk"""), + ('datacenter_url', 'http://gaez.fao.org/Main.html'), + ('download_date', '?'), + ('timestamp', dt.datetime.now().isoformat()) + ]) + return prov + + +# serializer classes +class AggSerializer(BaseSerializer): + """ see http://www.django-rest-framework.org/api-guide/serializers/#baseserializer """ + + def to_representation(self, obj): + """takes dictionary-like obj and returns geojson compliant structure""" + agg_function = obj['agg_function'] + val = obj[agg_function] + + # build GeoJSON response with 'provenance' extension + # ToDo (probably in views): change content-type to application/vnd.geo+json + # ToDo: enable support for different output formats + # format properties depending on 'by_direction' (vector or not) + try: + vlength = len(val) + except TypeError: + vlength = 1 + # TODO: check units below + if vlength > 1: + if obj['direction'] is None: + 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), + ('many', False), + (agg_function, val), + ('units', 'kt'), + ('radius', obj['radius']), + ('direction', obj['direction']), + ]) + if obj['direction'] is None: + properties.pop('direction') + + response = OrderedDict([ + ('type', 'Feature'), + ('geometry', OrderedDict([ + ('type', 'Point'), + ('coordinates', [obj['lon'], obj['lat']]), + ])), + ('properties', properties), + ('provenance', get_provenance(obj)), + ]) + + return response diff --git a/rice_production/tests.py b/rice_production/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..42e0b6f8b9d03bc221dda0e2f6ca8cd9b83dacbb --- /dev/null +++ b/rice_production/tests.py @@ -0,0 +1,12 @@ +from django.test import TestCase + +# Create your tests here. + +from rice_file_extraction import read_proxydata +from toar_location_services import settings + + +FILENAME = "rice.txt" +settings.PLOT_DATA = True + +read_proxydata(FILENAME, dummy=False, plot=True) diff --git a/rice_production/urls.py b/rice_production/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..91c8d414dc7d6067c7bfbaf48187e4a59bf84502 --- /dev/null +++ b/rice_production/urls.py @@ -0,0 +1,6 @@ +from django.conf.urls import url +from .views import RiceView + +urlpatterns = [ + url(r'^$', RiceView.as_view()), + ] diff --git a/rice_production/views.py b/rice_production/views.py new file mode 100644 index 0000000000000000000000000000000000000000..9a2ade4400f3a90be39b4719c0d9ec77380ba8f8 --- /dev/null +++ b/rice_production/views.py @@ -0,0 +1,97 @@ +import datetime as dt +import numpy as np +from collections import OrderedDict +from rest_framework.views import APIView +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 CommonViews +from utils.geoutils import Directions +from utils.extraction_tools import extract_value, extract_value_stats + +FILENAME = "rice.txt" + +lonvec, latvec, data, datainfo = read_proxydata(FILENAME) + +if DEBUG: + print("File %s successfully loaded" % FILENAME, datainfo) + + +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 = self.get_agg_function(agg) + min_angle = None + max_angle = None + if by_direction: + result = np.zeros((16,)) + direction = Directions.LABELS + for i, d in enumerate(Directions.LABELS): + min_angle, max_angle = Directions.edges(d) + # 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, + radius=radius, min_angle=min_angle, max_angle=max_angle, + agg=agg_function) + else: + if direction is not None: + min_angle, max_angle = Directions.edges(direction) + # 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, + 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) + # 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 rice_production app + + returns a Geo-JSON response with information about the rice production at or + around a point location. + + required arguments: + lat: latitude in degrees_north + lng: longitude in degrees_east (can be either -180 to 180 or 0 to 360) + + optional arguments: + radius: search radius in m. See settings.py for default and max allowed values. + Without 'agg', the radius defaults to None and the rice production at the + point location is returned. + agg: method of aggregation for data around point location. See settings.py for + default method. Only evaluated if radius > 0. Allowed methods are mean, + min, max, median, and NN-percentile (see views_commons.py) + direction: return data aggregation in one direction (wind sector) only. + Direction must be given as wind sector (e.g. 'N', 'NNE', 'NE', etc.). + 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 = 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) + + rawdata = OrderedDict([ + ("lat", lat), + ("lon", lon), + ("radius", radius), + ("direction", direction), + ("agg_function", agg), + (agg, result), + ]) + response = AggSerializer(rawdata).data + return Response(response) + 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 7afb2ab71381361af789acce6e880834d0c02c24..7b99bb19d36645086cabce641438ca3d3b4a19fe 100644 --- a/toar_location_services/settings.py +++ b/toar_location_services/settings.py @@ -44,7 +44,9 @@ INSTALLED_APPS = [ 'population-density', 'topography-tandem-x', 'stable_night_lights', - 'wheat_production' + 'wheat_production', + 'rice_production', + 'climatic_zones' ] MIDDLEWARE = [ @@ -143,8 +145,10 @@ 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 +USE_DUMMY_RICE_DATA = False +USE_DUMMY_CLIMATIC_ZONES_DATA = False PLOT_DATA = False diff --git a/toar_location_services/urls.py b/toar_location_services/urls.py index 0fd8d49bdd7897755f20e3c8f07e7d75fc98499b..80f498556e8b843b8f70f82afdc5dcff28b43ba6 100644 --- a/toar_location_services/urls.py +++ b/toar_location_services/urls.py @@ -9,5 +9,7 @@ urlpatterns = [ url(r'population-density/', include('population-density.urls'), name='population-density-density'), url(r'topography-tandem-x/', include('topography-tandem-x.urls'), name='topography-tandem-x'), url(r'stable_night_lights/', include('stable_night_lights.urls'), name='stable_night_lights'), - url(r'wheat_production/', include('wheat_production.urls'), name='wheat_production') + url(r'wheat_production/', include('wheat_production.urls'), name='wheat_production'), + url(r'rice_production/', include('rice_production.urls'), name='rice_production'), + url(r'climatic_zones/', include('climatic_zones.urls'), name='climatic_zones') ] diff --git a/toar_location_services/views.py b/toar_location_services/views.py index a90aa29405f319a04c1be88af373a297009aded9..542ab2a9e253b114d789cabee321f0306515a4e0 100644 --- a/toar_location_services/views.py +++ b/toar_location_services/views.py @@ -16,6 +16,8 @@ class LocationServicesRootView(APIView): ('topography-tandem-x', request.build_absolute_uri()+'topography-tandem-x/'), ('stable_night_lights', request.build_absolute_uri()+'stable_night_lights/'), ('wheat_production', request.build_absolute_uri()+'wheat_production/'), + ('rice_production', request.build_absolute_uri()+'rice_production/'), + ('climatic_zones', request.build_absolute_uri()+'climatic_zones/') ]), ] # add admin view if staff user 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)