terriajs
Version:
Geospatial data visualization platform.
480 lines (479 loc) • 23.6 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* This could use a lot of work, for example, due to the way both of:
* - how the component is currently composed
* - how it's currently hooked into the cesium viewer
* we needlessly force re-render it all even though there is no change to orbit
* or heading
*
* You'll also see a few weird numbers - this is due to the port from the scss
* styles, and will be leaving it as is for now
*/
//
import debounce from "lodash-es/debounce";
import { computed, runInAction, when } from "mobx";
import { PureComponent } from "react";
import { withTranslation } from "react-i18next";
import styled, { withTheme } from "styled-components";
import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import Matrix4 from "terriajs-cesium/Source/Core/Matrix4";
import Ray from "terriajs-cesium/Source/Core/Ray";
import Transforms from "terriajs-cesium/Source/Core/Transforms";
import getTimestamp from "terriajs-cesium/Source/Core/getTimestamp";
import CameraFlightPath from "terriajs-cesium/Source/Scene/CameraFlightPath";
import compassRotationMarker from "../../../../../../wwwroot/images/compass-rotation-marker.svg";
import isDefined from "../../../../../Core/isDefined";
import Box from "../../../../../Styled/Box";
import Icon, { StyledIcon } from "../../../../../Styled/Icon";
import { withTerriaRef } from "../../../../HOCs/withTerriaRef";
import FadeIn from "../../../../Transitions/FadeIn/FadeIn";
import { GyroscopeGuidance } from "./GyroscopeGuidance";
export const COMPASS_LOCAL_PROPERTY_KEY = "CompassHelpPrompted";
const StyledCompass = styled.div `
display: none;
position: relative;
width: ${(props) => props.theme.compassWidth}px;
height: ${(props) => props.theme.compassWidth}px;
@media (min-width: ${(props) => props.theme.sm}px) {
display: block;
}
`;
/**
* Take a compass width and scale it up 10px, instead of hardcoding values like:
* // const compassScaleRatio = 66 / 56;
*/
const getCompassScaleRatio = (compassWidth) => (Number(compassWidth) + 10) / Number(compassWidth);
/**
* You think 0.9999 is a joke but I kid you not, it's the root of all evil in
* bandaging these related issues:
* https://github.com/TerriaJS/terriajs/issues/4261
* https://github.com/TerriaJS/terriajs/pull/4262
* https://github.com/TerriaJS/terriajs/pull/4213
*
* It seems the rendering in Chrome means that in certain conditions
* - chrome (not even another webkit browser)
* - "default browser zoom" (doesn't happen when you are even at 110%, but will
* when shrunk down enough)
* - the way our compass is composed
*
* The action of triggering the 'active' state (scaled up to
* `getCompassScaleRatio()`) & back down means that the "InnerRing" will look
* off-center by 0.5-1px until you switch windows/tabs away and back, then
* chrome will decide to render it in the correct position.
*
* I haven't dug further to the root cause as doing it like this means wew now
* have a beautiful animating compass.
*
* So please leave scale(0.9999) alone unless you can fix the rendering issue in
* chrome, or if you want to develop a burning hatred for the compass 🙏🔥
*
**/
const StyledCompassOuterRing = styled.div `
${(props) => props.theme.centerWithoutFlex()}
// override the transform provided in centerWithoutFlex()
transform: translate(-50%,-50%) scale(0.9999);
z-index: ${(props) => (props.active ? "2" : "1")};
width: 100%;
${(props) => props.active &&
`transform: translate(-50%,-50%) scale(${getCompassScaleRatio(props.theme.compassWidth)});`};
transition: transform 0.3s;
`;
const StyledCompassInnerRing = styled.div `
${(props) => props.theme.verticalAlign()}
width: ${(props) => Number(props.theme.compassWidth) - Number(props.theme.ringWidth) - 10}px;
height: ${(props) => Number(props.theme.compassWidth) - Number(props.theme.ringWidth) - 10}px;
margin: 0 auto;
padding: 4px;
box-sizing: border-box;
`;
const StyledCompassRotationMarker = styled.div `
${(props) => props.theme.centerWithoutFlex()}
z-index: 3;
cursor: pointer;
width: ${(props) => Number(props.theme.compassWidth) + Number(props.theme.ringWidth) - 4}px;
height: ${(props) => Number(props.theme.compassWidth) + Number(props.theme.ringWidth) - 4}px;
border-radius: 50%;
background-repeat: no-repeat;
background-size: contain;
`;
// the compass on map
class Compass extends PureComponent {
_unsubscribeFromPostRender;
_unsubscribeFromAnimationFrame;
_unsubscribeFromViewerChange;
orbitMouseMoveFunction;
orbitMouseUpFunction;
rotateMouseMoveFunction;
rotateMouseUpFunction;
isRotating = false;
rotateInitialCursorAngle = 0;
rotateFrame;
rotateIsLook = false;
rotateInitialCameraAngle = 0;
rotateInitialCameraDistance = 0;
orbitFrame;
orbitIsLook = false;
orbitLastTimestamp = 0;
isOrbiting = false;
orbitAnimationFrameFunction;
showCompass;
/**
* @param {Props} props
*/
constructor(props) {
super(props);
this.state = {
orbitCursorAngle: 0,
heading: 0.0,
orbitCursorOpacity: 0,
active: false,
activeForTransition: false
};
when(() => isDefined(this.cesiumViewer), () => this.cesiumLoaded());
}
get cesiumViewer() {
return this.props.terria.cesium;
}
cesiumLoaded() {
this._unsubscribeFromViewerChange =
this.props.terria.mainViewer.afterViewerChanged.addEventListener(() => viewerChange(this));
viewerChange(this);
}
componentWillUnmount() {
if (this.orbitMouseMoveFunction) {
document.removeEventListener("mousemove", this.orbitMouseMoveFunction, false);
}
if (this.orbitMouseUpFunction) {
document.removeEventListener("mouseup", this.orbitMouseUpFunction, false);
}
if (this._unsubscribeFromAnimationFrame) {
this._unsubscribeFromAnimationFrame();
}
if (this._unsubscribeFromPostRender) {
this._unsubscribeFromPostRender();
}
if (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 nominalGyroRadius = 50;
if (distanceFraction < nominalGyroRadius / 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 = isDefined(ray)
? scene.globe.pick(ray, scene, centerScratch)
: undefined;
if (!isDefined(center)) {
// Globe is barely visible, so reset to home view.
this.props.terria.currentViewer.zoomTo(this.props.terria.mainViewer.homeCamera, 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 { t } = this.props;
const active = this.state.active;
const description = t("compass.description");
const showGuidance = !this.props.viewState.terria.getLocalProperty(COMPASS_LOCAL_PROPERTY_KEY);
return (_jsxs(StyledCompass, { onMouseDown: this.handleMouseDown.bind(this), onDoubleClick: this.handleDoubleClick.bind(this), onMouseUp: this.resetRotater.bind(this), active: active, children: [_jsx(StyledCompassOuterRing, { active: false, children: _jsx("div", { style: outerCircleStyle, children: _jsx(StyledIcon, { fillColor: this.props.theme.darkWithOverlay,
// if it's active hide outer ring
glyph: active ? null : Icon.GLYPHS.compassOuter }) }) }), _jsx(StyledCompassOuterRing, { active: active, title: description, "aria-hidden": "true", role: "presentation", children: _jsx("div", { ref: this.props.refFromHOC, style: outerCircleStyle, children: _jsx(StyledIcon, { fillColor: this.props.theme.darkWithOverlay, glyph: Icon.GLYPHS.compassOuter }) }) }), _jsx(StyledCompassInnerRing, { title: t("compass.title"), children: _jsx(StyledIcon, { fillColor: this.props.theme.darkWithOverlay, glyph: active ? Icon.GLYPHS.compassInnerArrows : Icon.GLYPHS.compassInner }) }), _jsx(StyledCompassRotationMarker, { title: description, style: {
backgroundImage: compassRotationMarker
}, onMouseOver: () => this.setState({ active: true }), onMouseOut: () => {
if (showGuidance) {
this.setState({ active: true });
}
else {
this.setState({ active: false });
}
},
// do we give focus to this? given it's purely a mouse tool
// focus it anyway..
tabIndex: 0, onFocus: () => this.setState({ active: true }), children: _jsx("div", { style: rotationMarkerStyle, children: _jsx(StyledIcon, { fillColor: this.props.theme.darkWithOverlay, glyph: Icon.GLYPHS.compassRotationMarker }) }) }), showGuidance && (_jsx(FadeIn, { isVisible: active, children: _jsx(Box, { css: `
${(p) => p.theme.verticalAlign("absolute")}
direction: rtl;
right: 55px;
`, children: _jsx(GyroscopeGuidance, { viewState: this.props.viewState,
// handleHelp={() => {
// this.props.viewState.showHelpPanel();
// this.props.viewState.selectHelpMenuItem("navigation");
// }}
onClose: () => this.setState({ active: false }) }) }) }))] }));
}
}
__decorate([
computed
], Compass.prototype, "cesiumViewer", null);
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.
if (viewModel.rotateMouseMoveFunction) {
document.removeEventListener("mousemove", viewModel.rotateMouseMoveFunction, false);
}
if (viewModel.rotateMouseUpFunction) {
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 = isDefined(ray)
? scene.globe.pick(ray, scene, centerScratch)
: undefined;
if (!isDefined(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;
if (viewModel.rotateMouseMoveFunction) {
document.removeEventListener("mousemove", viewModel.rotateMouseMoveFunction, false);
}
if (viewModel.rotateMouseUpFunction) {
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.
if (viewModel.orbitMouseMoveFunction) {
document.removeEventListener("mousemove", viewModel.orbitMouseMoveFunction, false);
}
if (viewModel.orbitMouseUpFunction) {
document.removeEventListener("mouseup", viewModel.orbitMouseUpFunction, false);
}
if (viewModel._unsubscribeFromAnimationFrame) {
viewModel._unsubscribeFromAnimationFrame();
}
viewModel._unsubscribeFromAnimationFrame = undefined;
viewModel.orbitMouseMoveFunction = undefined;
viewModel.orbitMouseUpFunction = undefined;
viewModel.orbitAnimationFrameFunction = undefined;
viewModel.isOrbiting = true;
viewModel.orbitLastTimestamp = getTimestamp();
const scene = viewModel.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 = isDefined(ray)
? scene.globe.pick(ray, scene, centerScratch)
: undefined;
if (!isDefined(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.orbitAnimationFrameFunction = 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;
const scene = viewModel.props.terria.cesium.scene;
const 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();
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);
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;
if (viewModel.orbitMouseMoveFunction) {
document.removeEventListener("mousemove", viewModel.orbitMouseMoveFunction, false);
}
if (viewModel.orbitMouseUpFunction) {
document.removeEventListener("mouseup", viewModel.orbitMouseUpFunction, false);
}
if (viewModel._unsubscribeFromAnimationFrame) {
viewModel._unsubscribeFromAnimationFrame();
}
viewModel._unsubscribeFromAnimationFrame = undefined;
viewModel.orbitMouseMoveFunction = undefined;
viewModel.orbitMouseUpFunction = undefined;
viewModel.orbitAnimationFrameFunction = undefined;
};
document.addEventListener("mousemove", viewModel.orbitMouseMoveFunction, false);
document.addEventListener("mouseup", viewModel.orbitMouseUpFunction, false);
subscribeToAnimationFrame(viewModel);
updateAngleAndOpacity(cursorVector, compassElement.getBoundingClientRect().width);
}
function subscribeToAnimationFrame(viewModel) {
viewModel._unsubscribeFromAnimationFrame = ((id) => () => cancelAnimationFrame(id))(requestAnimationFrame(() => {
if (isDefined(viewModel.orbitAnimationFrameFunction)) {
viewModel.orbitAnimationFrameFunction();
}
subscribeToAnimationFrame(viewModel);
}));
}
function viewerChange(viewModel) {
runInAction(() => {
if (isDefined(viewModel.props.terria.cesium)) {
if (viewModel._unsubscribeFromPostRender) {
viewModel._unsubscribeFromPostRender();
viewModel._unsubscribeFromPostRender = undefined;
}
viewModel._unsubscribeFromPostRender =
viewModel.props.terria.cesium.scene.postRender.addEventListener(debounce(function (scene) {
if (scene.view) {
viewModel.setState({
heading: scene.camera.heading
});
}
}, 200, {
maxWait: 200,
leading: true,
trailing: true
}));
}
else {
if (viewModel._unsubscribeFromPostRender) {
viewModel._unsubscribeFromPostRender();
viewModel._unsubscribeFromPostRender = undefined;
}
viewModel.showCompass = false;
}
});
}
export const COMPASS_NAME = "MapNavigationCompassOuterRing";
export const COMPASS_TOOL_ID = "compass";
export default withTranslation()(withTheme(withTerriaRef(Compass, COMPASS_NAME)));
//# sourceMappingURL=Compass.js.map