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)