terriajs
Version:
Geospatial data visualization platform.
341 lines (275 loc) • 15 kB
JSX
'use strict';
const React = require('react');
const PropTypes = require('prop-types');
import createReactClass from 'create-react-class';
const CameraFlightPath = require('terriajs-cesium/Source/Scene/CameraFlightPath');
const Cartesian2 = require('terriajs-cesium/Source/Core/Cartesian2');
const Cartesian3 = require('terriajs-cesium/Source/Core/Cartesian3');
const CesiumMath = require('terriajs-cesium/Source/Core/Math');
const defined = require('terriajs-cesium/Source/Core/defined');
const Ellipsoid = require('terriajs-cesium/Source/Core/Ellipsoid');
const getTimestamp = require('terriajs-cesium/Source/Core/getTimestamp');
const Matrix4 = require('terriajs-cesium/Source/Core/Matrix4');
const Ray = require('terriajs-cesium/Source/Core/Ray');
const Transforms = require('terriajs-cesium/Source/Core/Transforms');
import Styles from './compass.scss';
// the compass on map
const Compass = createReactClass({
propTypes: {
terria: PropTypes.object
},
getInitialState() {
return {
orbitCursorAngle: 0,
heading: 0.0,
orbitCursorOpacity: 0
};
},
componentDidMount() {
this._unsubscribeFromViewerChange = this.props.terria.afterViewerChanged.addEventListener(() => viewerChange(this));
viewerChange(this);
},
componentWillUnmount() {
document.removeEventListener('mousemove', this.orbitMouseMoveFunction, false);
document.removeEventListener('mouseup', this.orbitMouseUpFunction, false);
this._unsubscribeFromClockTick && this._unsubscribeFromClockTick();
this._unsubscribeFromPostRender && this._unsubscribeFromPostRender();
this._unsubscribeFromViewerChange && this._unsubscribeFromViewerChange();
},
handleMouseDown(e) {
if(e.stopPropagation) e.stopPropagation();
if(e.preventDefault) e.preventDefault();
const compassElement = e.currentTarget;
const compassRectangle = e.currentTarget.getBoundingClientRect();
const maxDistance = compassRectangle.width / 2.0;
const center = new Cartesian2((compassRectangle.right - compassRectangle.left) / 2.0, (compassRectangle.bottom - compassRectangle.top) / 2.0);
const clickLocation = new Cartesian2(e.clientX - compassRectangle.left, e.clientY - compassRectangle.top);
const vector = Cartesian2.subtract(clickLocation, center, vectorScratch);
const distanceFromCenter = Cartesian2.magnitude(vector);
const distanceFraction = distanceFromCenter / maxDistance;
const nominalTotalRadius = 145;
const norminalGyroRadius = 50;
if (distanceFraction < norminalGyroRadius / nominalTotalRadius) {
orbit(this, compassElement, vector);
} else if (distanceFraction < 1.0) {
rotate(this, compassElement, vector);
} else {
return true;
}
},
handleDoubleClick(e) {
const scene = this.props.terria.cesium.scene;
const camera = scene.camera;
const windowPosition = windowPositionScratch;
windowPosition.x = scene.canvas.clientWidth / 2;
windowPosition.y = scene.canvas.clientHeight / 2;
const ray = camera.getPickRay(windowPosition, pickRayScratch);
const center = scene.globe.pick(ray, scene, centerScratch);
if (!defined(center)) {
// Globe is barely visible, so reset to home view.
this.props.terria.currentViewer.zoomTo(this.props.terria.homeView, 1.5);
return;
}
const rotateFrame = Transforms.eastNorthUpToFixedFrame(center, Ellipsoid.WGS84);
const lookVector = Cartesian3.subtract(center, camera.position, new Cartesian3());
const flight = CameraFlightPath.createTween(scene, {
destination: Matrix4.multiplyByPoint(rotateFrame, new Cartesian3(0.0, 0.0, Cartesian3.magnitude(lookVector)), new Cartesian3()),
direction: Matrix4.multiplyByPointAsVector(rotateFrame, new Cartesian3(0.0, 0.0, -1.0), new Cartesian3()),
up: Matrix4.multiplyByPointAsVector(rotateFrame, new Cartesian3(0.0, 1.0, 0.0), new Cartesian3()),
duration: 1.5
});
scene.tweens.add(flight);
},
resetRotater() {
this.setState({
orbitCursorOpacity: 0,
orbitCursorAngle: 0
});
},
render() {
const rotationMarkerStyle = {
transform: 'rotate(-' + this.state.orbitCursorAngle + 'rad)',
WebkitTransform: 'rotate(-' + this.state.orbitCursorAngle + 'rad)',
opacity: this.state.orbitCursorOpacity
};
const outerCircleStyle = {
transform: 'rotate(-' + this.state.heading + 'rad)',
WebkitTransform: 'rotate(-' + this.state.heading + 'rad)',
opacity: ''
};
const description = 'Drag outer ring: rotate view.\nDrag inner gyroscope: free orbit.\nDouble-click: reset view.\nTIP: You can also free orbit by holding the CTRL key and dragging the map.';
return (
<div className={Styles.compass} title ={description} onMouseDown ={this.handleMouseDown} onDoubleClick ={this.handleDoubleClick} onMouseUp ={this.resetRotater}>
<div className={Styles.outerRing} style={outerCircleStyle}></div>
<div className={Styles.innerRing} title='Click and drag to rotate the camera'></div>
<div className={Styles.rotationMarker} style={rotationMarkerStyle}></div>
</div>
);
}
});
const vectorScratch = new Cartesian2();
const oldTransformScratch = new Matrix4();
const newTransformScratch = new Matrix4();
const centerScratch = new Cartesian3();
const windowPositionScratch = new Cartesian2();
const pickRayScratch = new Ray();
function rotate(viewModel, compassElement, cursorVector) {
// Remove existing event handlers, if any.
document.removeEventListener('mousemove', viewModel.rotateMouseMoveFunction, false);
document.removeEventListener('mouseup', viewModel.rotateMouseUpFunction, false);
viewModel.rotateMouseMoveFunction = undefined;
viewModel.rotateMouseUpFunction = undefined;
viewModel.isRotating = true;
viewModel.rotateInitialCursorAngle = Math.atan2(-cursorVector.y, cursorVector.x);
const scene = viewModel.props.terria.cesium.scene;
let camera = scene.camera;
const windowPosition = windowPositionScratch;
windowPosition.x = scene.canvas.clientWidth / 2;
windowPosition.y = scene.canvas.clientHeight / 2;
const ray = camera.getPickRay(windowPosition, pickRayScratch);
const viewCenter = scene.globe.pick(ray, scene, centerScratch);
if (!defined(viewCenter)) {
viewModel.rotateFrame = Transforms.eastNorthUpToFixedFrame(camera.positionWC, Ellipsoid.WGS84, newTransformScratch);
viewModel.rotateIsLook = true;
} else {
viewModel.rotateFrame = Transforms.eastNorthUpToFixedFrame(viewCenter, Ellipsoid.WGS84, newTransformScratch);
viewModel.rotateIsLook = false;
}
let oldTransform = Matrix4.clone(camera.transform, oldTransformScratch);
camera.lookAtTransform(viewModel.rotateFrame);
viewModel.rotateInitialCameraAngle = Math.atan2(camera.position.y, camera.position.x);
viewModel.rotateInitialCameraDistance = Cartesian3.magnitude(new Cartesian3(camera.position.x, camera.position.y, 0.0));
camera.lookAtTransform(oldTransform);
viewModel.rotateMouseMoveFunction = function(e) {
const compassRectangle = compassElement.getBoundingClientRect();
const center = new Cartesian2((compassRectangle.right - compassRectangle.left) / 2.0, (compassRectangle.bottom - compassRectangle.top) / 2.0);
const clickLocation = new Cartesian2(e.clientX - compassRectangle.left, e.clientY - compassRectangle.top);
const vector = Cartesian2.subtract(clickLocation, center, vectorScratch);
const angle = Math.atan2(-vector.y, vector.x);
const angleDifference = angle - viewModel.rotateInitialCursorAngle;
const newCameraAngle = CesiumMath.zeroToTwoPi(viewModel.rotateInitialCameraAngle - angleDifference);
camera = viewModel.props.terria.cesium.scene.camera;
oldTransform = Matrix4.clone(camera.transform, oldTransformScratch);
camera.lookAtTransform(viewModel.rotateFrame);
const currentCameraAngle = Math.atan2(camera.position.y, camera.position.x);
camera.rotateRight(newCameraAngle - currentCameraAngle);
camera.lookAtTransform(oldTransform);
viewModel.props.terria.cesium.notifyRepaintRequired();
};
viewModel.rotateMouseUpFunction = function(e) {
viewModel.isRotating = false;
document.removeEventListener('mousemove', viewModel.rotateMouseMoveFunction, false);
document.removeEventListener('mouseup', viewModel.rotateMouseUpFunction, false);
viewModel.rotateMouseMoveFunction = undefined;
viewModel.rotateMouseUpFunction = undefined;
};
document.addEventListener('mousemove', viewModel.rotateMouseMoveFunction, false);
document.addEventListener('mouseup', viewModel.rotateMouseUpFunction, false);
}
function orbit(viewModel, compassElement, cursorVector) {
// Remove existing event handlers, if any.
document.removeEventListener('mousemove', viewModel.orbitMouseMoveFunction, false);
document.removeEventListener('mouseup', viewModel.orbitMouseUpFunction, false);
if (defined(viewModel.orbitTickFunction)) {
viewModel.props.terria.clock.onTick.removeEventListener(viewModel.orbitTickFunction);
}
viewModel.orbitMouseMoveFunction = undefined;
viewModel.orbitMouseUpFunction = undefined;
viewModel.orbitTickFunction = undefined;
viewModel.isOrbiting = true;
viewModel.orbitLastTimestamp = getTimestamp();
let scene = viewModel.props.terria.cesium.scene;
let camera = scene.camera;
const windowPosition = windowPositionScratch;
windowPosition.x = scene.canvas.clientWidth / 2;
windowPosition.y = scene.canvas.clientHeight / 2;
const ray = camera.getPickRay(windowPosition, pickRayScratch);
let center = scene.globe.pick(ray, scene, centerScratch);
if (!defined(center)) {
viewModel.orbitFrame = Transforms.eastNorthUpToFixedFrame(camera.positionWC, Ellipsoid.WGS84, newTransformScratch);
viewModel.orbitIsLook = true;
} else {
viewModel.orbitFrame = Transforms.eastNorthUpToFixedFrame(center, Ellipsoid.WGS84, newTransformScratch);
viewModel.orbitIsLook = false;
}
viewModel.orbitTickFunction = function(e) {
const timestamp = getTimestamp();
const deltaT = timestamp - viewModel.orbitLastTimestamp;
const rate = (viewModel.state.orbitCursorOpacity - 0.5) * 2.5 / 1000;
const distance = deltaT * rate;
const angle = viewModel.state.orbitCursorAngle + CesiumMath.PI_OVER_TWO;
const x = Math.cos(angle) * distance;
const y = Math.sin(angle) * distance;
scene = viewModel.props.terria.cesium.scene;
camera = scene.camera;
const oldTransform = Matrix4.clone(camera.transform, oldTransformScratch);
camera.lookAtTransform(viewModel.orbitFrame);
if (viewModel.orbitIsLook) {
camera.look(Cartesian3.UNIT_Z, -x);
camera.look(camera.right, -y);
} else {
camera.rotateLeft(x);
camera.rotateUp(y);
}
camera.lookAtTransform(oldTransform);
viewModel.props.terria.cesium.notifyRepaintRequired();
viewModel.orbitLastTimestamp = timestamp;
};
function updateAngleAndOpacity(vector, compassWidth) {
const angle = Math.atan2(-vector.y, vector.x);
viewModel.setState({
orbitCursorAngle: CesiumMath.zeroToTwoPi(angle - CesiumMath.PI_OVER_TWO)
});
const distance = Cartesian2.magnitude(vector);
const maxDistance = compassWidth / 2.0;
const distanceFraction = Math.min(distance / maxDistance, 1.0);
const easedOpacity = 0.5 * distanceFraction * distanceFraction + 0.5;
viewModel.setState({
orbitCursorOpacity: easedOpacity
});
viewModel.props.terria.cesium.notifyRepaintRequired();
}
viewModel.orbitMouseMoveFunction = function(e) {
const compassRectangle = compassElement.getBoundingClientRect();
center = new Cartesian2((compassRectangle.right - compassRectangle.left) / 2.0, (compassRectangle.bottom - compassRectangle.top) / 2.0);
const clickLocation = new Cartesian2(e.clientX - compassRectangle.left, e.clientY - compassRectangle.top);
const vector = Cartesian2.subtract(clickLocation, center, vectorScratch);
updateAngleAndOpacity(vector, compassRectangle.width);
};
viewModel.orbitMouseUpFunction = function(e) {
// TODO: if mouse didn't move, reset view to looking down, north is up?
viewModel.isOrbiting = false;
document.removeEventListener('mousemove', viewModel.orbitMouseMoveFunction, false);
document.removeEventListener('mouseup', viewModel.orbitMouseUpFunction, false);
if (defined(viewModel.orbitTickFunction)) {
viewModel.props.terria.clock.onTick.removeEventListener(viewModel.orbitTickFunction);
}
viewModel.orbitMouseMoveFunction = undefined;
viewModel.orbitMouseUpFunction = undefined;
viewModel.orbitTickFunction = undefined;
};
document.addEventListener('mousemove', viewModel.orbitMouseMoveFunction, false);
document.addEventListener('mouseup', viewModel.orbitMouseUpFunction, false);
viewModel._unsubscribeFromClockTick = viewModel.props.terria.clock.onTick.addEventListener(viewModel.orbitTickFunction);
updateAngleAndOpacity(cursorVector, compassElement.getBoundingClientRect().width);
}
function viewerChange(viewModel) {
if (defined(viewModel.props.terria.cesium)) {
if (viewModel._unsubscribeFromPostRender) {
viewModel._unsubscribeFromPostRender();
viewModel._unsubscribeFromPostRender = undefined;
}
viewModel._unsubscribeFromPostRender = viewModel.props.terria.cesium.scene.postRender.addEventListener(function() {
viewModel.setState({
heading: viewModel.props.terria.cesium.scene.camera.heading
});
});
} else {
if (viewModel._unsubscribeFromPostRender) {
viewModel._unsubscribeFromPostRender();
viewModel._unsubscribeFromPostRender = undefined;
}
viewModel.showCompass = false;
}
}
module.exports = Compass;