From 8f760ecd7ff7b030c3d5071875ee0d1688371cfc Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 11 Jun 2021 14:22:53 -0400 Subject: [PATCH] Save/load potree measurements --- app/api/potree.py | 47 +++++++++++- app/api/urls.py | 3 +- app/static/app/js/ModelView.jsx | 123 +++++++++++++++++++++++++++++++- 3 files changed, 169 insertions(+), 4 deletions(-) diff --git a/app/api/potree.py b/app/api/potree.py index 0cfebb27..b48dfbe2 100644 --- a/app/api/potree.py +++ b/app/api/potree.py @@ -1,5 +1,7 @@ from .tasks import TaskNestedView +from .common import get_and_check_project from rest_framework.response import Response +from rest_framework import exceptions class Scene(TaskNestedView): def get(self, request, pk=None, project_pk=None): @@ -8,4 +10,47 @@ class Scene(TaskNestedView): """ task = self.get_and_check_task(request, pk) - return Response(task.potree_scene) \ No newline at end of file + return Response(task.potree_scene) + + def post(self, request, pk=None, project_pk=None): + """ + Store potree scene information (except camera view) + """ + get_and_check_project(request, project_pk, perms=("change_project", )) + task = self.get_and_check_task(request, pk) + scene = request.data + + # Quick type check + if scene.get('type') != 'Potree': + raise exceptions.ValidationError(detail="Invalid potree scene") + + for k in scene: + if not k in ["view", "pointclouds", "settings"]: + task.potree_scene[k] = scene[k] + + task.save() + return Response({'success': True}) + +class CameraView(TaskNestedView): + def post(self, request, pk=None, project_pk=None): + """ + Store camera view information + """ + get_and_check_project(request, project_pk, perms=("change_project", )) + task = self.get_and_check_task(request, pk) + + view = request.data + if not view: + raise exceptions.ValidationError(detail="view parameter missing") + + if not task.potree_scene: + init_p = { + 'type': 'Potree', + 'version': 1.7 + } + task.potree_scene = init_p + + task.potree_scene['view'] = view + task.save() + + return Response({'success': True}) \ No newline at end of file diff --git a/app/api/urls.py b/app/api/urls.py index 5141c7da..b5d6e7f7 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -10,7 +10,7 @@ from .admin import UserViewSet, GroupViewSet from rest_framework_nested import routers from rest_framework_jwt.views import obtain_jwt_token from .tiler import TileJson, Bounds, Metadata, Tiles, Export -from .potree import Scene +from .potree import Scene, CameraView from .workers import CheckTask, GetTaskResult router = routers.DefaultRouter() @@ -47,6 +47,7 @@ urlpatterns = [ url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/images/download/(?P.+)$', ImageDownload.as_view()), url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/3d/scene$', Scene.as_view()), + url(r'projects/(?P[^/.]+)/tasks/(?P[^/.]+)/3d/cameraview$', CameraView.as_view()), url(r'workers/check/(?P.+)', CheckTask.as_view()), url(r'workers/get/(?P.+)', GetTaskResult.as_view()), diff --git a/app/static/app/js/ModelView.jsx b/app/static/app/js/ModelView.jsx index d9120f2b..13a3b441 100644 --- a/app/static/app/js/ModelView.jsx +++ b/app/static/app/js/ModelView.jsx @@ -15,6 +15,57 @@ require('./vendor/OBJLoader'); require('./vendor/MTLLoader'); require('./vendor/ColladaLoader'); +class SetCameraView extends React.Component{ + static propTypes = { + viewer: PropTypes.object.isRequired, + task: PropTypes.object.isRequired + } + + constructor(props){ + super(props); + + this.state = { + error: "", + showOk: false + } + } + + handleClick = () => { + const { view } = Potree.saveProject(this.props.viewer); + const showError = () => { + this.setState({error: _("Cannot set initial camera view")}); + setTimeout(() => this.setState({error: ""}), 3000); + }; + const showOk = () => { + this.setState({showOk: true}); + setTimeout(() => this.setState({showOk: false}), 2000); + } + + $.ajax({ + url: `/api/projects/${this.props.task.project}/tasks/${this.props.task.id}/3d/cameraview`, + contentType: 'application/json', + data: JSON.stringify(view), + dataType: 'json', + type: 'POST' + }).done(result => { + if (result.success) showOk(); + else showError(); + }).fail(() => { + showError(); + }); + } + + render(){ + return ([, + this.state.showOk ? (
) : "", + this.state.error ? (
{this.state.error}
) : "" + ] + ); + } +} + class TexturedModelMenu extends React.Component{ static propTypes = { toggleTexturedModel: PropTypes.func.isRequired @@ -221,6 +272,17 @@ class ModelView extends React.Component { }); } + getSceneData(){ + let json = Potree.saveProject(window.viewer); + + // Remove view, settings since we don't want to trigger + // scene updates when these change. + delete json.view; + delete json.settings; + + return json; + } + componentDidMount() { let container = this.container; if (!container) return; // Enzyme tests don't have support for all WebGL methods so we just skip this @@ -250,6 +312,10 @@ class ModelView extends React.Component { $("#cameras").hide(); $("#cameras_container").hide(); } + + const $scv = $("
"); + $scv.prependTo($("#scene_export").parent()); + window.ReactDOM.render(, $scv.get(0)); }); viewer.scene.scene.add( new THREE.AmbientLight( 0x404040, 2.0 ) ); // soft white light ); @@ -280,9 +346,62 @@ class ModelView extends React.Component { type: "GET", url: `/api/projects/${this.props.task.project}/tasks/${this.props.task.id}/3d/scene` }).done(sceneData => { - Potree.loadProject(viewer, sceneData); + let localSceneData = Potree.saveProject(viewer); + + // Check if we do not have a view set + // if so, just keep the current view information + if (!sceneData.view || !sceneData.view.position){ + sceneData.view = localSceneData.view; + } + + sceneData.pointclouds = localSceneData.pointclouds; + sceneData.settings = localSceneData.settings; + + // Load + Potree.loadProject(viewer, sceneData); + + // Every 3 seconds, check if the scene has changed + // if it has, save the changes server-side + // Unfortunately Potree does not have reliable events + // for trivially detecting changes in measurements + let saveSceneReq = null; + let saveSceneInterval = null; + let saveSceneErrors = 0; + let prevSceneData = JSON.stringify(this.getSceneData()); + + const postSceneData = (sceneData) => { + if (saveSceneReq){ + saveSceneReq.abort(); + saveSceneReq = null; + } + + saveSceneReq = $.ajax({ + url: `/api/projects/${this.props.task.project}/tasks/${this.props.task.id}/3d/scene`, + contentType: 'application/json', + data: sceneData, + dataType: 'json', + type: 'POST' + }).done(result => { + if (result.success){ + saveSceneErrors = 0; + prevSceneData = sceneData; + }else{ + console.warn("Cannot save Potree scene"); + } + }).fail(() => { + console.error("Cannot save Potree scene"); + if (++saveSceneErrors === 5) clearInterval(saveSceneInterval); + }); + }; + + const checkScene = () => { + const sceneData = JSON.stringify(this.getSceneData()); + if (sceneData !== prevSceneData) postSceneData(sceneData); + }; + + saveSceneInterval = setInterval(checkScene, 3000); }).fail(e => { - console.error("Cannot load 3D scene information", e); + console.error("Cannot load 3D scene information", e); }); }); });