kopia lustrzana https://github.com/OpenDroneMap/WebODM
Download assets buttons working
rodzic
8eb884d7e7
commit
51ddd64b0a
|
@ -91,7 +91,8 @@ You may also need to set the environment variable PROJSO to the .so or .dll proj
|
|||
- [X] User Registration / Authentication
|
||||
- [X] UI mockup
|
||||
- [X] Task Processing
|
||||
- [ ] Model display (using Cesium/Leaflet) for both 2D and 3D outputs.
|
||||
- [X] 2D Map Display
|
||||
- [ ] 3D model display
|
||||
- [X] Cluster management and setup.
|
||||
- [ ] Mission Planner
|
||||
- [X] API
|
||||
|
@ -112,8 +113,3 @@ You may also need to set the environment variable PROJSO to the .so or .dll proj
|
|||

|
||||
|
||||

|
||||
|
||||
|
||||
## Work in progress
|
||||
|
||||
We will add more information to this document soon.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import mimetypes
|
||||
import os
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
@ -102,6 +103,7 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
output = task.console_output or ""
|
||||
return Response('\n'.join(output.split('\n')[line_num:]))
|
||||
|
||||
|
||||
def list(self, request, project_pk=None):
|
||||
get_and_check_project(request, project_pk)
|
||||
tasks = self.queryset.filter(project=project_pk)
|
||||
|
@ -155,7 +157,7 @@ class TaskViewSet(viewsets.ViewSet):
|
|||
return self.update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class TaskTilesBase(APIView):
|
||||
class TaskNestedView(APIView):
|
||||
queryset = models.Task.objects.all()
|
||||
|
||||
def get_and_check_task(self, request, pk, project_pk, defer=(None, )):
|
||||
|
@ -167,7 +169,7 @@ class TaskTilesBase(APIView):
|
|||
return task
|
||||
|
||||
|
||||
class TaskTiles(TaskTilesBase):
|
||||
class TaskTiles(TaskNestedView):
|
||||
def get(self, request, pk=None, project_pk=None, z="", x="", y=""):
|
||||
"""
|
||||
Returns a prerendered orthophoto tile for a task
|
||||
|
@ -181,7 +183,7 @@ class TaskTiles(TaskTilesBase):
|
|||
raise exceptions.NotFound()
|
||||
|
||||
|
||||
class TaskTilesJson(TaskTilesBase):
|
||||
class TaskTilesJson(TaskNestedView):
|
||||
def get(self, request, pk=None, project_pk=None):
|
||||
"""
|
||||
Returns a tiles.json file for consumption by a client
|
||||
|
@ -199,4 +201,36 @@ class TaskTilesJson(TaskTilesBase):
|
|||
'maxzoom': 22,
|
||||
'bounds': task.orthophoto.extent
|
||||
}
|
||||
return Response(json)
|
||||
return Response(json)
|
||||
|
||||
|
||||
class TaskAssets(TaskNestedView):
|
||||
def get(self, request, pk=None, project_pk=None, asset=""):
|
||||
"""
|
||||
Downloads a task asset (if available)
|
||||
"""
|
||||
task = self.get_and_check_task(request, pk, project_pk, ('orthophoto', 'console_output'))
|
||||
|
||||
allowed_assets = {
|
||||
'all': 'all.zip',
|
||||
'geotiff': os.path.join('odm_orthophoto', 'odm_orthophoto.tif'),
|
||||
'las': os.path.join('odm_georeferencing', 'odm_georeferenced_model.ply.las'),
|
||||
'ply': os.path.join('odm_georeferencing', 'odm_georeferenced_model.ply'),
|
||||
'csv': os.path.join('odm_georeferencing', 'odm_georeferenced_model.csv')
|
||||
}
|
||||
|
||||
if asset in allowed_assets:
|
||||
asset_path = task.assets_path(allowed_assets[asset])
|
||||
|
||||
if not os.path.exists(asset_path):
|
||||
raise exceptions.NotFound("Asset does not exist")
|
||||
|
||||
asset_filename = os.path.basename(asset_path)
|
||||
|
||||
file = open(asset_path, "rb")
|
||||
response = HttpResponse(FileWrapper(file),
|
||||
content_type=(mimetypes.guess_type(asset_filename)[0] or "application/zip"))
|
||||
response['Content-Disposition'] = "attachment; filename={}".format(asset_filename)
|
||||
return response
|
||||
else:
|
||||
raise exceptions.NotFound()
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf.urls import url, include
|
||||
from .projects import ProjectViewSet
|
||||
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson
|
||||
from .tasks import TaskViewSet, TaskTiles, TaskTilesJson, TaskAssets
|
||||
from .processingnodes import ProcessingNodeViewSet
|
||||
from rest_framework_nested import routers
|
||||
|
||||
|
@ -17,6 +17,7 @@ urlpatterns = [
|
|||
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/tiles/(?P<z>[\d]+)/(?P<x>[\d]+)/(?P<y>[\d]+)\.png$', TaskTiles.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/tiles\.json$', TaskTilesJson.as_view()),
|
||||
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>[^/.]+)/$', TaskAssets.as_view()),
|
||||
|
||||
url(r'^auth/', include('rest_framework.urls')),
|
||||
]
|
|
@ -132,12 +132,15 @@ class Task(models.Model):
|
|||
self.full_clean()
|
||||
super(Task, self).save(*args, **kwargs)
|
||||
|
||||
def media_path(self, path):
|
||||
|
||||
def assets_path(self, *args):
|
||||
"""
|
||||
Get a path relative to the media directory of this task
|
||||
Get a path relative to the place where assets are stored
|
||||
"""
|
||||
return os.path.join(settings.MEDIA_ROOT,
|
||||
assets_directory_path(self.id, self.project.id, path))
|
||||
assets_directory_path(self.id, self.project.id, ""),
|
||||
"assets",
|
||||
*args)
|
||||
|
||||
@staticmethod
|
||||
def create_from_images(images, project):
|
||||
|
@ -276,30 +279,25 @@ class Task(models.Model):
|
|||
|
||||
if self.status == status_codes.COMPLETED:
|
||||
try:
|
||||
orthophoto_stream = self.processing_node.download_task_asset(self.uuid, "orthophoto.tif")
|
||||
orthophoto_path = self.media_path("orthophoto.tif")
|
||||
assets_dir = self.assets_path("")
|
||||
if not os.path.exists(assets_dir):
|
||||
os.makedirs(assets_dir)
|
||||
|
||||
# Save to disk original photo
|
||||
with open(orthophoto_path, 'wb') as fd:
|
||||
for chunk in orthophoto_stream.iter_content(4096):
|
||||
fd.write(chunk)
|
||||
|
||||
# Add to database another copy
|
||||
self.orthophoto = GDALRaster(orthophoto_path, write=True)
|
||||
|
||||
# Download tiles
|
||||
tiles_zip_stream = self.processing_node.download_task_asset(self.uuid, "orthophoto_tiles.zip")
|
||||
tiles_zip_path = self.media_path("orthophoto_tiles.zip")
|
||||
with open(tiles_zip_path, 'wb') as fd:
|
||||
for chunk in tiles_zip_stream.iter_content(4096):
|
||||
# Download all assets
|
||||
zip_stream = self.processing_node.download_task_asset(self.uuid, "all.zip")
|
||||
zip_path = os.path.join(assets_dir, "all.zip")
|
||||
with open(zip_path, 'wb') as fd:
|
||||
for chunk in zip_stream.iter_content(4096):
|
||||
fd.write(chunk)
|
||||
|
||||
# Extract from zip
|
||||
with zipfile.ZipFile(tiles_zip_path, "r") as zip_h:
|
||||
zip_h.extractall(self.media_path(""))
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_h:
|
||||
zip_h.extractall(assets_dir)
|
||||
|
||||
# Delete zip archive
|
||||
os.remove(tiles_zip_path)
|
||||
# Add to database orthophoto
|
||||
orthophoto_path = self.assets_path("odm_orthophoto", "odm_orthophoto.tif")
|
||||
if os.path.exists(orthophoto_path):
|
||||
self.orthophoto = GDALRaster(orthophoto_path, write=True)
|
||||
|
||||
self.save()
|
||||
except ProcessingException as e:
|
||||
|
@ -314,7 +312,7 @@ class Task(models.Model):
|
|||
self.set_failure(str(e))
|
||||
|
||||
def get_tile_path(self, z, x, y):
|
||||
return self.media_path(os.path.join("orthophoto_tiles", z, x, "{}.png".format(y)))
|
||||
return self.assets_path("orthophoto_tiles", z, x, "{}.png".format(y))
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
directory_to_delete = os.path.join(settings.MEDIA_ROOT,
|
||||
|
@ -342,8 +340,9 @@ class Task(models.Model):
|
|||
)
|
||||
|
||||
|
||||
def image_directory_path(imageUpload, filename):
|
||||
return assets_directory_path(imageUpload.task.id, imageUpload.task.project.id, filename)
|
||||
def image_directory_path(image_upload, filename):
|
||||
return assets_directory_path(image_upload.task.id, image_upload.task.project.id, filename)
|
||||
|
||||
|
||||
class ImageUpload(models.Model):
|
||||
task = models.ForeignKey(Task, on_delete=models.CASCADE, help_text="Task this image belongs to")
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import logging
|
||||
import traceback
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.schedulers import SchedulerAlreadyRunningError, SchedulerNotRunningError
|
||||
from threading import Thread, Lock
|
||||
|
|
|
@ -99,3 +99,10 @@ button i.glyphicon{
|
|||
.btn.btn-secondary{
|
||||
background-color: #dddddd;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a{
|
||||
padding-left: 10px;
|
||||
.fa{
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ class MapView extends React.Component {
|
|||
<div className="col-md-3">
|
||||
<AssetDownloadButtons task={{id: this.props.task, project: this.props.project}} />
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
<div className="col-md-9 text-right">
|
||||
Orthophoto opacity: <input type="range" step="1" value={opacity} onChange={this.updateOpacity} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -22,8 +22,9 @@ class AssetDownloadButtons extends React.Component {
|
|||
}
|
||||
|
||||
downloadAsset(type){
|
||||
return () => {
|
||||
location.href = ``;
|
||||
return (e) => {
|
||||
e.preventDefault();
|
||||
location.href = `/api/projects/${this.props.task.project}/tasks/${this.props.task.id}/download/${type}/`;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -39,6 +40,9 @@ class AssetDownloadButtons extends React.Component {
|
|||
<li><a href="javascript:void(0);" onClick={this.downloadAsset("geotiff")}><i className="fa fa-map-o"></i> GeoTIFF</a></li>
|
||||
<li><a href="javascript:void(0);" onClick={this.downloadAsset("las")}><i className="fa fa-cube"></i> LAS</a></li>
|
||||
<li><a href="javascript:void(0);" onClick={this.downloadAsset("ply")}><i className="fa fa-cube"></i> PLY</a></li>
|
||||
<li><a href="javascript:void(0);" onClick={this.downloadAsset("ply")}><i className="fa fa-cube"></i> CSV</a></li>
|
||||
<li className="divider"></li>
|
||||
<li><a href="javascript:void(0);" onClick={this.downloadAsset("all")}><i className="fa fa-file-archive-o"></i> All Assets</a></li>
|
||||
</ul>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
@ -82,6 +82,13 @@ class Map extends React.Component {
|
|||
|
||||
if (showBackground) {
|
||||
const basemaps = [
|
||||
L.tileLayer('//{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', {
|
||||
attribution: 'Map data: © Google Maps',
|
||||
subdomains: ['mt0','mt1','mt2','mt3'],
|
||||
maxZoom: 22,
|
||||
minZoom: 0,
|
||||
label: 'Google Maps Hybrid'
|
||||
}),
|
||||
L.tileLayer('//server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
||||
maxZoom: 22,
|
||||
|
@ -93,12 +100,6 @@ class Map extends React.Component {
|
|||
maxZoom: 22,
|
||||
minZoom: 0,
|
||||
label: 'OSM Mapnik' // optional label used for tooltip
|
||||
}),
|
||||
L.tileLayer('//{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
||||
attribution: 'Map data: © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: © <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>',
|
||||
maxZoom: 22,
|
||||
minZoom: 0,
|
||||
label: 'OpenTopoMap'
|
||||
})
|
||||
];
|
||||
|
||||
|
|
|
@ -189,7 +189,7 @@ class TaskListItem extends React.Component {
|
|||
};
|
||||
|
||||
if (task.status === statusCodes.COMPLETED){
|
||||
addActionButton(" View Orthophoto", "btn-primary first", "fa fa-globe", () => {
|
||||
addActionButton(" View Orthophoto", "btn-primary", "fa fa-globe", () => {
|
||||
location.href = `/map/project/${task.project}/task/${task.id}/`;
|
||||
});
|
||||
}
|
||||
|
@ -232,7 +232,6 @@ class TaskListItem extends React.Component {
|
|||
)
|
||||
})}
|
||||
</div>);
|
||||
|
||||
expanded = (
|
||||
<div className="expanded-panel">
|
||||
<div className="row">
|
||||
|
@ -240,12 +239,15 @@ class TaskListItem extends React.Component {
|
|||
<div className="labels">
|
||||
<strong>Created on: </strong> {(new Date(task.created_at)).toLocaleString()}<br/>
|
||||
</div>
|
||||
<div className="labels">
|
||||
<strong>Status: </strong> {status}<br/>
|
||||
</div>
|
||||
<div className="labels">
|
||||
<strong>Options: </strong> {this.optionsToList(task.options)}<br/>
|
||||
</div>
|
||||
{status ? <div className="labels">
|
||||
<strong>Status: </strong> {status}<br/>
|
||||
</div>
|
||||
: ""}
|
||||
{Array.isArray(task.options) ?
|
||||
<div className="labels">
|
||||
<strong>Options: </strong> {this.optionsToList(task.options)}<br/>
|
||||
</div>
|
||||
: ""}
|
||||
{/* TODO: List of images? */}
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
|
|
|
@ -62,9 +62,10 @@
|
|||
.action-buttons{
|
||||
button{
|
||||
margin-right: 4px;
|
||||
&.first{
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-download-buttons{
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,6 +185,7 @@ class TestApi(BootTestCase):
|
|||
# - task creation via file upload
|
||||
# - scheduler processing steps
|
||||
# - tiles API urls (permissions, 404s)
|
||||
# - assets download
|
||||
|
||||
def test_processingnodes(self):
|
||||
client = APIClient()
|
||||
|
|
Ładowanie…
Reference in New Issue