kopia lustrzana https://github.com/OpenDroneMap/WebODM
Toggle 3D cameras
rodzic
24b354fdaa
commit
eb976672f7
|
@ -5,19 +5,11 @@ import SwitchModeButton from './components/SwitchModeButton';
|
||||||
import AssetDownloadButtons from './components/AssetDownloadButtons';
|
import AssetDownloadButtons from './components/AssetDownloadButtons';
|
||||||
import Standby from './components/Standby';
|
import Standby from './components/Standby';
|
||||||
import ShareButton from './components/ShareButton';
|
import ShareButton from './components/ShareButton';
|
||||||
|
import ImagePopup from './components/ImagePopup';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import epsg from 'epsg';
|
import epsg from 'epsg';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
// Add more proj definitions
|
|
||||||
const defs = [];
|
|
||||||
for (let k in epsg){
|
|
||||||
if (epsg[k]){
|
|
||||||
defs.push([k, epsg[k]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.proj4.defs(defs);
|
|
||||||
|
|
||||||
require('./vendor/OBJLoader');
|
require('./vendor/OBJLoader');
|
||||||
require('./vendor/MTLLoader');
|
require('./vendor/MTLLoader');
|
||||||
require('./vendor/ColladaLoader');
|
require('./vendor/ColladaLoader');
|
||||||
|
@ -49,6 +41,33 @@ class TexturedModelMenu extends React.Component{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CamerasMenu extends React.Component{
|
||||||
|
static propTypes = {
|
||||||
|
toggleCameras: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props){
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showCameras: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = (e) => {
|
||||||
|
this.setState({showCameras: e.target.checked});
|
||||||
|
this.props.toggleCameras(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(){
|
||||||
|
return (<label><input
|
||||||
|
type="checkbox"
|
||||||
|
checked={this.state.showCameras}
|
||||||
|
onChange={this.handleClick}
|
||||||
|
/> Show Cameras</label>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ModelView extends React.Component {
|
class ModelView extends React.Component {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
task: null,
|
task: null,
|
||||||
|
@ -66,13 +85,16 @@ class ModelView extends React.Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
error: "",
|
error: "",
|
||||||
showTexturedModel: false,
|
showTexturedModel: false,
|
||||||
initializingModel: false
|
initializingModel: false,
|
||||||
|
selectedCamera: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pointCloud = null;
|
this.pointCloud = null;
|
||||||
this.modelReference = null;
|
this.modelReference = null;
|
||||||
|
|
||||||
this.toggleTexturedModel = this.toggleTexturedModel.bind(this);
|
this.toggleTexturedModel = this.toggleTexturedModel.bind(this);
|
||||||
|
this.toggleCameras = this.toggleCameras.bind(this);
|
||||||
|
|
||||||
|
|
||||||
this.cameraMeshes = [];
|
this.cameraMeshes = [];
|
||||||
}
|
}
|
||||||
|
@ -118,6 +140,10 @@ class ModelView extends React.Component {
|
||||||
return this.props.task.available_assets.indexOf('textured_model.zip') !== -1;
|
return this.props.task.available_assets.indexOf('textured_model.zip') !== -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasCameras(){
|
||||||
|
return this.props.task.available_assets.indexOf('shots.geojson') !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
objFilePath(cb){
|
objFilePath(cb){
|
||||||
const geoUrl = this.texturedModelDirectoryPath() + 'odm_textured_model_geo.obj';
|
const geoUrl = this.texturedModelDirectoryPath() + 'odm_textured_model_geo.obj';
|
||||||
const nongeoUrl = this.texturedModelDirectoryPath() + 'odm_textured_model.obj';
|
const nongeoUrl = this.texturedModelDirectoryPath() + 'odm_textured_model.obj';
|
||||||
|
@ -158,6 +184,13 @@ class ModelView extends React.Component {
|
||||||
$("#textured_model").hide();
|
$("#textured_model").hide();
|
||||||
$("#textured_model_container").hide();
|
$("#textured_model_container").hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.hasCameras()){
|
||||||
|
window.ReactDOM.render(<CamerasMenu toggleCameras={this.toggleCameras}/>, $("#cameras_button").get(0));
|
||||||
|
}else{
|
||||||
|
$("#cameras").hide();
|
||||||
|
$("#cameras_container").hide();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
viewer.scene.scene.add( new THREE.AmbientLight( 0x404040, 2.0 ) ); // soft white light );
|
viewer.scene.scene.add( new THREE.AmbientLight( 0x404040, 2.0 ) ); // soft white light );
|
||||||
|
@ -167,7 +200,6 @@ class ModelView extends React.Component {
|
||||||
directional.position.z = 99999999999;
|
directional.position.z = 99999999999;
|
||||||
viewer.scene.scene.add( directional );
|
viewer.scene.scene.add( directional );
|
||||||
|
|
||||||
|
|
||||||
this.pointCloudFilePath(pointCloudPath => {
|
this.pointCloudFilePath(pointCloudPath => {
|
||||||
Potree.loadPointCloud(pointCloudPath, "Point Cloud", e => {
|
Potree.loadPointCloud(pointCloudPath, "Point Cloud", e => {
|
||||||
if (e.type == "loading_failed"){
|
if (e.type == "loading_failed"){
|
||||||
|
@ -182,22 +214,22 @@ class ModelView extends React.Component {
|
||||||
let material = e.pointcloud.material;
|
let material = e.pointcloud.material;
|
||||||
material.size = 1;
|
material.size = 1;
|
||||||
|
|
||||||
this.loadCameras();
|
|
||||||
|
|
||||||
window.scene = viewer.scene.scene;
|
|
||||||
|
|
||||||
viewer.fitToScreen();
|
viewer.fitToScreen();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
viewer.renderer.domElement.addEventListener( 'mousedown', this.handleRenderClick );
|
viewer.renderer.domElement.addEventListener( 'mousedown', this.handleRenderMouseClick );
|
||||||
|
viewer.renderer.domElement.addEventListener( 'mousemove', this.handleRenderMouseMove );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(){
|
componentWillUnmount(){
|
||||||
viewer.renderer.domElement.removeEventListener( 'mousedown', this.handleRenderClick );
|
viewer.renderer.domElement.removeEventListener( 'mousedown', this.handleRenderMouseClick );
|
||||||
|
viewer.renderer.domElement.removeEventListener( 'mousemove', this.handleRenderMouseMove );
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRenderClick = (evt) => {
|
getCameraUnderCursor = (evt) => {
|
||||||
const raycaster = new THREE.Raycaster();
|
const raycaster = new THREE.Raycaster();
|
||||||
const rect = viewer.renderer.domElement.getBoundingClientRect();
|
const rect = viewer.renderer.domElement.getBoundingClientRect();
|
||||||
const [x, y] = [evt.clientX, evt.clientY];
|
const [x, y] = [evt.clientX, evt.clientY];
|
||||||
|
@ -214,12 +246,50 @@ class ModelView extends React.Component {
|
||||||
const intersects = raycaster.intersectObjects( this.cameraMeshes );
|
const intersects = raycaster.intersectObjects( this.cameraMeshes );
|
||||||
|
|
||||||
if ( intersects.length > 0){
|
if ( intersects.length > 0){
|
||||||
//console.log(intersects);
|
|
||||||
const intersection = intersects[0];
|
const intersection = intersects[0];
|
||||||
console.log(intersection);
|
return intersection.object;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCameraOpacity(camera, opacity){
|
||||||
|
camera.material.forEach(m => {
|
||||||
|
m.opacity = opacity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenderMouseMove = (evt) => {
|
||||||
|
if (this._prevCamera && this._prevCamera !== this.state.selectedCamera) {
|
||||||
|
this.setCameraOpacity(this._prevCamera, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
const camera = this.getCameraUnderCursor(evt);
|
||||||
|
if (camera){
|
||||||
|
viewer.renderer.domElement.classList.add("pointer-cursor");
|
||||||
|
this.setCameraOpacity(camera, 1);
|
||||||
|
}else{
|
||||||
|
viewer.renderer.domElement.classList.remove("pointer-cursor");
|
||||||
|
}
|
||||||
|
this._prevCamera = camera;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenderMouseClick = (evt) => {
|
||||||
|
let camera = this.getCameraUnderCursor(evt);
|
||||||
|
// Deselect
|
||||||
|
if (camera === this.state.selectedCamera){
|
||||||
|
this.setState({selectedCamera: null});
|
||||||
|
}else if (camera){
|
||||||
|
if (this.state.selectedCamera){
|
||||||
|
this.setCameraOpacity(this.state.selectedCamera, 0.7);
|
||||||
|
}
|
||||||
|
this.setState({selectedCamera: camera});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeThumb = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.setState({selectedCamera: null});
|
||||||
|
}
|
||||||
|
|
||||||
loadCameras(){
|
loadCameras(){
|
||||||
const { task } = this.props;
|
const { task } = this.props;
|
||||||
|
|
||||||
|
@ -234,7 +304,7 @@ class ModelView extends React.Component {
|
||||||
return matrix.transpose();
|
return matrix.transpose();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (task.available_assets.indexOf('shots.geojson') !== -1){
|
if (this.hasCameras()){
|
||||||
const colladaLoader = new THREE.ColladaLoader();
|
const colladaLoader = new THREE.ColladaLoader();
|
||||||
const fileloader = new THREE.FileLoader();
|
const fileloader = new THREE.FileLoader();
|
||||||
|
|
||||||
|
@ -254,13 +324,17 @@ class ModelView extends React.Component {
|
||||||
|
|
||||||
const toScene = proj4(gjproj, pcproj);
|
const toScene = proj4(gjproj, pcproj);
|
||||||
const cameraObj = dae.children[0];
|
const cameraObj = dae.children[0];
|
||||||
|
cameraObj.material.forEach(m => {
|
||||||
|
m.transparent = true;
|
||||||
|
m.opacity = 0.7;
|
||||||
|
});
|
||||||
|
|
||||||
// const cameraObj = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial());
|
// const cameraObj = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial());
|
||||||
|
|
||||||
// TODO: instancing doesn't seem to work :/
|
// TODO: instancing doesn't seem to work :/
|
||||||
// const cameraMeshes = new THREE.InstancedMesh( cameraObj.geometry, cameraObj.material, geojson.features.length );
|
// const cameraMeshes = new THREE.InstancedMesh( cameraObj.geometry, cameraObj.material, geojson.features.length );
|
||||||
// const dummy = new THREE.Object3D();
|
// const dummy = new THREE.Object3D();
|
||||||
|
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
geojson.features.forEach(feat => {
|
geojson.features.forEach(feat => {
|
||||||
const coords = feat.geometry.coordinates;
|
const coords = feat.geometry.coordinates;
|
||||||
|
@ -268,7 +342,8 @@ class ModelView extends React.Component {
|
||||||
const utm = toScene.forward([coords[0], coords[1]]);
|
const utm = toScene.forward([coords[0], coords[1]]);
|
||||||
utm.push(coords[2]); // z in meters doesn't change
|
utm.push(coords[2]); // z in meters doesn't change
|
||||||
|
|
||||||
const cameraMesh = new THREE.Mesh(cameraObj.geometry, cameraObj.material);
|
const material = cameraObj.material.map(m => m.clone());
|
||||||
|
const cameraMesh = new THREE.Mesh(cameraObj.geometry, material);
|
||||||
cameraMesh.matrixAutoUpdate = false;
|
cameraMesh.matrixAutoUpdate = false;
|
||||||
cameraMesh.matrix.set(...getMatrix(utm, feat.properties.rotation).elements);
|
cameraMesh.matrix.set(...getMatrix(utm, feat.properties.rotation).elements);
|
||||||
viewer.scene.scene.add(cameraMesh);
|
viewer.scene.scene.add(cameraMesh);
|
||||||
|
@ -289,6 +364,16 @@ class ModelView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleCameras(e){
|
||||||
|
if (this.cameraMeshes.length === 0){
|
||||||
|
this.loadCameras();
|
||||||
|
if (this.cameraMeshes.length === 0) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isVisible = this.cameraMeshes[0].visible;
|
||||||
|
this.cameraMeshes.forEach(cam => cam.visible = !isVisible);
|
||||||
|
}
|
||||||
|
|
||||||
toggleTexturedModel(e){
|
toggleTexturedModel(e){
|
||||||
const value = e.target.checked;
|
const value = e.target.checked;
|
||||||
|
|
||||||
|
@ -353,6 +438,9 @@ class ModelView extends React.Component {
|
||||||
|
|
||||||
// React render
|
// React render
|
||||||
render(){
|
render(){
|
||||||
|
const { selectedCamera } = this.state;
|
||||||
|
const { task } = this.props;
|
||||||
|
|
||||||
return (<div className="model-view">
|
return (<div className="model-view">
|
||||||
<ErrorMessage bind={[this, "error"]} />
|
<ErrorMessage bind={[this, "error"]} />
|
||||||
<div className="container potree_container"
|
<div className="container potree_container"
|
||||||
|
@ -383,6 +471,11 @@ class ModelView extends React.Component {
|
||||||
type="modelToMap" />
|
type="modelToMap" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedCamera ? <div className="thumbnail">
|
||||||
|
<a className="close-thumb" href="javascript:void(0)" onClick={this.closeThumb}><i className="fa fa-window-close"></i></a>
|
||||||
|
<ImagePopup feature={selectedCamera._feat} task={task} />
|
||||||
|
</div> : ""}
|
||||||
|
|
||||||
<Standby
|
<Standby
|
||||||
message="Loading textured model..."
|
message="Loading textured model..."
|
||||||
show={this.state.initializingModel}
|
show={this.state.initializingModel}
|
||||||
|
@ -392,6 +485,15 @@ class ModelView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
$(function(){
|
$(function(){
|
||||||
|
// Add more proj definitions
|
||||||
|
const defs = [];
|
||||||
|
for (let k in epsg){
|
||||||
|
if (epsg[k]){
|
||||||
|
defs.push([k, epsg[k]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.proj4.defs(defs);
|
||||||
|
|
||||||
$("[data-modelview]").each(function(){
|
$("[data-modelview]").each(function(){
|
||||||
let props = $(this).data();
|
let props = $(this).data();
|
||||||
delete(props.modelview);
|
delete(props.modelview);
|
||||||
|
|
|
@ -17,11 +17,15 @@ class ImagePopup extends React.Component {
|
||||||
loading: true,
|
loading: true,
|
||||||
expandThumb: false,
|
expandThumb: false,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { feature, task } = props;
|
getImageUrl(){
|
||||||
const imageUrl = `/api/projects/${task.project}/tasks/${task.id}/images/thumbnail/${feature.properties.filename}`;
|
const { feature, task } = this.props;
|
||||||
this.thumbUrl = `${imageUrl}?size=320`;
|
return `/api/projects/${task.project}/tasks/${task.id}/images/thumbnail/${feature.properties.filename}`;
|
||||||
this.highresUrl = `${imageUrl}?size=9999999`;
|
}
|
||||||
|
|
||||||
|
getThumbUrl(size){
|
||||||
|
return `${this.getImageUrl()}?size=${size}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(){
|
componentDidMount(){
|
||||||
|
@ -64,11 +68,11 @@ class ImagePopup extends React.Component {
|
||||||
|
|
||||||
const downloadImageLink = `/api/projects/${task.project}/tasks/${task.id}/images/download/${feature.properties.filename}`;
|
const downloadImageLink = `/api/projects/${task.project}/tasks/${task.id}/images/download/${feature.properties.filename}`;
|
||||||
const downloadShotsLink = `/api/projects/${task.project}/tasks/${task.id}/download/shots.geojson`;
|
const downloadShotsLink = `/api/projects/${task.project}/tasks/${task.id}/download/shots.geojson`;
|
||||||
const imageUrl = expandThumb ? this.highresUrl : this.thumbUrl;
|
const imageUrl = expandThumb ? this.getThumbUrl(999999999) : this.getThumbUrl(320);
|
||||||
const assetDownload = AssetDownloads.only(["shots.geojson"])[0];
|
const assetDownload = AssetDownloads.only(["shots.geojson"])[0];
|
||||||
|
|
||||||
return (<div className="image-popup">
|
return (<div className="image-popup">
|
||||||
<strong>{feature.properties.filename}</strong>
|
<div className="title" title={feature.properties.filename}>{feature.properties.filename}</div>
|
||||||
{loading ? <div><i className="fa fa-circle-notch fa-spin fa-fw"></i></div>
|
{loading ? <div><i className="fa fa-circle-notch fa-spin fa-fw"></i></div>
|
||||||
: ""}
|
: ""}
|
||||||
{error !== "" ? <div style={{marginTop: "8px"}}>{error}</div>
|
{error !== "" ? <div style={{marginTop: "8px"}}>{error}</div>
|
||||||
|
|
|
@ -3,6 +3,15 @@
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.title{
|
||||||
|
margin-top: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
max-width: 300px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
div.image{
|
div.image{
|
||||||
a{
|
a{
|
||||||
cursor: zoom-in;
|
cursor: zoom-in;
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
#potree_render_area > canvas{
|
#potree_render_area > canvas{
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
|
|
||||||
|
&.pointer-cursor{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.container{
|
.container{
|
||||||
|
@ -180,6 +184,25 @@
|
||||||
#potree_download_profile_ortho_link, #potree_download_profile_link{
|
#potree_download_profile_ortho_link, #potree_download_profile_link{
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thumbnail{
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
background: #19282c;
|
||||||
|
border-color: #a6a9aa;
|
||||||
|
color: #ccccff;
|
||||||
|
.close-thumb{
|
||||||
|
opacity: 0.8;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 8px;
|
||||||
|
&:hover{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#profile_window{
|
#profile_window{
|
||||||
|
|
Ładowanie…
Reference in New Issue