Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local GeoTIFF import #216

Merged
merged 9 commits into from
Jun 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/actinia_core/core/resource_data_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ def set_user_data(self, user_data):
"""
self.user_data = user_data

def set_request_data(self, request_data):
self.request_data = request_data

def set_storage_model_to_file(self):
self.storage_model = "file"

Expand Down
14 changes: 14 additions & 0 deletions src/actinia_core/core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,17 @@ def get_mv_process(source, dest):
id=f"importer_mv_{os.path.basename(source)}",
skip_permission_check=True)
return p


def allowed_file(filename, allowed_extensions):
"""The function checks if the file has an allowed extension.

Args:
filename (str): The file name
allowed_extensions (list): The list of allowed extensions

Returns:
(bool): Returns True, if file extension is allowed
"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in allowed_extensions
19 changes: 0 additions & 19 deletions src/actinia_core/models/openapi/raster_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@
from copy import deepcopy
from flask_restful_swagger_2 import Schema
from actinia_core.models.response_models import ProcessingResponseModel
from actinia_core.models.openapi.map_layer_base import SetRegionModel


__license__ = "GPLv3"
__author__ = "Sören Gebbert, Carmen Tawalika"
Expand Down Expand Up @@ -196,20 +194,3 @@ class RasterInfoResponseModel(ProcessingResponseModel):
},
"user_id": "user"
}


class RasterRegionCreationModel(Schema):
"""Schema for random raster map layer generation using r.mapcalc in a specific region
"""
type = 'object'
properties = {
'region': SetRegionModel,
'expression': {
'type': 'string',
'description': 'The r.mapcalc expression to create a new raster map '
'layer. The expression must not contain the name of '
'the new raster map layer only the statement after '
'the equal operator: "a + b" instead of "c = a + b"',
'default': "1"
}
}
124 changes: 83 additions & 41 deletions src/actinia_core/rest/raster_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# performance processing of geographical data that uses GRASS GIS for
# computational tasks. For details, see https://actinia.mundialis.de/
#
# Copyright (c) 2016-2018 Sören Gebbert and mundialis GmbH & Co. KG
# Copyright (c) 2016-2021 Sören Gebbert and mundialis GmbH & Co. KG
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -24,21 +24,26 @@
"""
Raster layer resources
"""
from flask import jsonify, make_response
from flask import jsonify, make_response, request
from flask_restful_swagger_2 import swagger
import os
import pickle
from .ephemeral_processing import EphemeralProcessing
from .persistent_processing import PersistentProcessing
from uuid import uuid4
from werkzeug.utils import secure_filename
from actinia_core.rest.ephemeral_processing import EphemeralProcessing
from actinia_core.rest.persistent_processing import PersistentProcessing
from actinia_core.rest.map_layer_base import MapLayerRegionResourceBase
from actinia_core.core.redis_interface import enqueue_job
from actinia_core.core.exceptions import AsyncProcessError
from actinia_core.core.utils import allowed_file
from actinia_core.models.response_models import \
ProcessingResponseModel, ProcessingErrorResponseModel
from actinia_core.core.exceptions import AsyncProcessError
from .map_layer_base import MapLayerRegionResourceBase
from actinia_core.models.response_models import SimpleResponseModel
from actinia_core.models.openapi.raster_layer import \
RasterInfoResponseModel, RasterRegionCreationModel, RasterInfoModel
anikaweinmann marked this conversation as resolved.
Show resolved Hide resolved
RasterInfoResponseModel, RasterInfoModel

__license__ = "GPLv3"
__author__ = "Sören Gebbert, Carmen Tawalika"
__author__ = "Sören Gebbert, Carmen Tawalika, Guido Riembauer, Anika Weinmann"
__copyright__ = "Copyright 2016-2021, Sören Gebbert and mundialis GmbH & Co. KG"
__maintainer__ = "mundialis"

Expand Down Expand Up @@ -168,9 +173,8 @@ def delete(self, location_name, mapset_name, raster_name):

@swagger.doc({
'tags': ['Raster Management'],
'description': 'Create a new raster map layer based on a r.mapcalc expression '
'in a user specific region. This method will fail if '
'the map already exists. '
'description': 'Create a new raster map layer by uploading a GeoTIFF. '
'This method will fail if the map already exists. '
'Minimum required user role: user.',
'parameters': [
{
Expand All @@ -194,17 +198,9 @@ def delete(self, location_name, mapset_name, raster_name):
'required': True,
'in': 'path',
'type': 'string'
},
{
'name': 'creation_params',
'description': 'Parameters to create raster map layer '
'using r.mapcalc in a specific region.',
'required': True,
'in': 'body',
'schema': RasterRegionCreationModel
}
],
'consumes': ['application/json'],
'consumes': ['Content-Type: multipart/form-data'],
'produces': ["application/json"],
'responses': {
'200': {
Expand All @@ -219,19 +215,57 @@ def delete(self, location_name, mapset_name, raster_name):
}
})
def post(self, location_name, mapset_name, raster_name):
"""Create a new raster layer using r.mapcalc expression in a specific
region and value
"""Create a new raster layer by uploading a GeoTIFF
"""
rdc = self.preprocess(has_json=True, has_xml=False,

allowed_extensions = ['tif', 'tiff']

# TODO check if another content type can be used
content_type = request.content_type.split(';')[0]
if content_type != "multipart/form-data":
return make_response(jsonify(SimpleResponseModel(
status="error",
message="Content type is not 'multipart/form-data'")), 400)

if 'file' not in request.files:
return make_response(jsonify(SimpleResponseModel(
status="error",
message="No file part indicated in postbody.")), 400)

# create download cache path if does not exists
if os.path.exists(self.download_cache):
pass
else:
os.mkdir(self.download_cache)

# save file from request
id = str(uuid4())
file = request.files['file']
if file.filename == '':
return make_response(jsonify(SimpleResponseModel(
status="error",
message="No selected file")), 400)
if allowed_file(file.filename, allowed_extensions):
name, extension = secure_filename(file.filename).rsplit('.', 1)
filename = f"{name}_{id}.{extension}"
file_path = os.path.join(self.download_cache, filename)
file.save(file_path)
else:
os.remove(file_path)
return make_response(jsonify(SimpleResponseModel(
status="error",
message="File has a not allowed extension. "
f"Please use {','.join(allowed_extensions)}.")), 400)

rdc = self.preprocess(has_json=False, has_xml=False,
location_name=location_name,
mapset_name=mapset_name,
map_name=raster_name)
if rdc:
rdc.set_request_data(file_path)
enqueue_job(self.job_timeout, start_create_job, rdc)
http_code, response_model = self.wait_until_finish(0.1)
else:
http_code, response_model = pickle.loads(self.response_data)

http_code, response_model = pickle.loads(self.response_data)
return make_response(jsonify(response_model), http_code)


Expand Down Expand Up @@ -343,16 +377,12 @@ def _execute(self):
3. Setup GRASS and create the temporary mapset
4. Execute g.list of the first process chain to check if the target
raster exists
5. If the target raster does not exists then run r.mapcalc
5. If the target raster does not exists then run r.import
6. Copy the local temporary mapset to the storage and merge it into the
target mapset
"""
self._setup()

region = None
if "region" in self.request_data:
region = self.request_data["region"]
expression = self.request_data["expression"]
raster_name = self.map_name
self.required_mapsets.append(self.target_mapset_name)

Expand All @@ -366,14 +396,13 @@ def _execute(self):
process_chain=pc_1)

pc_2 = {}
if region:
pc_2["1"] = {"module": "g.region", "inputs": {}, "flags": "g"}
for key in region:
value = region[key]
pc_2["1"]["inputs"][key] = value

pc_2["2"] = {"module": "r.mapcalc", "inputs": {
"expression": "%s = %s" % (raster_name, expression)}}
pc_2["1"] = {
mmacata marked this conversation as resolved.
Show resolved Hide resolved
"module": "r.import",
"inputs": {
"input": self.rdc.request_data,
"output": raster_name
}
}
# Check the second process chain
pc_2 = self._validate_process_chain(skip_permission_check=True,
process_chain=pc_2)
Expand All @@ -387,9 +416,22 @@ def _execute(self):
raster_list = self.module_output_log[0]["stdout"].split("\n")

if len(raster_list[0]) > 0:
raise AsyncProcessError("Raster layer <%s> exists." % raster_name)
try:
os.remove(self.rdc.request_data)
except Exception:
pass
raise AsyncProcessError(
"Raster layer <%s> exists. Please rename it or delete the old "
"raster layer" % raster_name)

self._execute_process_list(pc_2)
self._copy_merge_tmp_mapset_to_target_mapset()

self.finish_message = "Raster layer <%s> successfully created." % raster_name
# Delete imported file
msg = ""
try:
os.remove(self.rdc.request_data)
except Exception:
mmacata marked this conversation as resolved.
Show resolved Hide resolved
msg = " WARNING: Uploaded file can not be removed."

self.finish_message = f"Raster layer <{raster_name}> successfully created.{msg}"
2 changes: 2 additions & 0 deletions src/actinia_core/rest/resource_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"""
Base class for asynchronous and synchronous responses
"""
import os
import pickle
import time
import uuid
Expand Down Expand Up @@ -103,6 +104,7 @@ def __init__(self, resource_id=None, iteration=None, post_url=None):
self.grass_base_dir = global_config.GRASS_GIS_BASE
self.grass_start_script = global_config.GRASS_GIS_START_SCRIPT
self.grass_addon_path = global_config.GRASS_ADDON_PATH
self.download_cache = os.path.join(global_config.DOWNLOAD_CACHE, self.user_id)

# Set the resource id
if resource_id is None:
Expand Down
66 changes: 55 additions & 11 deletions tests/test_raster_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,36 @@ def test_raster_layer_set_colors(self):
self.create_new_mapset(new_mapset)

# Create
rv = self.server.post(URL_PREFIX + '/locations/nc_spm_08/mapsets/%s/raster_layers/test_layer' % new_mapset,
postbody = {
"list": [
{
"id": "set_region",
"module": "g.region",
"inputs": [
{"param": "n", "value": "228500"},
{"param": "s", "value": "215000"},
{"param": "e", "value": "645000"},
{"param": "w", "value": "630000"},
{"param": "ewres", "value": "50"},
{"param": "nsres", "value": "50"}
]
},
{
"id": "create_raster",
"module": "r.mapcalc",
"inputs": [
{"param": "expression", "value": "test_layer = 1"}]
}
],
"version": "1"
}
rv = self.server.post(URL_PREFIX + '/locations/nc_spm_08/mapsets/%s/processing_async' % new_mapset,
headers=self.user_auth_header,
data=json_dumps({"region":{"n":228500, "s":215000,
"e":645000, "w":630000,
"ewres": 50, "nsres": 50},
"expression": "1"}),
data=json_dumps(postbody),
content_type="application/json")
self.waitAsyncStatusAssertHTTP(
rv, headers=self.user_auth_header, http_status=200, status="finished")

pprint(json_load(rv.data))
self.assertEqual(rv.status_code, 200, "HTML status code is wrong %i" % rv.status_code)
self.assertEqual(rv.mimetype, "application/json", "Wrong mimetype %s" % rv.mimetype)
Expand Down Expand Up @@ -120,14 +143,35 @@ def test_1_raster_layer_set_colors_errors(self):
self.create_new_mapset(new_mapset)

# Create
rv = self.server.post(URL_PREFIX + '/locations/nc_spm_08/mapsets/%s/raster_layers/test_layer' % new_mapset,
postbody = {
"list": [
{
"id": "set_region",
"module": "g.region",
"inputs": [
{"param": "n", "value": "228500"},
{"param": "s", "value": "215000"},
{"param": "e", "value": "645000"},
{"param": "w", "value": "630000"},
{"param": "ewres", "value": "50"},
{"param": "nsres", "value": "50"}
]
},
{
"id": "create_raster",
"module": "r.mapcalc",
"inputs": [
{"param": "expression", "value": "test_layer = 1"}]
}
],
"version": "1"
}
rv = self.server.post(URL_PREFIX + '/locations/nc_spm_08/mapsets/%s/processing_async' % new_mapset,
headers=self.user_auth_header,
data=json_dumps({"region":{"n":228500, "s":215000,
"e":645000, "w":630000,
"ewres": 50, "nsres": 50},
"expression": "1"}),
data=json_dumps(postbody),
content_type="application/json")

self.waitAsyncStatusAssertHTTP(
rv, headers=self.admin_auth_header, http_status=200, status="finished")
pprint(json_load(rv.data))
self.assertEqual(rv.status_code, 200, "HTML status code is wrong %i" % rv.status_code)
self.assertEqual(rv.mimetype, "application/json", "Wrong mimetype %s" % rv.mimetype)
Expand Down
Loading