Download assets buttons working

pull/43/head
Piero Toffanin 2016-11-11 12:55:56 -05:00
rodzic 8eb884d7e7
commit 51ddd64b0a
12 zmienionych plików z 104 dodań i 56 usunięć

Wyświetl plik

@ -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
![image](https://cloud.githubusercontent.com/assets/1951843/17680196/9bfe878e-6304-11e6-852e-c09f1e02f3c0.png)
![er diagram - webodm 2](https://cloud.githubusercontent.com/assets/1951843/17717379/4a227e28-63d3-11e6-9518-6a63cc1bcd3b.png)
## Work in progress
We will add more information to this document soon.

Wyświetl plik

@ -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()

Wyświetl plik

@ -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')),
]

Wyświetl plik

@ -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")

Wyświetl plik

@ -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

Wyświetl plik

@ -99,3 +99,10 @@ button i.glyphicon{
.btn.btn-secondary{
background-color: #dddddd;
}
.dropdown-menu>li>a{
padding-left: 10px;
.fa{
margin-right: 4px;
}
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>);
}

Wyświetl plik

@ -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: &copy; 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 &copy; Esri &mdash; 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: &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: &copy; <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'
})
];

Wyświetl plik

@ -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">

Wyświetl plik

@ -62,9 +62,10 @@
.action-buttons{
button{
margin-right: 4px;
&.first{
margin-left: 4px;
}
}
.asset-download-buttons{
margin-right: 4px;
}
}
}

Wyświetl plik

@ -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()