Merge pull request #1405 from pierotofy/contours

GDAL based contours
pull/1406/head
Piero Toffanin 2023-09-26 13:04:21 -04:00 zatwierdzone przez GitHub
commit 9b49ad777d
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 78 dodań i 115 usunięć

Wyświetl plik

@ -3,10 +3,80 @@ import os
from rest_framework import status
from rest_framework.response import Response
from app.plugins.views import TaskView, CheckTask, GetTaskResult
from worker.tasks import execute_grass_script
from app.plugins.grass_engine import grass, GrassEngineException, cleanup_grass_context
from app.plugins.worker import run_function_async
from django.utils.translation import gettext_lazy as _
class ContoursException(Exception):
pass
def calc_contours(dem, epsg, interval, output_format, simplify):
import os
import subprocess
import tempfile
import shutil
import glob
from webodm import settings
ext = ""
if output_format == "GeoJSON":
ext = "json"
elif output_format == "GPKG":
ext = "gpkg"
elif output_format == "DXF":
ext = "dxf"
elif output_format == "ESRI Shapefile":
ext = "shp"
MIN_CONTOUR_LENGTH = 10
tmpdir = os.path.join(settings.MEDIA_TMP, os.path.basename(tempfile.mkdtemp('_contours', dir=settings.MEDIA_TMP)))
gdal_contour_bin = shutil.which("gdal_contour")
ogr2ogr_bin = shutil.which("ogr2ogr")
if gdal_contour_bin is None:
return {'error': 'Cannot find gdal_contour'}
if ogr2ogr_bin is None:
return {'error': 'Cannot find ogr2ogr'}
contours_file = f"contours.gpkg"
p = subprocess.Popen([gdal_contour_bin, "-q", "-a", "level", "-3d", "-f", "GPKG", "-i", str(interval), dem, contours_file], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
out = out.decode('utf-8').strip()
err = err.decode('utf-8').strip()
success = p.returncode == 0
if not success:
return {'error', f'Error calling gdal_contour: {str(err)}'}
outfile = os.path.join(tmpdir, f"output.{ext}")
p = subprocess.Popen([ogr2ogr_bin, outfile, contours_file, "-simplify", str(simplify), "-f", output_format, "-t_srs", f"EPSG:{epsg}", "-nln", "contours",
"-dialect", "sqlite", "-sql", f"SELECT * FROM contour WHERE ST_Length(GEOM) >= {MIN_CONTOUR_LENGTH}"], cwd=tmpdir, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = p.communicate()
out = out.decode('utf-8').strip()
err = err.decode('utf-8').strip()
success = p.returncode == 0
if not success:
return {'error', f'Error calling ogr2ogr: {str(err)}'}
if not os.path.isfile(outfile):
return {'error': f'Cannot find output file: {outfile}'}
if output_format == "ESRI Shapefile":
ext="zip"
shp_dir = os.path.join(tmpdir, "contours")
os.makedirs(shp_dir)
contour_files = glob.glob(os.path.join(tmpdir, "output.*"))
for cf in contour_files:
shutil.move(cf, shp_dir)
shutil.make_archive(os.path.join(tmpdir, 'output'), 'zip', shp_dir)
outfile = os.path.join(tmpdir, f"output.{ext}")
return {'file': outfile}
class TaskContoursGenerate(TaskView):
def post(self, request, pk=None):
task = self.get_and_check_task(request, pk)
@ -23,36 +93,24 @@ class TaskContoursGenerate(TaskView):
elif layer == 'DTM':
dem = os.path.abspath(task.get_asset_download_path("dtm.tif"))
else:
raise GrassEngineException('{} is not a valid layer.'.format(layer))
raise ContoursException('{} is not a valid layer.'.format(layer))
context = grass.create_context({'auto_cleanup' : False})
epsg = int(request.data.get('epsg', '3857'))
interval = float(request.data.get('interval', 1))
format = request.data.get('format', 'GPKG')
supported_formats = ['GPKG', 'ESRI Shapefile', 'DXF', 'GeoJSON']
if not format in supported_formats:
raise GrassEngineException("Invalid format {} (must be one of: {})".format(format, ",".join(supported_formats)))
raise ContoursException("Invalid format {} (must be one of: {})".format(format, ",".join(supported_formats)))
simplify = float(request.data.get('simplify', 0.01))
context.add_param('dem_file', dem)
context.add_param('interval', interval)
context.add_param('format', format)
context.add_param('simplify', simplify)
context.add_param('epsg', epsg)
context.set_location(dem)
celery_task_id = execute_grass_script.delay(os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"calc_contours.py"
), context.serialize(), 'file').task_id
celery_task_id = run_function_async(calc_contours, dem, epsg, interval, format, simplify).task_id
return Response({'celery_task_id': celery_task_id}, status=status.HTTP_200_OK)
except GrassEngineException as e:
except ContoursException as e:
return Response({'error': str(e)}, status=status.HTTP_200_OK)
class TaskContoursCheck(CheckTask):
def on_error(self, result):
cleanup_grass_context(result['context'])
pass
def error_check(self, result):
contours_file = result.get('file')

Wyświetl plik

@ -1,93 +0,0 @@
#%module
#% description: Calculate contours
#%end
#%option
#% key: dem_file
#% type: string
#% required: yes
#% multiple: no
#% description: GeoTIFF DEM containing the surface to calculate contours
#%end
#%option
#% key: interval
#% type: double
#% required: yes
#% multiple: no
#% description: Contours interval
#%end
#%option
#% key: format
#% type: string
#% required: yes
#% multiple: no
#% description: OGR output format
#%end
#%option
#% key: simplify
#% type: double
#% required: yes
#% multiple: no
#% description: OGR output format
#%end
#%option
#% key: epsg
#% type: string
#% required: yes
#% multiple: no
#% description: target EPSG code
#%end
# output: If successful, prints the full path to the contours file. Otherwise it prints "error"
import sys
import glob
import os
import shutil
from grass.pygrass.modules import Module
import grass.script as grass
import subprocess
def main():
ext = ""
if opts['format'] == "GeoJSON":
ext = "json"
elif opts['format'] == "GPKG":
ext = "gpkg"
elif opts['format'] == "DXF":
ext = "dxf"
elif opts['format'] == "ESRI Shapefile":
ext = "shp"
MIN_CONTOUR_LENGTH = 5
Module("r.external", input=opts['dem_file'], output="dem", overwrite=True)
Module("g.region", raster="dem")
Module("r.contour", input="dem", output="contours", step=opts["interval"], overwrite=True)
Module("v.generalize", input="contours", output="contours_smooth", method="douglas", threshold=opts["simplify"], overwrite=True)
Module("v.generalize", input="contours_smooth", output="contours_simplified", method="chaiken", threshold=1, overwrite=True)
Module("v.generalize", input="contours_simplified", output="contours_final", method="douglas", threshold=opts["simplify"], overwrite=True)
Module("v.edit", map="contours_final", tool="delete", threshold=[-1,0,-MIN_CONTOUR_LENGTH], query="length")
Module("v.out.ogr", input="contours_final", output="temp.gpkg", format="GPKG")
subprocess.check_call(["ogr2ogr", "-t_srs", "EPSG:%s" % opts['epsg'],
'-overwrite', '-f', opts["format"], "output.%s" % ext, "temp.gpkg"], stdout=subprocess.DEVNULL)
if os.path.isfile("output.%s" % ext):
if opts["format"] == "ESRI Shapefile":
ext="zip"
os.makedirs("contours")
contour_files = glob.glob("output.*")
for cf in contour_files:
shutil.move(cf, os.path.join("contours", os.path.basename(cf)))
shutil.make_archive('output', 'zip', 'contours/')
print(os.path.join(os.getcwd(), "output.%s" % ext))
else:
print("error")
return 0
if __name__ == "__main__":
opts, _ = grass.parser()
sys.exit(main())

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "WebODM",
"version": "2.1.2",
"version": "2.1.3",
"description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js",
"scripts": {

Wyświetl plik

@ -175,7 +175,6 @@ def execute_grass_script(script, serialized_context = {}, out_key='output'):
logger.error(str(e))
return {'error': str(e), 'context': ctx.serialize()}
@app.task(bind=True)
def export_raster(self, input, **opts):
try:
@ -238,4 +237,3 @@ def check_quotas():
break
else:
p.clear_quota_deadline()