From b46163ff9322674db0e56ee4ddb76948d4f3fe3e Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Sat, 30 Mar 2019 19:07:24 -0400 Subject: [PATCH] Contours GRASS/GDAL script working, async execution, grass engine changes, auto cleanup flag, contours UI --- app/plugins/grass_engine.py | 20 +- app/tests/test_plugins.py | 7 +- package.json | 2 +- plugins/contours/api.py | 72 ++++++ plugins/contours/calc_contours.grass | 38 +++ plugins/contours/manifest.json | 2 +- plugins/contours/plugin.py | 12 +- plugins/contours/public/Contours.jsx | 10 +- plugins/contours/public/Contours.scss | 2 + plugins/contours/public/ContoursPanel.jsx | 276 +++++++++++++-------- plugins/contours/public/ContoursPanel.scss | 9 +- plugins/contours/public/icon.png | Bin 1052 -> 0 bytes plugins/contours/public/icon.svg | 22 +- plugins/contours/public/icon@2x.png | Bin 2127 -> 0 bytes plugins/contours/public/main.js | 10 +- plugins/measure/api.py | 7 +- plugins/measure/manifest.json | 2 +- worker/tasks.py | 2 +- 18 files changed, 359 insertions(+), 134 deletions(-) create mode 100644 plugins/contours/api.py create mode 100755 plugins/contours/calc_contours.grass delete mode 100644 plugins/contours/public/icon.png delete mode 100644 plugins/contours/public/icon@2x.png diff --git a/app/plugins/grass_engine.py b/app/plugins/grass_engine.py index 81aaa87e..94aa080d 100644 --- a/app/plugins/grass_engine.py +++ b/app/plugins/grass_engine.py @@ -28,13 +28,14 @@ class GrassEngine: class GrassContext: - def __init__(self, grass_binary, tmpdir = None, template_args = {}, location = None): + def __init__(self, grass_binary, tmpdir = None, template_args = {}, location = None, auto_cleanup=True): self.grass_binary = grass_binary if tmpdir is None: tmpdir = os.path.basename(tempfile.mkdtemp('_grass_engine', dir=settings.MEDIA_TMP)) self.tmpdir = tmpdir self.template_args = template_args self.location = location + self.auto_cleanup = auto_cleanup def get_cwd(self): return os.path.join(settings.MEDIA_TMP, self.tmpdir) @@ -82,6 +83,9 @@ class GrassContext: tmpl = Template(script_content) # Write script to disk + if not os.path.exists(self.get_cwd()): + os.mkdir(self.get_cwd()) + with open(os.path.join(self.get_cwd(), 'script.sh'), 'w') as f: f.write(tmpl.substitute(self.template_args)) @@ -94,6 +98,9 @@ class GrassContext: out = out.decode('utf-8').strip() err = err.decode('utf-8').strip() + logger.info("GOT!") + logger.info(out) + if p.returncode == 0: return out else: @@ -103,15 +110,18 @@ class GrassContext: return { 'tmpdir': self.tmpdir, 'template_args': self.template_args, - 'location': self.location + 'location': self.location, + 'auto_cleanup': self.auto_cleanup } - def __del__(self): - pass - # Cleanup + def cleanup(self): if os.path.exists(self.get_cwd()): shutil.rmtree(self.get_cwd()) + def __del__(self): + if self.auto_cleanup: + self.cleanup() + class GrassEngineException(Exception): pass diff --git a/app/tests/test_plugins.py b/app/tests/test_plugins.py index fa7ae981..90bb1e39 100644 --- a/app/tests/test_plugins.py +++ b/app/tests/test_plugins.py @@ -113,11 +113,13 @@ class TestPlugins(BootTestCase): }""") ctx.set_location("EPSG:4326") - output = execute_grass_script.delay( + result = execute_grass_script.delay( os.path.join(grass_scripts_dir, "simple_test.grass"), ctx.serialize() ).get() - self.assertTrue("Number of points: 1" in output) + self.assertTrue("Number of points: 1" in result.get('output')) + + self.assertTrue(result.get('context') == ctx.serialize()) error = execute_grass_script.delay( os.path.join(grass_scripts_dir, "nonexistant_script.grass"), @@ -129,6 +131,7 @@ class TestPlugins(BootTestCase): with self.assertRaises(GrassEngineException): ctx.execute(os.path.join(grass_scripts_dir, "nonexistant_script.grass")) + # TODO: verify autocleanup works def test_plugin_datastore(self): test_plugin = get_plugin_by_name("test") diff --git a/package.json b/package.json index 383d6185..e1f655fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "WebODM", - "version": "0.8.2", + "version": "0.9.0", "description": "Open Source Drone Image Processing", "main": "index.js", "scripts": { diff --git a/plugins/contours/api.py b/plugins/contours/api.py new file mode 100644 index 00000000..82eefd7b --- /dev/null +++ b/plugins/contours/api.py @@ -0,0 +1,72 @@ +import os + +from rest_framework import status +from rest_framework.response import Response +from app.plugins.views import TaskView +from worker.tasks import execute_grass_script +from app.plugins.grass_engine import grass, GrassEngineException +from worker.celery import app as celery + +class TaskContoursGenerate(TaskView): + def post(self, request, pk=None): + task = self.get_and_check_task(request, pk) + + layer = request.data.get('layer', None) + if layer == 'DSM' and task.dsm_extent is None: + return Response({'error': 'No DSM layer is available.'}) + elif layer == 'DTM' and task.dtm_extent is None: + return Response({'error': 'No DTM layer is available.'}) + + try: + if layer == 'DSM': + dem = os.path.abspath(task.get_asset_download_path("dsm.tif")) + elif layer == 'DTM': + dem = os.path.abspath(task.get_asset_download_path("dtm.tif")) + else: + raise GrassEngineException('{} 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))) + 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('epsg:' + str(epsg)) + + celery_task_id = execute_grass_script.delay(os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "calc_contours.grass" + ), context.serialize()).task_id + + return Response({'celery_task_id': celery_task_id}, status=status.HTTP_200_OK) + except GrassEngineException as e: + return Response({'error': str(e)}, status=status.HTTP_200_OK) + +class TaskContoursCheck(TaskView): + def get(self, request, pk=None, celery_task_id=None): + task = self.get_and_check_task(request, pk) + + # res = celery.AsyncResult(celery_task_id) + # res.wait() + # print(res.get()) + + #while not res.ready(): + + #if isinstance(output, dict) and 'error' in output: raise GrassEngineException(output['error']) + + # if isinstance(output, dict) and 'error' in output: raise GrassEngineException(output['error']) + # + # cols = output.split(':') + # if len(cols) == 7: + # return Response({'volume': str(abs(float(cols[6])))}, status=status.HTTP_200_OK) + # else: + # raise GrassEngineException(output) + diff --git a/plugins/contours/calc_contours.grass b/plugins/contours/calc_contours.grass new file mode 100755 index 00000000..21c0a655 --- /dev/null +++ b/plugins/contours/calc_contours.grass @@ -0,0 +1,38 @@ +# dem_file: GeoTIFF DEM containing the surface to calculate contours +# interval: Contours interval +# format: OGR output format +# simplify: Simplify value +# epsg: target EPSG code +# destination: destination folder. If it does not exist, it will be created. +# +# ------ +# output: If successful, prints the full path to the contours file. Otherwise it prints "error" + +ext="" +if [ "${format}" = "GeoJSON" ]; then + ext="geojson" +elif [ "${format}" = "GPKG" ]; then + ext="gpkg" +elif [ "${format}" = "DXF" ]; then + ext="dxf" +elif [ "${format}" = "ESRI Shapefile" ]; then + ext="shp" +fi + +gdal_contour -i ${interval} -f GPKG "${dem_file}" contours.gpkg > /dev/null +ogr2ogr -simplify ${simplify} -t_srs EPSG:${epsg} -overwrite -f "${format}" output.$$ext contours.gpkg > /dev/null + +if [ -e "output.$$ext" ]; then + # ESRI ShapeFile extra steps to compress into a zip archive + # we leverage Python's shutil in this case + if [ "${format}" = "ESRI Shapefile" ]; then + ext="zip" + mkdir contours/ + mv output* contours/ + echo "import shutil;shutil.make_archive('output', 'zip', 'contours/')" | python + fi + + echo "$$(pwd)/output.$$ext" +else + echo "error" +fi \ No newline at end of file diff --git a/plugins/contours/manifest.json b/plugins/contours/manifest.json index 16568b12..a47d2c59 100644 --- a/plugins/contours/manifest.json +++ b/plugins/contours/manifest.json @@ -1,6 +1,6 @@ { "name": "Contours", - "webodmMinVersion": "0.8.2", + "webodmMinVersion": "0.9.0", "description": "Compute, preview and export contours from DEMs", "version": "1.0.0", "author": "Piero Toffanin", diff --git a/plugins/contours/plugin.py b/plugins/contours/plugin.py index 5f0eb060..7d839d8e 100644 --- a/plugins/contours/plugin.py +++ b/plugins/contours/plugin.py @@ -1,8 +1,18 @@ from app.plugins import PluginBase +from app.plugins import MountPoint +from .api import TaskContoursGenerate +from .api import TaskContoursCheck + class Plugin(PluginBase): def include_js_files(self): return ['main.js'] def build_jsx_components(self): - return ['Contours.jsx'] \ No newline at end of file + return ['Contours.jsx'] + + def api_mount_points(self): + return [ + MountPoint('task/(?P[^/.]+)/contours/generate', TaskContoursGenerate.as_view()), + MountPoint('task/(?P[^/.]+)/contours/check/(?P.+)', TaskContoursCheck.as_view()), + ] \ No newline at end of file diff --git a/plugins/contours/public/Contours.jsx b/plugins/contours/public/Contours.jsx index 668d299f..8e88e3f7 100644 --- a/plugins/contours/public/Contours.jsx +++ b/plugins/contours/public/Contours.jsx @@ -1,10 +1,15 @@ import L from 'leaflet'; import ReactDOM from 'ReactDOM'; import React from 'react'; +import PropTypes from 'prop-types'; import './Contours.scss'; import ContoursPanel from './ContoursPanel'; class ContoursButton extends React.Component { + static propTypes = { + tasks: PropTypes.object.isRequired + } + constructor(props){ super(props); @@ -28,7 +33,7 @@ class ContoursButton extends React.Component { - + ); } } @@ -41,9 +46,8 @@ export default L.Control.extend({ onAdd: function (map) { var container = L.DomUtil.create('div', 'leaflet-control-contours leaflet-bar leaflet-control'); L.DomEvent.disableClickPropagation(container); - ReactDOM.render(, container); + ReactDOM.render(, container); - // this._map = map; return container; } }); diff --git a/plugins/contours/public/Contours.scss b/plugins/contours/public/Contours.scss index 22d2385c..f600c127 100644 --- a/plugins/contours/public/Contours.scss +++ b/plugins/contours/public/Contours.scss @@ -1,4 +1,6 @@ .leaflet-control-contours{ + z-index: 999; + a.leaflet-control-contours-button{ background: url(icon.svg) no-repeat 0 0; background-size: 26px 26px; diff --git a/plugins/contours/public/ContoursPanel.jsx b/plugins/contours/public/ContoursPanel.jsx index a684e632..810a4e6c 100644 --- a/plugins/contours/public/ContoursPanel.jsx +++ b/plugins/contours/public/ContoursPanel.jsx @@ -3,13 +3,15 @@ import PropTypes from 'prop-types'; import Storage from 'webodm/classes/Storage'; import L from 'leaflet'; import './ContoursPanel.scss'; +import ErrorMessage from 'webodm/components/ErrorMessage'; export default class ContoursPanel extends React.Component { static defaultProps = { - }; static propTypes = { - onClose: PropTypes.func.isRequired + onClose: PropTypes.func.isRequired, + tasks: PropTypes.object.isRequired, + isShowed: PropTypes.bool.isRequired } constructor(props){ @@ -17,37 +19,52 @@ export default class ContoursPanel extends React.Component { this.state = { error: "", + permanentError: "", interval: Storage.getItem("last_contours_interval") || "1", customInterval: Storage.getItem("last_contours_custom_interval") || "1", layer: "", - projection: Storage.getItem("last_contours_projection") || "4326", - customProjection: Storage.getItem("last_contours_custom_projection") || "4326", + epsg: Storage.getItem("last_contours_epsg") || "4326", + customEpsg: Storage.getItem("last_contours_custom_epsg") || "4326", + layers: [], + loading: true, + task: props.tasks[0] || null, + previewLoading: false, }; } - componentDidMount(){ + componentDidUpdate(){ + if (this.props.isShowed && this.state.loading){ + const {id, project} = this.state.task; + + this.loadingReq = $.getJSON(`/api/projects/${project}/tasks/${id}/`) + .done(res => { + const { available_assets } = res; + let layers = []; + + if (available_assets.indexOf("dsm.tif") !== -1) layers.push("DSM"); + if (available_assets.indexOf("dtm.tif") !== -1) layers.push("DTM"); + + if (layers.length > 0){ + this.setState({layers, layer: layers[0]}); + }else{ + this.setState({permanentError: "No DSM or DTM is available. To export contours, make sure to process a task with either the --dsm or --dtm option checked."}); + } + }) + .fail(() => { + this.setState({permanentError: `Cannot retrieve information for task ${id}. Are you are connected to the internet.`}) + }) + .always(() => { + this.setState({loading: false}); + this.loadingReq = null; + }); + } } componentWillUnmount(){ - } - - calculateVolume(){ - // $.ajax({ - // type: 'POST', - // url: `/api/plugins/measure/task/${task.id}/volume`, - // data: JSON.stringify({'area': this.props.resultFeature.toGeoJSON()}), - // contentType: "application/json" - // }).done(result => { - // if (result.volume){ - // this.setState({volume: parseFloat(result.volume)}); - // }else if (result.error){ - // this.setState({error: result.error}); - // }else{ - // this.setState({error: "Invalid response: " + result}); - // } - // }).fail(error => { - // this.setState({error}); - // }); + if (this.loadingReq){ + this.loadingReq.abort(); + this.loadingReq = null; + } } handleSelectInterval = e => { @@ -62,95 +79,150 @@ export default class ContoursPanel extends React.Component { this.setState({customInterval: e.target.value}); } - handleSelectProjection = e => { - this.setState({projection: e.target.value}); + handleSelectEpsg = e => { + this.setState({Epsg: e.target.value}); } - handleChangeCustomProjection = e => { - this.setState({customProjection: e.target.value}); + handleChangeCustomEpsg = e => { + this.setState({customEpsg: e.target.value}); + } + + getFormValues = () => { + const { interval, customInterval, epsg, customEpsg, layer } = this.state; + return { + interval: interval !== "custom" ? interval : customInterval, + epsg: epsg !== "custom" ? epsg : customEpsg, + layer + }; + } + + handleShowPreview = () => { + this.setState({previewLoading: true}); + + const data = this.getFormValues(); + data.interval = 1; + data.epsg = 3857; + data.format = "GeoJSON"; + data.simplify = 0.05; + + $.ajax({ + type: 'POST', + url: `/api/plugins/contours/task/${this.state.task.id}/contours/generate`, + data: data + }).done(result => { + if (result.celery_task_id){ + console.log(result); + }else if (result.error){ + this.setState({error: result.error}); + }else{ + this.setState({error: "Invalid response: " + result}); + } + + this.setState({previewLoading: false}); + }).fail(error => { + this.setState({previewLoading: false, error: JSON.stringify(error)}); + }); } render(){ - const { error, interval, customInterval, layer, - projection, customProjection } = this.state; + const { loading, task, layers, error, permanentError, interval, customInterval, layer, + epsg, customEpsg, + previewLoading } = this.state; const intervalValues = [0.25, 0.5, 1, 1.5, 2]; + const disabled = (interval === "custom" && !customInterval) || + (epsg === "custom" && !customEpsg); + + let content = ""; + if (loading) content = ( Loading...); + else if (error) content = (); + else if (permanentError) content = (
{permanentError}
); + else{ + content = (
+
+ +
+ +
+
+ {interval === "custom" ? +
+ +
+ meter +
+
+ : ""} + +
+ +
+ +
+
+ +
+ +
+ +
+
+ {epsg === "custom" ? +
+ +
+ +
+
+ : ""} + +
+ + + +
+
); + } + return (
Contours

- -
- -
- -
-
- {interval === "custom" ? -
- -
- meter -
-
- : ""} - -
- -
- -
-
- -
- -
- -
-
- {projection === "custom" ? -
- -
- -
-
- : ""} - -
- - - -
- + {content}
); } } \ No newline at end of file diff --git a/plugins/contours/public/ContoursPanel.scss b/plugins/contours/public/ContoursPanel.scss index 31346957..354f7cc1 100644 --- a/plugins/contours/public/ContoursPanel.scss +++ b/plugins/contours/public/ContoursPanel.scss @@ -2,6 +2,7 @@ padding: 6px 10px 6px 6px; background: #fff; min-width: 250px; + max-width: 300px; .close-button{ display: inline-block; @@ -57,9 +58,11 @@ .dropdown-menu{ a{ - display: inline; - padding-top: 8px; - padding-bottom: 8px; + width: 100%; + text-align: left; + display: block; + padding-top: 0; + padding-bottom: 0; } } diff --git a/plugins/contours/public/icon.png b/plugins/contours/public/icon.png deleted file mode 100644 index 21c9708c9656c56354d136a26fb434486ded0742..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1052 zcmV+%1mpXOP)ZaBT z@gp%c?olGFh)p1hXk4)+$^u;(R2pO4xM4$xHNhq>)Zj) zT3cJ2N~O{(s`@JM;08R`MC83%t#)B{cDA}{N5+_Efb&3mCYlkE(KJne$}$1iAtJrN z6F_Sw`&~qi1VQk{27!UK_LMOuHO8p5_8V*MAmFU?i({>Q!5H(cF-DCsskQcvb)6Vv zj%UWpl}hF01`g`sWy!2E|))=n3(vx5bPmf7`UaX`=clt18{SL=H})LK#!`vA4Sobye!6;x059KTtuD+ zdRf`G_H6!ik|f7G&vUEQ>em3(YW0Wa=H@owamR6X#c}-EYDOI$9qmbyOaiyc<+9Bq zG{&3-P82F|1GtvW_W-+BOC$1b5CpFSbar;O)M~W=XsFd{opW<@p#zYnX)fj8-JF5Cxf%{irEQ@%bh&)ip z;7k-nr!v0nNs|0Q;V`%W3kwUe=Xs9;dsVe5j^j%JaU73$p0@-%0+fOKvaEYVWP29* zKO%BE48zm8)5gZev%q5_a%p;c`h(Ss04kNrJ`wqzl?!_~48u=yT}w;LJ>_!wsN*_5ulB97zlnwpw!ipW9WfaiJ7d7k%YwOaiJ&~+TV=y~2Sa0ozEPexI6 zu4uI8(X`ecRMiiGotbD37**9DvpJ6Aw5n<^(8uj&I4L4WgCO{_p7V~4&-eXC5jiX( zL##YE>tS3~&pD1W90bA5wG1}Z%6;G8?YeG1D=$di(CHxCb WF=~r^TJl8z0000 + - + style="opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;stroke:#191919;stroke-width:1.33004403;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal" /> diff --git a/plugins/contours/public/icon@2x.png b/plugins/contours/public/icon@2x.png deleted file mode 100644 index c3cf42ab381987d7732b0e5aaf6ee97eb831a0e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2127 zcmV-V2(b5wP)+(eP;0 zG*KF<5+P0E5U@~0S`g3_fuev&A}Az^Xn2X@Qc#R2sI-9VCP+w#q>#|kM=B+R7E_=> za_ilDXKm~nSjSFwW)45Rc4ud2cE^wWLH_=o$36EuGxu@sy}%+Cv4}0Gi6`%dYE=wY0Ph_V@Q+y%zm0R6-<#SOZ`)Gv7=^s{vRw;xm98fC~U# z0PtI_^@Xy2enjTeSW5YJ0C#zw*F{8fo?P-o^e7Sa0DAx!b=*Aw8h{PpN?Df#kOm-Pwm)u~=7C%; zHyW2??1a+k^le1+XqiwL{tMs~fK!HHoLjbR*~?KAKby@qjE#-G)APKW0o)E?Gk`aR z`7*Nt&}G~9**H97B_xFSJb-TiFslL(onz()P1Aflm&;AWi5G-rS&fe4bP~~C0PhKL zaRKbtT7RcrthxzFDUUJp{t(m4%)HOG?cda=93Sa)`VJ!c7Jzqzn7^*I{!*>>RbSi> zA;cj7hXQJsh>mEjKUypn|2{jq^-(MqU+L)RI6g5kK>%(+i+Y6h@fYkeZDO=v=@clpkQ`-(hOm zpGKe7dPmT=Dj^}nIsi`vI=;uY?eEv6xA|ZH7VS z&p(k!e8}&U@C!t=!7qEB*Bcgi`SRr=up4|2UDeuAIU@_5jvDT&pQ*~Siw1%P@7*cP4ll2d~OXYi0FuI+Yd+hM?xv} zVO&&>S+Rfk`gNoeKDl}iCE_1P}h*c;v!R5*?kwG!4vsZ%5{X1`CA((#4GVxsZZip8x^(FZfJvXNQA*iV-2?#eN>GtVBtA1& zc8Nq{Z=5~=V8x0R%?OO7GIs#gbLdsS;CWs)g2UOMf|(CWDfdM9M?y+@7l5iG0TG=E zaR-2R1>^z0P>~Q31^0*bVG$k2>Bbw!>S>tF{39X6iPqNE8zacfKuWn<2yv2`dxAUw zOgfJ9U|2iTG%GkU^NW5t5ip(suya}_qE1YmR4Xt%JZuXgjsQ3u(&tvs^R`MUUjp!y z=Xw1A{#CXuHw+`o%(pW04Pk`KI6O2oR0uOL^G?4U{F4=vtF5g~I*v1l%4(si0Gu`n zA)WxRwFa@d;E8e-T~%&{6wa>WIF^3{uK3l1gM*5hf9=-LsX#(%pCAO24FXU_s2*A0G=VDhZ`Ci9vK)I zn2d5N54F?qm0LvAtCZRq^bJi&O4-iL7g2faeVvFlDy9BZk7ao-t_5%trta~sFPlb* zNCUWFn&u0GgM+GGtn!`tlc>B77&Q!IRX(53#Y(6g=uQBA0iXX6QKwSsR6QaK0U^X@ zOkLEZd}=15ZAz(=VQnI>v5Ljw-%_bm3BX35ItidFl}aVk>GU(Bqoctpc|nlLWRl5b z@-TphP+1j#hz=;FdZOCayq7B$i%+LgsU(1P{+IyPl}e?~=H}*s@$vCFzZ8Ukq?8}L zdiClFOl=3j5p+~*{k56_;`Abf_#A+5BK!lKXXb9hFn+0&x*R875apWj!_2%FQ}Zqe z7r;KP^>^#Vn$2bXdH_GG@eVW&;50LzHVotWrlzKTf6?eiCX-2ykB{HL%xj5g6M#(s znj_-onYqih?LWlfIjbW=dwctJrBdlAfKQ_Sa|koDCL$YeRPjrRNHB9cM&BM0{h*gi;QJi8OL$%CZaA(y{XOz zIRKBiuG=#-H1yB823fF2V=3io0GrC?$xWC_FltzO00seEAfo31oQ|_$$0D*2LBf#D zW*bIEMkEovm53SvfSD(ldBnEuJR)kgMJ!?wi&#W0{14Ii_huPn7uWy*002ovPDHLk FV1kF4*W3UA diff --git a/plugins/contours/public/main.js b/plugins/contours/public/main.js index f3b1f0c4..4b9b4c8f 100644 --- a/plugins/contours/public/main.js +++ b/plugins/contours/public/main.js @@ -2,5 +2,13 @@ PluginsAPI.Map.willAddControls([ 'contours/build/Contours.js', 'contours/build/Contours.css' ], function(args, Contours){ - args.map.addControl(new Contours()); + var tasks = []; + for (var i = 0; i < args.tiles.length; i++){ + tasks.push(args.tiles[i].meta.task); + } + + // TODO: add support for map view where multiple tasks are available? + if (tasks.length === 1){ + args.map.addControl(new Contours({tasks: tasks})); + } }); diff --git a/plugins/measure/api.py b/plugins/measure/api.py index afd8e459..30dfc707 100644 --- a/plugins/measure/api.py +++ b/plugins/measure/api.py @@ -36,12 +36,15 @@ class TaskVolume(TaskView): context.add_param('dsm_file', dsm) context.set_location(dsm) - output = execute_grass_script.delay(os.path.join( + result = execute_grass_script.delay(os.path.join( os.path.dirname(os.path.abspath(__file__)), "calc_volume.grass" ), context.serialize()).get() - if isinstance(output, dict) and 'error' in output: raise GrassEngineException(output['error']) + if not isinstance(result, dict): raise GrassEngineException("Unexpected output from GRASS (expected dict)") + if 'error' in result: raise GrassEngineException(result['error']) + + output = result.get('output', '') cols = output.split(':') if len(cols) == 7: return Response({'volume': str(abs(float(cols[6])))}, status=status.HTTP_200_OK) diff --git a/plugins/measure/manifest.json b/plugins/measure/manifest.json index 9f339e66..4dda9a11 100644 --- a/plugins/measure/manifest.json +++ b/plugins/measure/manifest.json @@ -1,6 +1,6 @@ { "name": "Volume/Area/Length Measurements", - "webodmMinVersion": "0.5.0", + "webodmMinVersion": "0.9.0", "description": "Compute volume, area and length measurements on Leaflet", "version": "1.0.0", "author": "Abdelkoddouss Izem, Piero Toffanin", diff --git a/worker/tasks.py b/worker/tasks.py index 19de34c2..410e2b97 100644 --- a/worker/tasks.py +++ b/worker/tasks.py @@ -86,6 +86,6 @@ def process_pending_tasks(): def execute_grass_script(script, serialized_context = {}): try: ctx = grass.create_context(serialized_context) - return ctx.execute(script) + return {'output': ctx.execute(script), 'context': ctx.serialize()} except GrassEngineException as e: return {'error': str(e)} \ No newline at end of file