kopia lustrzana https://github.com/OpenDroneMap/WebODM
UI fixes, handle non-georeferenced datasets
rodzic
1ccad9312d
commit
53943c3f69
|
@ -518,6 +518,9 @@ class Export(TaskNestedView):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise exceptions.ValidationError(_("Invalid EPSG code: %(value)s") % {'value': epsg})
|
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):
|
if (formula and not bands) or (not formula and bands):
|
||||||
raise exceptions.ValidationError(_("Both formula and bands parameters are required"))
|
raise exceptions.ValidationError(_("Both formula and bands parameters are required"))
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import rasterio
|
import rasterio
|
||||||
import os
|
import os
|
||||||
|
from app.pointcloud_utils import is_pointcloud_georeferenced
|
||||||
from webodm import settings
|
from webodm import settings
|
||||||
|
|
||||||
def update_epsg_fields(apps, schema_editor):
|
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
|
break # We assume all assets are in the same CRS
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(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))
|
print("Updating {} (with epsg: {})".format(t, epsg))
|
||||||
|
|
||||||
t.epsg = epsg
|
t.epsg = epsg
|
||||||
|
@ -41,7 +51,8 @@ def remove_all_zip(apps, schema_editor):
|
||||||
print("Cleaned up {}".format(asset_path))
|
print("Cleaned up {}".format(asset_path))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
|
@ -32,6 +32,7 @@ from app import pending_actions
|
||||||
from django.contrib.gis.db.models.fields import GeometryField
|
from django.contrib.gis.db.models.fields import GeometryField
|
||||||
|
|
||||||
from app.cogeo import assure_cogeo
|
from app.cogeo import assure_cogeo
|
||||||
|
from app.pointcloud_utils import is_pointcloud_georeferenced
|
||||||
from app.testwatch import testWatch
|
from app.testwatch import testWatch
|
||||||
from app.security import path_traversal_check
|
from app.security import path_traversal_check
|
||||||
from nodeodm import status_codes
|
from nodeodm import status_codes
|
||||||
|
@ -933,7 +934,8 @@ class Task(models.Model):
|
||||||
'id': str(self.id),
|
'id': str(self.id),
|
||||||
'project': self.project.id,
|
'project': self.project.id,
|
||||||
'available_assets': self.available_assets,
|
'available_assets': self.available_assets,
|
||||||
'public': self.public
|
'public': self.public,
|
||||||
|
'epsg': self.epsg
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_deferred_asset(self, archive, directory, stream=False):
|
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
|
break # We assume all assets are in the same CRS
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(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()
|
if commit: self.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
from app.security import double_quote
|
from app.security import double_quote
|
||||||
|
|
||||||
logger = logging.getLogger('app.logger')
|
logger = logging.getLogger('app.logger')
|
||||||
|
@ -21,3 +23,15 @@ def export_pointcloud(input, output, **opts):
|
||||||
'--writers.ply.storage_mode', 'little endian']
|
'--writers.ply.storage_mode', 'little endian']
|
||||||
|
|
||||||
subprocess.check_output(["pdal", "translate", input, output] + reprojection_args + extra_args)
|
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
|
||||||
|
|
|
@ -144,6 +144,7 @@ class ModelView extends React.Component {
|
||||||
showTexturedModel: false,
|
showTexturedModel: false,
|
||||||
initializingModel: false,
|
initializingModel: false,
|
||||||
selectedCamera: null,
|
selectedCamera: null,
|
||||||
|
modalOpen: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pointCloud = null;
|
this.pointCloud = null;
|
||||||
|
@ -639,12 +640,14 @@ class ModelView extends React.Component {
|
||||||
<div id="potree_sidebar_container"> </div>
|
<div id="potree_sidebar_container"> </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="model-action-buttons">
|
<div className={"model-action-buttons " + (this.state.modalOpen ? "modal-open" : "")}>
|
||||||
<AssetDownloadButtons
|
<AssetDownloadButtons
|
||||||
task={this.props.task}
|
task={this.props.task}
|
||||||
direction="up"
|
direction="up"
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
buttonClass="btn-secondary" />
|
buttonClass="btn-secondary"
|
||||||
|
onModalOpen={() => this.setState({modalOpen: true})}
|
||||||
|
onModalClose={() => this.setState({modalOpen: false})} />
|
||||||
{(this.props.shareButtons && !this.props.public) ?
|
{(this.props.shareButtons && !this.props.public) ?
|
||||||
<ShareButton
|
<ShareButton
|
||||||
ref={(ref) => { this.shareButton = ref; }}
|
ref={(ref) => { this.shareButton = ref; }}
|
||||||
|
|
|
@ -19,7 +19,9 @@ class AssetDownloadButtons extends React.Component {
|
||||||
task: PropTypes.object.isRequired,
|
task: PropTypes.object.isRequired,
|
||||||
direction: PropTypes.string,
|
direction: PropTypes.string,
|
||||||
buttonClass: PropTypes.string,
|
buttonClass: PropTypes.string,
|
||||||
showLabel: PropTypes.bool
|
showLabel: PropTypes.bool,
|
||||||
|
onModalOpen: PropTypes.func,
|
||||||
|
onModalClose: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props){
|
constructor(props){
|
||||||
|
@ -32,6 +34,7 @@ class AssetDownloadButtons extends React.Component {
|
||||||
|
|
||||||
onHide = () => {
|
onHide = () => {
|
||||||
this.setState({exportDialogProps: null});
|
this.setState({exportDialogProps: null});
|
||||||
|
if (this.props.onModalClose) this.props.onModalClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
|
@ -71,6 +74,7 @@ class AssetDownloadButtons extends React.Component {
|
||||||
exportParams: asset.exportParams,
|
exportParams: asset.exportParams,
|
||||||
assetLabel: asset.label
|
assetLabel: asset.label
|
||||||
}});
|
}});
|
||||||
|
if (this.props.onModalOpen) this.props.onModalOpen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (<li key={i}>
|
return (<li key={i}>
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default class ExportAssetPanel extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
error: "",
|
error: "",
|
||||||
format: props.exportFormats[0],
|
format: props.exportFormats[0],
|
||||||
epsg: this.props.task.epsg || "4326",
|
epsg: this.props.task.epsg || null,
|
||||||
customEpsg: Storage.getItem("last_export_custom_epsg") || "4326",
|
customEpsg: Storage.getItem("last_export_custom_epsg") || "4326",
|
||||||
exporting: false
|
exporting: false
|
||||||
}
|
}
|
||||||
|
@ -107,8 +107,10 @@ export default class ExportAssetPanel extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
params.format = format;
|
params.format = format;
|
||||||
params.epsg = this.getEpsg();
|
|
||||||
console.log(params);
|
const epsg = this.getEpsg();
|
||||||
|
if (epsg) params.epsg = this.getEpsg();
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,10 @@
|
||||||
bottom: 12px;
|
bottom: 12px;
|
||||||
right: 6px;
|
right: 6px;
|
||||||
|
|
||||||
|
&.modal-open{
|
||||||
|
z-index: 999999;
|
||||||
|
}
|
||||||
|
|
||||||
.switchModeButton{
|
.switchModeButton{
|
||||||
position: initial;
|
position: initial;
|
||||||
}
|
}
|
||||||
|
@ -82,7 +86,6 @@
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Potree specific */
|
/* Potree specific */
|
||||||
#potree_map{
|
#potree_map{
|
||||||
|
|
|
@ -250,6 +250,9 @@ class TestApiTask(BootTransactionTestCase):
|
||||||
# No processing node is set
|
# No processing node is set
|
||||||
self.assertTrue(task.processing_node is None)
|
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
|
# tiles.json, bounds, metadata should not be accessible at this point
|
||||||
tile_types = ['orthophoto', 'dsm', 'dtm']
|
tile_types = ['orthophoto', 'dsm', 'dtm']
|
||||||
endpoints = ['tiles.json', 'bounds', 'metadata']
|
endpoints = ['tiles.json', 'bounds', 'metadata']
|
||||||
|
@ -318,6 +321,7 @@ class TestApiTask(BootTransactionTestCase):
|
||||||
# Processing should have started and a UUID is assigned
|
# Processing should have started and a UUID is assigned
|
||||||
# Calling process pending tasks should finish the process
|
# Calling process pending tasks should finish the process
|
||||||
# and invoke the plugins completed signal
|
# and invoke the plugins completed signal
|
||||||
|
time.sleep(0.5)
|
||||||
task.refresh_from_db()
|
task.refresh_from_db()
|
||||||
self.assertTrue(task.status in [status_codes.RUNNING, status_codes.COMPLETED]) # Sometimes this finishes before we get here
|
self.assertTrue(task.status in [status_codes.RUNNING, status_codes.COMPLETED]) # Sometimes this finishes before we get here
|
||||||
self.assertTrue(len(task.uuid) > 0)
|
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("dsm_tiles")))
|
||||||
self.assertTrue(os.path.exists(task.assets_path("dtm_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
|
# Can access only tiles of available assets
|
||||||
res = client.get("/api/projects/{}/tasks/{}/dsm/tiles.json".format(project.id, task.id))
|
res = client.get("/api/projects/{}/tasks/{}/dsm/tiles.json".format(project.id, task.id))
|
||||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||||
|
|
Ładowanie…
Reference in New Issue