From 53943c3f69386d17eb39f06eafe82fdd53db6115 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 5 Nov 2021 15:27:13 -0400 Subject: [PATCH] UI fixes, handle non-georeferenced datasets --- app/api/tiler.py | 3 +++ app/migrations/0032_task_epsg.py | 15 +++++++++++++-- app/models/task.py | 14 ++++++++++++-- app/pointcloud_utils.py | 14 ++++++++++++++ app/static/app/js/ModelView.jsx | 7 +++++-- .../app/js/components/AssetDownloadButtons.jsx | 6 +++++- app/static/app/js/components/ExportAssetPanel.jsx | 8 +++++--- app/static/app/js/css/ModelView.scss | 5 ++++- app/tests/test_api_task.py | 7 +++++++ 9 files changed, 68 insertions(+), 11 deletions(-) diff --git a/app/api/tiler.py b/app/api/tiler.py index fed221f4..3de4256c 100644 --- a/app/api/tiler.py +++ b/app/api/tiler.py @@ -518,6 +518,9 @@ class Export(TaskNestedView): except ValueError: raise exceptions.ValidationError(_("Invalid EPSG code: %(value)s") % {'value': epsg}) + if epsg is not None and task.epsg is None: + raise exceptions.ValidationError(_("Cannot use epsg on non-georeferenced dataset")) + if (formula and not bands) or (not formula and bands): raise exceptions.ValidationError(_("Both formula and bands parameters are required")) diff --git a/app/migrations/0032_task_epsg.py b/app/migrations/0032_task_epsg.py index db96a0a9..d4fa7e32 100644 --- a/app/migrations/0032_task_epsg.py +++ b/app/migrations/0032_task_epsg.py @@ -3,6 +3,7 @@ from django.db import migrations, models import rasterio import os +from app.pointcloud_utils import is_pointcloud_georeferenced from webodm import settings def update_epsg_fields(apps, schema_editor): @@ -23,7 +24,16 @@ def update_epsg_fields(apps, schema_editor): break # We assume all assets are in the same CRS except Exception as e: print(e) - + + # If point cloud is not georeferenced, dataset is not georeferenced + # (2D assets might be using pseudo-georeferencing) + point_cloud = os.path.join(settings.MEDIA_ROOT, "project", str(t.project.id), "task", str(t.id), "assets", "odm_georeferencing", "odm_georeferenced_model.laz") + + if epsg is not None and os.path.isfile(point_cloud): + if not is_pointcloud_georeferenced(point_cloud): + print("{} is not georeferenced".format(t)) + epsg = None + print("Updating {} (with epsg: {})".format(t, epsg)) t.epsg = epsg @@ -41,7 +51,8 @@ def remove_all_zip(apps, schema_editor): print("Cleaned up {}".format(asset_path)) except Exception as e: print(e) - + + class Migration(migrations.Migration): dependencies = [ diff --git a/app/models/task.py b/app/models/task.py index e2ab6d29..1dce0ee8 100644 --- a/app/models/task.py +++ b/app/models/task.py @@ -32,6 +32,7 @@ from app import pending_actions from django.contrib.gis.db.models.fields import GeometryField from app.cogeo import assure_cogeo +from app.pointcloud_utils import is_pointcloud_georeferenced from app.testwatch import testWatch from app.security import path_traversal_check from nodeodm import status_codes @@ -933,7 +934,8 @@ class Task(models.Model): 'id': str(self.id), 'project': self.project.id, 'available_assets': self.available_assets, - 'public': self.public + 'public': self.public, + 'epsg': self.epsg } def generate_deferred_asset(self, archive, directory, stream=False): @@ -980,8 +982,16 @@ class Task(models.Model): break # We assume all assets are in the same CRS except Exception as e: logger.warning(e) - self.epsg = epsg + # If point cloud is not georeferenced, dataset is not georeferenced + # (2D assets might be using pseudo-georeferencing) + point_cloud = self.assets_path(self.ASSETS_MAP['georeferenced_model.laz']) + if epsg is not None and os.path.isfile(point_cloud): + if not is_pointcloud_georeferenced(point_cloud): + logger.info("{} is not georeferenced".format(self)) + epsg = None + + self.epsg = epsg if commit: self.save() diff --git a/app/pointcloud_utils.py b/app/pointcloud_utils.py index c8a0b82d..8960a6dd 100644 --- a/app/pointcloud_utils.py +++ b/app/pointcloud_utils.py @@ -1,6 +1,8 @@ import logging import os import subprocess +import json + from app.security import double_quote logger = logging.getLogger('app.logger') @@ -21,3 +23,15 @@ def export_pointcloud(input, output, **opts): '--writers.ply.storage_mode', 'little endian'] subprocess.check_output(["pdal", "translate", input, output] + reprojection_args + extra_args) + + +def is_pointcloud_georeferenced(laz_path): + if not os.path.isfile(laz_path): + return False + + try: + j = json.loads(subprocess.check_output(["pdal", "info", "--summary", laz_path])) + return 'summary' in j and 'srs' in j['summary'] + except Exception as e: + logger.warning(e) + return True # Assume georeferenced diff --git a/app/static/app/js/ModelView.jsx b/app/static/app/js/ModelView.jsx index 34f30902..98c2fb3e 100644 --- a/app/static/app/js/ModelView.jsx +++ b/app/static/app/js/ModelView.jsx @@ -144,6 +144,7 @@ class ModelView extends React.Component { showTexturedModel: false, initializingModel: false, selectedCamera: null, + modalOpen: false }; this.pointCloud = null; @@ -639,12 +640,14 @@ class ModelView extends React.Component {
-
+
+ buttonClass="btn-secondary" + onModalOpen={() => this.setState({modalOpen: true})} + onModalClose={() => this.setState({modalOpen: false})} /> {(this.props.shareButtons && !this.props.public) ? { this.shareButton = ref; }} diff --git a/app/static/app/js/components/AssetDownloadButtons.jsx b/app/static/app/js/components/AssetDownloadButtons.jsx index a504685d..29fd6d59 100644 --- a/app/static/app/js/components/AssetDownloadButtons.jsx +++ b/app/static/app/js/components/AssetDownloadButtons.jsx @@ -19,7 +19,9 @@ class AssetDownloadButtons extends React.Component { task: PropTypes.object.isRequired, direction: PropTypes.string, buttonClass: PropTypes.string, - showLabel: PropTypes.bool + showLabel: PropTypes.bool, + onModalOpen: PropTypes.func, + onModalClose: PropTypes.func }; constructor(props){ @@ -32,6 +34,7 @@ class AssetDownloadButtons extends React.Component { onHide = () => { this.setState({exportDialogProps: null}); + if (this.props.onModalClose) this.props.onModalClose(); } render(){ @@ -71,6 +74,7 @@ class AssetDownloadButtons extends React.Component { exportParams: asset.exportParams, assetLabel: asset.label }}); + if (this.props.onModalOpen) this.props.onModalOpen(); } } return (
  • diff --git a/app/static/app/js/components/ExportAssetPanel.jsx b/app/static/app/js/components/ExportAssetPanel.jsx index b69441d9..fa710a38 100644 --- a/app/static/app/js/components/ExportAssetPanel.jsx +++ b/app/static/app/js/components/ExportAssetPanel.jsx @@ -73,7 +73,7 @@ export default class ExportAssetPanel extends React.Component { this.state = { error: "", format: props.exportFormats[0], - epsg: this.props.task.epsg || "4326", + epsg: this.props.task.epsg || null, customEpsg: Storage.getItem("last_export_custom_epsg") || "4326", exporting: false } @@ -107,8 +107,10 @@ export default class ExportAssetPanel extends React.Component { } params.format = format; - params.epsg = this.getEpsg(); - console.log(params); + + const epsg = this.getEpsg(); + if (epsg) params.epsg = this.getEpsg(); + return params; } diff --git a/app/static/app/js/css/ModelView.scss b/app/static/app/js/css/ModelView.scss index 143f6051..d1016424 100644 --- a/app/static/app/js/css/ModelView.scss +++ b/app/static/app/js/css/ModelView.scss @@ -68,6 +68,10 @@ bottom: 12px; right: 6px; + &.modal-open{ + z-index: 999999; + } + .switchModeButton{ position: initial; } @@ -82,7 +86,6 @@ margin-right: 8px; } } - /* Potree specific */ #potree_map{ diff --git a/app/tests/test_api_task.py b/app/tests/test_api_task.py index 1dd918ea..19d78528 100644 --- a/app/tests/test_api_task.py +++ b/app/tests/test_api_task.py @@ -250,6 +250,9 @@ class TestApiTask(BootTransactionTestCase): # No processing node is set self.assertTrue(task.processing_node is None) + # EPSG should be null + self.assertTrue(task.epsg is None) + # tiles.json, bounds, metadata should not be accessible at this point tile_types = ['orthophoto', 'dsm', 'dtm'] endpoints = ['tiles.json', 'bounds', 'metadata'] @@ -318,6 +321,7 @@ class TestApiTask(BootTransactionTestCase): # Processing should have started and a UUID is assigned # Calling process pending tasks should finish the process # and invoke the plugins completed signal + time.sleep(0.5) task.refresh_from_db() self.assertTrue(task.status in [status_codes.RUNNING, status_codes.COMPLETED]) # Sometimes this finishes before we get here self.assertTrue(len(task.uuid) > 0) @@ -895,6 +899,9 @@ class TestApiTask(BootTransactionTestCase): self.assertTrue(os.path.exists(task.assets_path("dsm_tiles"))) self.assertTrue(os.path.exists(task.assets_path("dtm_tiles"))) + # EPSG should be populated + self.assertEqual(task.epsg, 32615) + # Can access only tiles of available assets res = client.get("/api/projects/{}/tasks/{}/dsm/tiles.json".format(project.id, task.id)) self.assertEqual(res.status_code, status.HTTP_200_OK)