kopia lustrzana https://github.com/OpenDroneMap/WebODM
Contours preview working
rodzic
b46163ff93
commit
b6c3a004c5
|
@ -125,4 +125,8 @@ class GrassContext:
|
||||||
class GrassEngineException(Exception):
|
class GrassEngineException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def cleanup_grass_context(serialized_context):
|
||||||
|
ctx = grass.create_context(serialized_context)
|
||||||
|
ctx.cleanup()
|
||||||
|
|
||||||
grass = GrassEngine()
|
grass = GrassEngine()
|
|
@ -120,8 +120,12 @@ class Map extends React.Component {
|
||||||
|
|
||||||
// For some reason, getLatLng is not defined for tileLayer?
|
// For some reason, getLatLng is not defined for tileLayer?
|
||||||
// We need this function if other code calls layer.openPopup()
|
// We need this function if other code calls layer.openPopup()
|
||||||
|
let self = this;
|
||||||
layer.getLatLng = function(){
|
layer.getLatLng = function(){
|
||||||
return this.options.bounds.getCenter();
|
let latlng = self.lastClickedLatLng ?
|
||||||
|
self.lastClickedLatLng :
|
||||||
|
this.options.bounds.getCenter();
|
||||||
|
return latlng;
|
||||||
};
|
};
|
||||||
|
|
||||||
var popup = L.DomUtil.create('div', 'infoWindow');
|
var popup = L.DomUtil.create('div', 'infoWindow');
|
||||||
|
@ -270,6 +274,7 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||||
// Find first tile layer at the selected coordinates
|
// Find first tile layer at the selected coordinates
|
||||||
for (let layer of this.imageryLayers){
|
for (let layer of this.imageryLayers){
|
||||||
if (layer._map && layer.options.bounds.contains(e.latlng)){
|
if (layer._map && layer.options.bounds.contains(e.latlng)){
|
||||||
|
this.lastClickedLatLng = this.map.mouseEventToLatLng(e.originalEvent);
|
||||||
this.updatePopupFor(layer);
|
this.updatePopupFor(layer);
|
||||||
layer.openPopup();
|
layer.openPopup();
|
||||||
break;
|
break;
|
||||||
|
@ -279,6 +284,8 @@ https://a.tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||||
// Load task assets links in popup
|
// Load task assets links in popup
|
||||||
if (e.popup && e.popup._source && e.popup._content){
|
if (e.popup && e.popup._source && e.popup._content){
|
||||||
const infoWindow = e.popup._content;
|
const infoWindow = e.popup._content;
|
||||||
|
if (typeof infoWindow === 'string') return;
|
||||||
|
|
||||||
const $assetLinks = $("ul.asset-links", infoWindow);
|
const $assetLinks = $("ul.asset-links", infoWindow);
|
||||||
|
|
||||||
if ($assetLinks.length > 0 && $assetLinks.hasClass('loading')){
|
if ($assetLinks.length > 0 && $assetLinks.hasClass('loading')){
|
||||||
|
|
|
@ -55,3 +55,6 @@ class TestWorker(BootTestCase):
|
||||||
self.assertFalse(Project.objects.filter(pk=project.id).exists())
|
self.assertFalse(Project.objects.filter(pk=project.id).exists())
|
||||||
|
|
||||||
pnserver.terminate()
|
pnserver.terminate()
|
||||||
|
|
||||||
|
# TODO: check tmp directory cleanup
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.http import FileResponse
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from wsgiref.util import FileWrapper
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from app.plugins.views import TaskView
|
from app.plugins.views import TaskView
|
||||||
from worker.tasks import execute_grass_script
|
from worker.tasks import execute_grass_script
|
||||||
from app.plugins.grass_engine import grass, GrassEngineException
|
from app.plugins.grass_engine import grass, GrassEngineException, cleanup_grass_context
|
||||||
from worker.celery import app as celery
|
from worker.celery import app as celery
|
||||||
|
|
||||||
class TaskContoursGenerate(TaskView):
|
class TaskContoursGenerate(TaskView):
|
||||||
|
@ -52,21 +56,47 @@ class TaskContoursGenerate(TaskView):
|
||||||
|
|
||||||
class TaskContoursCheck(TaskView):
|
class TaskContoursCheck(TaskView):
|
||||||
def get(self, request, pk=None, celery_task_id=None):
|
def get(self, request, pk=None, celery_task_id=None):
|
||||||
task = self.get_and_check_task(request, pk)
|
res = celery.AsyncResult(celery_task_id)
|
||||||
|
if not res.ready():
|
||||||
|
return Response({'ready': False}, status=status.HTTP_200_OK)
|
||||||
|
else:
|
||||||
|
result = res.get()
|
||||||
|
if result.get('error', None) is not None:
|
||||||
|
cleanup_grass_context(result['context'])
|
||||||
|
return Response({'ready': True, 'error': result['error']})
|
||||||
|
|
||||||
# res = celery.AsyncResult(celery_task_id)
|
contours_file = result.get('output')
|
||||||
# res.wait()
|
if not contours_file:
|
||||||
# print(res.get())
|
cleanup_grass_context(result['context'])
|
||||||
|
return Response({'ready': True, 'error': 'No contours file was generated. This could be a bug.'})
|
||||||
|
|
||||||
#while not res.ready():
|
request.session['contours_' + celery_task_id] = contours_file
|
||||||
|
return Response({'ready': True})
|
||||||
|
|
||||||
#if isinstance(output, dict) and 'error' in output: raise GrassEngineException(output['error'])
|
|
||||||
|
|
||||||
# if isinstance(output, dict) and 'error' in output: raise GrassEngineException(output['error'])
|
class TaskContoursDownload(TaskView):
|
||||||
#
|
def get(self, request, pk=None, celery_task_id=None):
|
||||||
# cols = output.split(':')
|
contours_file = request.session.get('contours_' + celery_task_id, None)
|
||||||
# if len(cols) == 7:
|
|
||||||
# return Response({'volume': str(abs(float(cols[6])))}, status=status.HTTP_200_OK)
|
|
||||||
# else:
|
|
||||||
# raise GrassEngineException(output)
|
|
||||||
|
|
||||||
|
if contours_file is not None:
|
||||||
|
filename = os.path.basename(contours_file)
|
||||||
|
filesize = os.stat(contours_file).st_size
|
||||||
|
|
||||||
|
f = open(contours_file, "rb")
|
||||||
|
|
||||||
|
# More than 100mb, normal http response, otherwise stream
|
||||||
|
# Django docs say to avoid streaming when possible
|
||||||
|
stream = filesize > 1e8
|
||||||
|
if stream:
|
||||||
|
response = FileResponse(f)
|
||||||
|
else:
|
||||||
|
response = HttpResponse(FileWrapper(f),
|
||||||
|
content_type=(mimetypes.guess_type(filename)[0] or "application/zip"))
|
||||||
|
|
||||||
|
response['Content-Type'] = mimetypes.guess_type(filename)[0] or "application/zip"
|
||||||
|
response['Content-Disposition'] = "inline; filename={}".format(filename)
|
||||||
|
response['Content-Length'] = filesize
|
||||||
|
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
return Response({'error': 'Invalid contours download id'})
|
||||||
|
|
|
@ -19,8 +19,8 @@ elif [ "${format}" = "ESRI Shapefile" ]; then
|
||||||
ext="shp"
|
ext="shp"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
gdal_contour -i ${interval} -f GPKG "${dem_file}" contours.gpkg > /dev/null
|
gdal_contour -a elevation -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
|
ogr2ogr -dialect SQLite -where "ST_Length(geom) > 4" -simplify ${simplify} -t_srs EPSG:${epsg} -overwrite -f "${format}" output.$$ext contours.gpkg > /dev/null
|
||||||
|
|
||||||
if [ -e "output.$$ext" ]; then
|
if [ -e "output.$$ext" ]; then
|
||||||
# ESRI ShapeFile extra steps to compress into a zip archive
|
# ESRI ShapeFile extra steps to compress into a zip archive
|
||||||
|
|
|
@ -2,6 +2,7 @@ from app.plugins import PluginBase
|
||||||
from app.plugins import MountPoint
|
from app.plugins import MountPoint
|
||||||
from .api import TaskContoursGenerate
|
from .api import TaskContoursGenerate
|
||||||
from .api import TaskContoursCheck
|
from .api import TaskContoursCheck
|
||||||
|
from .api import TaskContoursDownload
|
||||||
|
|
||||||
|
|
||||||
class Plugin(PluginBase):
|
class Plugin(PluginBase):
|
||||||
|
@ -15,4 +16,5 @@ class Plugin(PluginBase):
|
||||||
return [
|
return [
|
||||||
MountPoint('task/(?P<pk>[^/.]+)/contours/generate', TaskContoursGenerate.as_view()),
|
MountPoint('task/(?P<pk>[^/.]+)/contours/generate', TaskContoursGenerate.as_view()),
|
||||||
MountPoint('task/(?P<pk>[^/.]+)/contours/check/(?P<celery_task_id>.+)', TaskContoursCheck.as_view()),
|
MountPoint('task/(?P<pk>[^/.]+)/contours/check/(?P<celery_task_id>.+)', TaskContoursCheck.as_view()),
|
||||||
|
MountPoint('task/(?P<pk>[^/.]+)/contours/download/(?P<celery_task_id>.+)', TaskContoursDownload.as_view()),
|
||||||
]
|
]
|
|
@ -7,7 +7,8 @@ import ContoursPanel from './ContoursPanel';
|
||||||
|
|
||||||
class ContoursButton extends React.Component {
|
class ContoursButton extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tasks: PropTypes.object.isRequired
|
tasks: PropTypes.object.isRequired,
|
||||||
|
map: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -33,7 +34,7 @@ class ContoursButton extends React.Component {
|
||||||
<a href="javascript:void(0);"
|
<a href="javascript:void(0);"
|
||||||
onClick={this.handleOpen}
|
onClick={this.handleOpen}
|
||||||
className="leaflet-control-contours-button leaflet-bar-part theme-secondary"></a>
|
className="leaflet-control-contours-button leaflet-bar-part theme-secondary"></a>
|
||||||
<ContoursPanel isShowed={showPanel} tasks={this.props.tasks} onClose={this.handleClose} />
|
<ContoursPanel map={this.props.map} isShowed={showPanel} tasks={this.props.tasks} onClose={this.handleClose} />
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +47,7 @@ export default L.Control.extend({
|
||||||
onAdd: function (map) {
|
onAdd: function (map) {
|
||||||
var container = L.DomUtil.create('div', 'leaflet-control-contours leaflet-bar leaflet-control');
|
var container = L.DomUtil.create('div', 'leaflet-control-contours leaflet-bar leaflet-control');
|
||||||
L.DomEvent.disableClickPropagation(container);
|
L.DomEvent.disableClickPropagation(container);
|
||||||
ReactDOM.render(<ContoursButton tasks={this.options.tasks} />, container);
|
ReactDOM.render(<ContoursButton map={this.options.map} tasks={this.options.tasks} />, container);
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,8 @@ export default class ContoursPanel extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
tasks: PropTypes.object.isRequired,
|
tasks: PropTypes.object.isRequired,
|
||||||
isShowed: PropTypes.bool.isRequired
|
isShowed: PropTypes.bool.isRequired,
|
||||||
|
map: PropTypes.object.isRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -65,6 +66,10 @@ export default class ContoursPanel extends React.Component {
|
||||||
this.loadingReq.abort();
|
this.loadingReq.abort();
|
||||||
this.loadingReq = null;
|
this.loadingReq = null;
|
||||||
}
|
}
|
||||||
|
if (this.generateReq){
|
||||||
|
this.generateReq.abort();
|
||||||
|
this.generateReq = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectInterval = e => {
|
handleSelectInterval = e => {
|
||||||
|
@ -80,7 +85,7 @@ export default class ContoursPanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSelectEpsg = e => {
|
handleSelectEpsg = e => {
|
||||||
this.setState({Epsg: e.target.value});
|
this.setState({epsg: e.target.value});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChangeCustomEpsg = e => {
|
handleChangeCustomEpsg = e => {
|
||||||
|
@ -96,29 +101,100 @@ export default class ContoursPanel extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitForCompletion = (taskId, celery_task_id, cb) => {
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
const check = () => {
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: `/api/plugins/contours/task/${taskId}/contours/check/${celery_task_id}`
|
||||||
|
}).done(result => {
|
||||||
|
if (result.error){
|
||||||
|
cb(result.error);
|
||||||
|
}else if (result.ready){
|
||||||
|
cb();
|
||||||
|
}else{
|
||||||
|
// Retry
|
||||||
|
setTimeout(() => check(), 2000);
|
||||||
|
}
|
||||||
|
}).fail(error => {
|
||||||
|
console.warn(error);
|
||||||
|
if (errorCount++ < 10) setTimeout(() => check(), 2000);
|
||||||
|
else cb(JSON.stringify(error));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
check();
|
||||||
|
}
|
||||||
|
|
||||||
|
addGeoJSONFromURL = (url, cb) => {
|
||||||
|
const { map } = this.props;
|
||||||
|
|
||||||
|
$.getJSON(url)
|
||||||
|
.done((geojson) => {
|
||||||
|
try{
|
||||||
|
if (this.previewLayer){
|
||||||
|
map.removeLayer(this.previewLayer);
|
||||||
|
this.previewLayer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previewLayer = L.geoJSON(geojson, {
|
||||||
|
onEachFeature: (feature, layer) => {
|
||||||
|
if (feature.properties && feature.properties.elevation !== undefined) {
|
||||||
|
layer.bindPopup(`<b>Elevation:</b> ${feature.properties.elevation} meters`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: feature => {
|
||||||
|
// TODO: different colors for different elevations?
|
||||||
|
return {color: "yellow"};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.previewLayer.addTo(map);
|
||||||
|
|
||||||
|
cb();
|
||||||
|
}catch(e){
|
||||||
|
cb(e.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(cb);
|
||||||
|
}
|
||||||
|
|
||||||
handleShowPreview = () => {
|
handleShowPreview = () => {
|
||||||
this.setState({previewLoading: true});
|
this.setState({previewLoading: true});
|
||||||
|
|
||||||
const data = this.getFormValues();
|
const data = this.getFormValues();
|
||||||
data.interval = 1;
|
data.epsg = 4326;
|
||||||
data.epsg = 3857;
|
|
||||||
data.format = "GeoJSON";
|
data.format = "GeoJSON";
|
||||||
data.simplify = 0.05;
|
data.simplify = 0.05;
|
||||||
|
const taskId = this.state.task.id;
|
||||||
|
|
||||||
$.ajax({
|
this.generateReq = $.ajax({
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
url: `/api/plugins/contours/task/${this.state.task.id}/contours/generate`,
|
url: `/api/plugins/contours/task/${taskId}/contours/generate`,
|
||||||
data: data
|
data: data
|
||||||
}).done(result => {
|
}).done(result => {
|
||||||
if (result.celery_task_id){
|
if (result.celery_task_id){
|
||||||
console.log(result);
|
this.waitForCompletion(taskId, result.celery_task_id, error => {
|
||||||
}else if (result.error){
|
if (error) this.setState({previewLoading: false, error});
|
||||||
this.setState({error: result.error});
|
else{
|
||||||
}else{
|
const fileUrl = `/api/plugins/contours/task/${taskId}/contours/download/${result.celery_task_id}`;
|
||||||
this.setState({error: "Invalid response: " + result});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({previewLoading: false});
|
// Preview
|
||||||
|
this.addGeoJSONFromURL(fileUrl, e => {
|
||||||
|
if (e) this.setState({error: JSON.stringify(e)});
|
||||||
|
this.setState({previewLoading: false});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download
|
||||||
|
// location.href = ;
|
||||||
|
// this.setState({previewLoading: false});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}else if (result.error){
|
||||||
|
this.setState({previewLoading: false, error: result.error});
|
||||||
|
}else{
|
||||||
|
this.setState({previewLoading: false, error: "Invalid response: " + result});
|
||||||
|
}
|
||||||
}).fail(error => {
|
}).fail(error => {
|
||||||
this.setState({previewLoading: false, error: JSON.stringify(error)});
|
this.setState({previewLoading: false, error: JSON.stringify(error)});
|
||||||
});
|
});
|
||||||
|
@ -167,7 +243,7 @@ export default class ContoursPanel extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row form-group form-inline">
|
<div className="row form-group form-inline">
|
||||||
<label className="col-sm-3 control-label">Epsg:</label>
|
<label className="col-sm-3 control-label">Projection:</label>
|
||||||
<div className="col-sm-9 ">
|
<div className="col-sm-9 ">
|
||||||
<select className="form-control" value={epsg} onChange={this.handleSelectEpsg}>
|
<select className="form-control" value={epsg} onChange={this.handleSelectEpsg}>
|
||||||
<option value="4326">WGS84 (EPSG:4326)</option>
|
<option value="4326">WGS84 (EPSG:4326)</option>
|
||||||
|
|
|
@ -9,6 +9,6 @@ PluginsAPI.Map.willAddControls([
|
||||||
|
|
||||||
// TODO: add support for map view where multiple tasks are available?
|
// TODO: add support for map view where multiple tasks are available?
|
||||||
if (tasks.length === 1){
|
if (tasks.length === 1){
|
||||||
args.map.addControl(new Contours({tasks: tasks}));
|
args.map.addControl(new Contours({map: args.map, tasks: tasks}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,6 +23,14 @@ app.conf.beat_schedule = {
|
||||||
'retry': False
|
'retry': False
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'cleanup-tmp-directory': {
|
||||||
|
'task': 'worker.tasks.cleanup_tmp_directory',
|
||||||
|
'schedule': 3600,
|
||||||
|
'options': {
|
||||||
|
'expires': 1799,
|
||||||
|
'retry': False
|
||||||
|
}
|
||||||
|
},
|
||||||
'process-pending-tasks': {
|
'process-pending-tasks': {
|
||||||
'task': 'worker.tasks.process_pending_tasks',
|
'task': 'worker.tasks.process_pending_tasks',
|
||||||
'schedule': 5,
|
'schedule': 5,
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
import time
|
||||||
from celery.utils.log import get_task_logger
|
from celery.utils.log import get_task_logger
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
@ -35,6 +38,26 @@ def cleanup_projects():
|
||||||
logger.info("Deleted {} projects".format(count_dict['app.Project']))
|
logger.info("Deleted {} projects".format(count_dict['app.Project']))
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def cleanup_tmp_directory():
|
||||||
|
# Delete files and folder in the tmp directory that are
|
||||||
|
# older than 24 hours
|
||||||
|
tmpdir = settings.MEDIA_TMP
|
||||||
|
time_limit = 60 * 60 * 24
|
||||||
|
|
||||||
|
for f in os.listdir(tmpdir):
|
||||||
|
now = time.time()
|
||||||
|
filepath = os.path.join(tmpdir, f)
|
||||||
|
modified = os.stat(filepath).st_mtime
|
||||||
|
if modified < now - time_limit:
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(filepath, ignore_errors=True)
|
||||||
|
|
||||||
|
logger.info('Cleaned up: %s (%s)' % (f, modified))
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def process_task(taskId):
|
def process_task(taskId):
|
||||||
try:
|
try:
|
||||||
|
|
Ładowanie…
Reference in New Issue