Skip to content

Commit

Permalink
Merge pull request #7 from neteler/new_raster_sampling_endpoint
Browse files Browse the repository at this point in the history
New raster layer sampling endpoint "sampling_sync"
  • Loading branch information
neteler committed Mar 17, 2022
2 parents 7c60256 + 214b495 commit e434056
Show file tree
Hide file tree
Showing 4 changed files with 591 additions and 8 deletions.
36 changes: 32 additions & 4 deletions src/actinia_statistic_plugin/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,18 @@
AsyncEphemeralSTRDSSamplingGeoJSONResource,
SyncEphemeralSTRDSSamplingGeoJSONResource,
)
from .raster_sampling import (
AsyncEphemeralRasterSamplingResource,
SyncEphemeralRasterSamplingResource,
)

# from .raster_sampling_geojson import AsyncEphemeralRasterSamplingGeoJSONResource, \
# SyncEphemeralRasterSamplingGeoJSONResource


__license__ = "GPLv3"
__author__ = "Sören Gebbert"
__copyright__ = "Copyright 2016, Sören Gebbert"
__maintainer__ = "Sören Gebbert"
__email__ = "soerengebbert@googlemail.com"
__author__ = "Sören Gebbert, Markus Neteler"
__copyright__ = "Copyright 2016-present, Sören Gebbert and mundialis GmbH & Co. KG"


def create_endpoints(flask_api):
Expand Down Expand Up @@ -89,6 +95,7 @@ def create_endpoints(flask_api):
"<string:raster_name>"
"/area_stats_univar_sync",
)

flask_api.add_resource(
AsyncEphemeralSTRDSSamplingResource,
"/locations/<string:location_name>/mapsets/"
Expand All @@ -113,3 +120,24 @@ def create_endpoints(flask_api):
"<string:mapset_name>/strds/<string:strds_name>"
"/sampling_sync_geojson",
)

flask_api.add_resource(
AsyncEphemeralRasterSamplingResource,
"/locations/<string:location_name>/mapsets/"
"<string:mapset_name>/raster_layers/<string:raster_name>"
"/sampling_async",
)
flask_api.add_resource(
SyncEphemeralRasterSamplingResource,
"/locations/<string:location_name>/mapsets/"
"<string:mapset_name>/raster_layers/<string:raster_name>"
"/sampling_sync",
)


# flask_api.add_resource(AsyncEphemeralRasterSamplingGeoJSONResource, '/locations/<string:location_name>/mapsets/'
# '<string:mapset_name>/raster_layers/<string:raster_name>'
# '/sampling_async_geojson')
# flask_api.add_resource(SyncEphemeralRasterSamplingGeoJSONResource, '/locations/<string:location_name>/mapsets/'
# '<string:mapset_name>/raster_layers/<string:raster_name>'
# '/sampling_sync_geojson')
259 changes: 259 additions & 0 deletions src/actinia_statistic_plugin/raster_sampling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# -*- coding: utf-8 -*-
"""
Perform raster map sampling on a raster map layer based on input points.
"""

import pickle
import tempfile
from flask import jsonify, make_response
from copy import deepcopy
from flask_restful_swagger_2 import swagger
from flask_restful_swagger_2 import Schema
from actinia_core.models.response_models import ProcessingErrorResponseModel
from actinia_core.rest.ephemeral_processing import EphemeralProcessing
from actinia_core.rest.resource_base import ResourceBase
from actinia_core.core.common.redis_interface import enqueue_job
from flask.json import dumps
from actinia_core.core.common.app import auth
from actinia_core.core.common.api_logger import log_api_call
from .response_models import RasterSamplingResponseModel


__license__ = "GPLv3"
__author__ = "Markus Neteler"
__copyright__ = "Copyright 2022-present, Markus Neteler and mundialis GmbH & Co. KG"


class PointListModel(Schema):
"""This schema defines the JSON input of the raster sampling resource"""

type = "object"
properties = {
"points": {
"type": "array",
"items": {
"type": "array",
"items": {"type": "string", "maxItems": 3, "minItems": 3},
},
"description": "A list of coordinate points with unique ids [(id, x, y), (id, x, y), (id, x, y)]",
}
}
example = {"points": [["a", "1", "1"], ["b", "2", "2"], ["c", "3", "3"]]}
required = ["points"]


SCHEMA_DOC = {
"tags": ["Raster Sampling"],
"description": "Spatial sampling of a raster dataset with vector points. The vector points must "
"be in the same coordinate reference system as the location that contains the "
"raster dataset. The result of the sampling is located in the resource response"
"JSON document after the processing was finished, "
"as a list of values for each vector point. "
"Minimum required user role: user.",
"consumes": ["application/json"],
"parameters": [
{
"name": "location_name",
"description": "The location name",
"required": True,
"in": "path",
"type": "string",
},
{
"name": "mapset_name",
"description": "The name of the mapset that contains the required raster map layer",
"required": True,
"in": "path",
"type": "string",
},
{
"name": "raster_name",
"description": "The name of the raster map layer to perform the raster map sampling from",
"required": True,
"in": "path",
"type": "string",
},
{
"name": "points",
"description": "The sampling point array [[id, x, y],[id, x, y]]. "
"The coordinates of the sampling points must be the same as of the location "
"that contains the raster dataset.",
"required": True,
"in": "body",
"schema": PointListModel,
},
],
"responses": {
"200": {
"description": "The result of the raster map sampling",
"schema": RasterSamplingResponseModel,
},
"400": {
"description": "The error message and a detailed log why raster sampling did not succeed",
"schema": ProcessingErrorResponseModel,
},
},
}


class AsyncEphemeralRasterSamplingResource(ResourceBase):
"""Perform raster map sampling on a raster map layer based on input points, asynchronous call"""

decorators = [log_api_call, auth.login_required]

def _execute(self, location_name, mapset_name, raster_name):

rdc = self.preprocess(
has_json=True,
has_xml=False,
location_name=location_name,
mapset_name=mapset_name,
map_name=raster_name,
)
if rdc:
enqueue_job(self.job_timeout, start_job, rdc)

return rdc

@swagger.doc(deepcopy(SCHEMA_DOC))
def post(self, location_name, mapset_name, raster_name):
"""Perform raster map sampling on a raster map layer based on input points asynchronously"""
self._execute(location_name, mapset_name, raster_name)
html_code, response_model = pickle.loads(self.response_data)
return make_response(jsonify(response_model), html_code)


class SyncEphemeralRasterSamplingResource(AsyncEphemeralRasterSamplingResource):
"""Perform raster map sampling on a raster map layer based on input points, synchronous call"""

decorators = [log_api_call, auth.login_required]

@swagger.doc(deepcopy(SCHEMA_DOC))
def post(self, location_name, mapset_name, raster_name):
"""Perform raster map sampling on a raster map layer based on input points synchronously"""
check = self._execute(location_name, mapset_name, raster_name)
if check is not None:
http_code, response_model = self.wait_until_finish()
else:
http_code, response_model = pickle.loads(self.response_data)
return make_response(jsonify(response_model), http_code)


def start_job(*args):
processing = AsyncEphemeralRasterSampling(*args)
processing.run()


class AsyncEphemeralRasterSampling(EphemeralProcessing):
"""Sample a raster map at vector points"""

def __init__(self, *args):
EphemeralProcessing.__init__(self, *args)
self.response_model_class = RasterSamplingResponseModel

def _execute(self):

self._setup()

# Points are stored in self.request_data
raster_name = self.map_name
points = self.request_data["points"]

if not points or len(points) == 0:
raise AsyncProcessError("Empty coordinate list")

point_file = tempfile.NamedTemporaryFile(dir=self.temp_file_path, delete=True)
result_file = tempfile.NamedTemporaryFile(dir=self.temp_file_path, delete=True)

for tuple in points:
if len(tuple) != 3:
raise AsyncProcessError("Wrong number of coordinate entries")

id, x, y = tuple
row = "%s|%s|%s\n" % (id, x, y)
point_file.write(row.encode())

point_file.flush()

pc = {
"list": [
{
"id": "v_in_ascii",
"module": "v.in.ascii",
"inputs": [
{"param": "input", "value": point_file.name},
{"param": "format", "value": "point"},
{
"param": "column",
"value": "id text, x double precision, y double precision",
},
{"param": "x", "value": "2"},
{"param": "y", "value": "3"},
],
"outputs": [{"param": "output", "value": "input_points"}],
},
{
"id": "g_region",
"module": "g.region",
"inputs": [
{"param": "vector", "value": "input_points"},
{
"param": "align",
"value": "%s@%s" % (raster_name, self.mapset_name),
},
],
"flags": "p",
},
{
"id": "r_what",
"module": "r.what",
"inputs": [
{
"param": "map",
"value": "%s@%s" % (raster_name, self.mapset_name),
},
{"param": "points", "value": "input_points"},
],
"outputs": [{"param": "output", "value": result_file.name}],
"flags": "nrf",
"overwrite": True,
"superquiet": True,
},
],
"version": "1",
}

self.request_data = pc

# Run the process chain
EphemeralProcessing._execute(self, skip_permission_check=True)

result = open(result_file.name, "r").readlines()

output_list = []
raster_name_qualified = "%s@%s" % (raster_name, self.mapset_name)
# remove map name from columns
colum_name = [
col
if raster_name_qualified not in col
else col.replace(f"{raster_name_qualified}_", "").replace(
raster_name_qualified, "value"
)
for col in result[0].strip().split("|")
]
for line, point in zip(result[1:], points):
entry = dict()
entry[point[0]] = {
key: value for key, value in zip(colum_name, line.strip().split("|"))
}
# remove site_name (always empty)
del entry[point[0]]["site_name"]
# add "map_name": raster_name
entry[point[0]]["map_name"] = raster_name

output_list.append(entry)

self.module_results = output_list

point_file.close()
result_file.close()
Loading

0 comments on commit e434056

Please sign in to comment.