diff --git a/app/api/tasks.py b/app/api/tasks.py index a17697c6..d95a8589 100644 --- a/app/api/tasks.py +++ b/app/api/tasks.py @@ -12,6 +12,8 @@ from rest_framework import status, serializers, viewsets, filters, exceptions, p from rest_framework.response import Response from rest_framework.decorators import detail_route from rest_framework.views import APIView + +from nodeodm import status_codes from .common import get_and_check_project, get_tile_json, path_traversal_check from app import models, scheduler, pending_actions @@ -27,10 +29,14 @@ class TaskSerializer(serializers.ModelSerializer): project = serializers.PrimaryKeyRelatedField(queryset=models.Project.objects.all()) processing_node = serializers.PrimaryKeyRelatedField(queryset=ProcessingNode.objects.all()) images_count = serializers.SerializerMethodField() + available_assets = serializers.SerializerMethodField() def get_images_count(self, obj): return obj.imageupload_set.count() + def get_available_assets(self, obj): + return obj.get_available_assets() + class Meta: model = models.Task exclude = ('processing_lock', 'console_output', 'orthophoto', ) @@ -203,7 +209,7 @@ class TaskTilesJson(TaskNestedView): }) if task.orthophoto_area is None: - raise exceptions.ValidationError("An orthophoto has not been processed for this task. Tiles are not available yet.") + raise exceptions.ValidationError("An orthophoto has not been processed for this task. Tiles are not available.") json = get_tile_json(task.name, [ '/api/projects/{}/tasks/{}/tiles/{{z}}/{{x}}/{{y}}.png'.format(task.project.id, task.id) @@ -222,38 +228,22 @@ class TaskDownloads(TaskNestedView): """ task = self.get_and_check_task(request, pk, project_pk) - allowed_assets = { - 'all': 'all.zip', - 'geotiff': os.path.join('odm_orthophoto', 'odm_orthophoto.tif'), - 'texturedmodel': '_SEE_PATH_BELOW_', - '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') - } - - # Generate textured mesh if requested + # Check and download try: - if asset == 'texturedmodel': - allowed_assets[asset] = os.path.basename(task.get_textured_model_archive()) + asset_path = task.get_asset_download_path(asset) except FileNotFoundError: raise exceptions.NotFound("Asset does not exist") - # Check and download - 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") - if not os.path.exists(asset_path): - raise exceptions.NotFound("Asset does not exist") + asset_filename = os.path.basename(asset_path) - 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() + 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 """ Raw access to the task's asset folder resources diff --git a/app/models.py b/app/models.py index 3a749ef0..7a52f2ee 100644 --- a/app/models.py +++ b/app/models.py @@ -70,7 +70,8 @@ class Project(models.Model): def get_tile_json_data(self): return [task.get_tile_json_data() for task in self.task_set.filter( - status=status_codes.COMPLETED + status=status_codes.COMPLETED, + orthophoto__isnull=False ).only('id', 'project_id')] class Meta: @@ -116,6 +117,8 @@ def validate_task_options(value): class Task(models.Model): + ASSET_DOWNLOADS = ("all", "geotiff", "texturedmodel", "las", "csv", "ply",) + STATUS_CODES = ( (status_codes.QUEUED, 'QUEUED'), (status_codes.RUNNING, 'RUNNING'), @@ -221,6 +224,28 @@ class Task(models.Model): "assets", *args) + def get_asset_download_path(self, asset): + """ + Get the path to an asset download + :param asset: one of ASSET_DOWNLOADS + :return: path + """ + if asset == 'texturedmodel': + return self.assets_path(os.path.basename(self.get_textured_model_archive())) + else: + map = { + '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 map: + return self.assets_path(map[asset]) + else: + raise FileNotFoundError("{} is not a valid asset".format(asset)) + def process(self): """ This method contains the logic for processing tasks asynchronously @@ -439,6 +464,19 @@ class Task(models.Model): return archive_path + def get_available_assets(self): + # We make some assumptions for the sake of speed + # as checking the filesystem would be slow + if self.status == status_codes.COMPLETED: + assets = list(self.ASSET_DOWNLOADS) + + if self.orthophoto is None: + assets.remove('geotiff') + + return assets + else: + return [] + def delete(self, using=None, keep_parents=False): directory_to_delete = os.path.join(settings.MEDIA_ROOT, task_directory_path(self.id, self.project.id)) diff --git a/app/static/app/js/ModelView.jsx b/app/static/app/js/ModelView.jsx index 8f2736e4..87e6007d 100644 --- a/app/static/app/js/ModelView.jsx +++ b/app/static/app/js/ModelView.jsx @@ -1046,6 +1046,8 @@ class ModelView extends React.Component { // React render render(){ + const showSwitchModeButton = this.props.task.available_assets.indexOf('geotiff') !== -1; + return (
{ this.texturedModelStandby = domNode; }} /> - + {showSwitchModeButton ? + : ""}
); diff --git a/app/static/app/js/classes/AssetDownloads.js b/app/static/app/js/classes/AssetDownloads.js index 5749babb..9189e2aa 100644 --- a/app/static/app/js/classes/AssetDownloads.js +++ b/app/static/app/js/classes/AssetDownloads.js @@ -43,6 +43,11 @@ const api = { excludeSeparators: function(){ return api.all().filter(asset => !asset.separator); + }, + + // @param assets {String[]} list of assets (example: ['geotiff', 'las'])) + only: function(assets){ + return api.all().filter(asset => assets.indexOf(asset.asset) !== -1); } } diff --git a/app/static/app/js/components/AssetDownloadButtons.jsx b/app/static/app/js/components/AssetDownloadButtons.jsx index 10020677..8dcc402b 100644 --- a/app/static/app/js/components/AssetDownloadButtons.jsx +++ b/app/static/app/js/components/AssetDownloadButtons.jsx @@ -29,7 +29,7 @@ class AssetDownloadButtons extends React.Component { } render(){ - const assetDownloads = AssetDownloads.all(); + const assetDownloads = AssetDownloads.only(this.props.task.available_assets); return (
: ""} {/* TODO: List of images? */} + + {showGeotiffMissingWarning ? +
An orthophoto could not be generated. To generate one, make sure GPS information is embedded in the EXIF tags of your images.
: ""}