pull/1615/head
Piero Toffanin 2025-02-18 20:00:19 +01:00
rodzic 5c26ff0742
commit 8570bf8e42
5 zmienionych plików z 135 dodań i 50 usunięć

Wyświetl plik

@ -4,6 +4,10 @@ import shutil
from wsgiref.util import FileWrapper
import mimetypes
import rasterio
from rasterio.enums import ColorInterp
from PIL import Image
import io
from shutil import copyfileobj, move
from django.core.exceptions import ObjectDoesNotExist, SuspiciousFileOperation, ValidationError
@ -235,7 +239,7 @@ class TaskViewSet(viewsets.ViewSet):
return Response({'success': True, 'task': TaskSerializer(new_task).data}, status=status.HTTP_200_OK)
else:
return Response({'error': _("Cannot duplicate task")}, status=status.HTTP_200_OK)
def create(self, request, project_pk=None):
project = get_and_check_project(request, project_pk, ('change_project', ))
@ -403,6 +407,65 @@ class TaskDownloads(TaskNestedView):
else:
return download_file_response(request, asset_fs, 'attachment', download_filename=download_filename)
class TaskThumbnail(TaskNestedView):
def get(self, request, pk=None, project_pk=None):
"""
Generate a thumbnail on the fly for a particular task
"""
task = self.get_and_check_task(request, pk)
orthophoto_path = task.get_check_file_asset_path("orthophoto.tif")
if orthophoto_path is None:
raise exceptions.NotFound()
try:
thumb_size = int(self.request.query_params.get('size', 512))
if thumb_size < 1 or thumb_size > 2048:
raise ValueError()
quality = int(self.request.query_params.get('quality', 75))
if quality < 0 or quality > 100:
raise ValueError()
except ValueError:
raise exceptions.ValidationError("Invalid query parameters")
with rasterio.open(orthophoto_path, "r") as raster:
ci = raster.colorinterp
indexes = (1, 2, 3,)
# More than 4 bands?
if len(ci) > 4:
# Try to find RGBA band order
if ColorInterp.red in ci and \
ColorInterp.green in ci and \
ColorInterp.blue in ci:
indexes = (ci.index(ColorInterp.red) + 1,
ci.index(ColorInterp.green) + 1,
ci.index(ColorInterp.blue) + 1,)
elif len(ci) < 3:
raise exceptions.NotFound()
if ColorInterp.alpha in ci:
indexes += (ci.index(ColorInterp.alpha) + 1, )
img = raster.read(indexes=indexes, boundless=True, fill_value=255, out_shape=(
len(indexes),
thumb_size,
thumb_size,
), resampling=rasterio.enums.Resampling.nearest).transpose((1, 2, 0))
img = Image.fromarray(img)
output = io.BytesIO()
img.save(output, format='PNG', quality=quality)
res = HttpResponse(content_type="image/png")
res['Content-Disposition'] = 'inline'
res.write(output.getvalue())
output.close()
return res
"""
Raw access to the task's asset folder resources
Useful when accessing a textured 3d model, or the Potree point cloud data

Wyświetl plik

@ -3,7 +3,7 @@ from django.conf.urls import url, include
from app.api.presets import PresetViewSet
from app.plugins.views import api_view_handler
from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskBackup, TaskAssetsImport
from .tasks import TaskViewSet, TaskDownloads, TaskThumbnail, TaskAssets, TaskBackup, TaskAssetsImport
from .imageuploads import Thumbnail, ImageDownload
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
from .admin import AdminUserViewSet, AdminGroupViewSet, AdminProfileViewSet
@ -46,6 +46,7 @@ urlpatterns = [
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/download/(?P<asset>.+)$', TaskDownloads.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/assets/(?P<unsafe_asset_path>.+)$', TaskAssets.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/import$', TaskAssetsImport.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/thumbnail$', TaskThumbnail.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/backup$', TaskBackup.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/thumbnail/(?P<image_filename>.+)$', Thumbnail.as_view()),
url(r'projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/images/download/(?P<image_filename>.+)$', ImageDownload.as_view()),

Wyświetl plik

@ -1260,6 +1260,11 @@ class Task(models.Model):
shutil.copy(alignment_file, dst_file)
else:
logger.warn("Cannot set alignment file for {}, {} does not exist".format(self, alignment_file))
def get_check_file_asset_path(self, asset):
file = self.assets_path(self.ASSETS_MAP[asset])
if isinstance(file, str) and os.path.isfile(file):
return file
def handle_images_upload(self, files):
uploaded = {}

Wyświetl plik

@ -171,6 +171,10 @@ class TaskListItem extends React.Component {
return `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/output/?line=${line}`;
}
thumbnailUrl = () => {
return `/api/projects/${this.state.task.project}/tasks/${this.state.task.id}/thumbnail?size=192`;
}
hoursMinutesSecs(t){
if (t === 0 || t === -1) return "-- : -- : --";
@ -556,54 +560,62 @@ class TaskListItem extends React.Component {
<div className="expanded-panel">
<div className="row">
<div className="col-md-12 no-padding">
<table className="table table-condensed info-table">
<tbody>
<tr>
<td><strong>{_("Created on:")}</strong></td>
<td>{(new Date(task.created_at)).toLocaleString()}</td>
</tr>
<tr>
<td><strong>{_("Processing Node:")}</strong></td>
<td>{task.processing_node_name || "-"} ({task.auto_processing_node ? _("auto") : _("manual")})</td>
</tr>
{Array.isArray(task.options) &&
<tr>
<td><strong>{_("Options:")}</strong></td>
<td>{this.optionsToList(task.options)}</td>
</tr>}
{stats && stats.gsd &&
<tr>
<td><strong>{_("Average GSD:")}</strong></td>
<td>{parseFloat(stats.gsd.toFixed(2)).toLocaleString()} cm</td>
</tr>}
{stats && stats.area &&
<tr>
<td><strong>{_("Area:")}</strong></td>
<td>{parseFloat(stats.area.toFixed(2)).toLocaleString()} m&sup2;</td>
</tr>}
{stats && stats.pointcloud && stats.pointcloud.points &&
<tr>
<td><strong>{_("Reconstructed Points:")}</strong></td>
<td>{stats.pointcloud.points.toLocaleString()}</td>
</tr>}
{task.size > 0 &&
<tr>
<td><strong>{_("Disk Usage:")}</strong></td>
<td>{Utils.bytesToSize(task.size * 1024 * 1024)}</td>
</tr>}
<tr>
<td><strong>{_("Task ID:")}</strong></td>
<td>{task.id}</td>
</tr>
<tr>
<td><strong>{_("Task Output:")}</strong></td>
<td><div className="btn-group btn-toggle">
<button onClick={this.setView("console")} className={"btn btn-xs " + (this.state.view === "basic" ? "btn-default" : "btn-primary")}>{_("On")}</button>
<button onClick={this.setView("basic")} className={"btn btn-xs " + (this.state.view === "console" ? "btn-default" : "btn-primary")}>{_("Off")}</button>
</div></td>
</tr>
</tbody>
</table>
<div className="col-md-9 col-sm-10 no-padding">
<table className="table table-condensed info-table">
<tbody>
<tr>
<td><strong>{_("Created on:")}</strong></td>
<td>{(new Date(task.created_at)).toLocaleString()}</td>
</tr>
<tr>
<td><strong>{_("Processing Node:")}</strong></td>
<td>{task.processing_node_name || "-"} ({task.auto_processing_node ? _("auto") : _("manual")})</td>
</tr>
{Array.isArray(task.options) &&
<tr>
<td><strong>{_("Options:")}</strong></td>
<td>{this.optionsToList(task.options)}</td>
</tr>}
{stats && stats.gsd &&
<tr>
<td><strong>{_("Average GSD:")}</strong></td>
<td>{parseFloat(stats.gsd.toFixed(2)).toLocaleString()} cm</td>
</tr>}
{stats && stats.area &&
<tr>
<td><strong>{_("Area:")}</strong></td>
<td>{parseFloat(stats.area.toFixed(2)).toLocaleString()} m&sup2;</td>
</tr>}
{stats && stats.pointcloud && stats.pointcloud.points &&
<tr>
<td><strong>{_("Reconstructed Points:")}</strong></td>
<td>{stats.pointcloud.points.toLocaleString()}</td>
</tr>}
{task.size > 0 &&
<tr>
<td><strong>{_("Disk Usage:")}</strong></td>
<td>{Utils.bytesToSize(task.size * 1024 * 1024)}</td>
</tr>}
<tr>
<td><strong>{_("Task ID:")}</strong></td>
<td>{task.id}</td>
</tr>
<tr>
<td><strong>{_("Task Output:")}</strong></td>
<td><div className="btn-group btn-toggle">
<button onClick={this.setView("console")} className={"btn btn-xs " + (this.state.view === "basic" ? "btn-default" : "btn-primary")}>{_("On")}</button>
<button onClick={this.setView("basic")} className={"btn btn-xs " + (this.state.view === "console" ? "btn-default" : "btn-primary")}>{_("Off")}</button>
</div></td>
</tr>
</tbody>
</table>
</div>
{task.status === statusCodes.COMPLETED ?
<div className="col-md-3 col-sm-2 text-center">
<a href={`/map/project/${task.project}/task/${task.id}/`}>
<img className="task-thumbnail" src={this.thumbnailUrl()} alt={_("Thumbnail")}/>
</a>
</div> : ""}
{this.state.view === 'console' ?
<Console

Wyświetl plik

@ -156,4 +156,8 @@
.name-link{
margin-right: 4px;
}
.task-thumbnail{
max-width: 100%;
}
}