From 3f9804605681b3b12dbc7ccafedde29a939887e5 Mon Sep 17 00:00:00 2001 From: Nanda Date: Mon, 17 Feb 2020 13:11:30 +1100 Subject: [PATCH 1/2] Port AR. --- lib/Models/AugmentedVirtuality.ts | 892 ++++++++++++++++++ .../Navigation/AugmentedVirtualityTool.jsx | 93 +- .../augmented_virtuality_tool.scss.d.ts | 16 + 3 files changed, 954 insertions(+), 47 deletions(-) create mode 100644 lib/Models/AugmentedVirtuality.ts create mode 100644 lib/ReactViews/Map/Navigation/augmented_virtuality_tool.scss.d.ts diff --git a/lib/Models/AugmentedVirtuality.ts b/lib/Models/AugmentedVirtuality.ts new file mode 100644 index 00000000000..22fd73fa472 --- /dev/null +++ b/lib/Models/AugmentedVirtuality.ts @@ -0,0 +1,892 @@ +import { observable, action, computed } from "mobx"; +import CesiumMath from "terriajs-cesium/Source/Core/Math"; +import CesiumMatrix3 from "terriajs-cesium/Source/Core/Matrix3"; +import CesiumCartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; +import EllipsoidTerrainProvider from "terriajs-cesium/Source/Core/EllipsoidTerrainProvider"; +import Camera from "terriajs-cesium/Source/Scene/Camera"; +import Scene from "terriajs-cesium/Source/Scene/Scene"; +import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; +import Terria from "./Terria"; + +const sampleTerrainMostDetailed = require("terriajs-cesium/Source/Core/sampleTerrainMostDetailed") + .default; + +interface EventLoopState { + intervalId?: any; +} + +interface TerriaOrientation { + orientation: { + roll: number; + pitch: number; + heading: number; + }; + destination?: CesiumCartesian3; +} + +export default class AugmentedVirtuality { + /** + * Gets the the maximum number of times that the camera orientation will be + * updated per second by default. This is the number of camera orientation + * updates per seconds is capped to by default (explicitly the number of times + * the orientation is updated per second might be less but it won't be more + * then this number). We want the number of times that the orientation is + * updated capped so that we don't consume to much battery life updating to + * frequently, but responsiveness is still acceptable. + */ + static readonly DEFAULT_MAXIMUM_UPDATES_PER_SECOND = 10.0; + + /** + * The minimum height that the viewer is allowed to hover at. + */ + static readonly MINIMUM_HOVER_HEIGHT = 20.0; + + /* These are the heights that we can toggle through (in meters - above the surface height). + */ + static readonly PRESET_HEIGHTS = [1000, 250, 20]; + + @observable private manualAlignment = false; + @observable private eventLoopState: EventLoopState = {}; + @observable private orientationUpdated = false; + @observable private alpha = 0; + @observable private beta = 0; + @observable private gamma = 0; + @observable private realignAlpha = 0; + @observable private realignHeading = 0; + @observable private lastScreenOrientation?: number; + + @observable private maximumUpdatesPerSecond = + AugmentedVirtuality.DEFAULT_MAXIMUM_UPDATES_PER_SECOND; + + // Set the default height to be the last height so that when we first toggle + // (and increment) we cycle and go to the first height. + @observable private hoverLevel = + AugmentedVirtuality.PRESET_HEIGHTS.length - 1; + + constructor(readonly terria: Terria) { + // Always run the device orientation event, this way as soon as we enable we + // know where we are and set the orientation rather then having to wait for + // the next update. The following is disabled because chrome does not + // currently support deviceorientationabsolute correctly: if + // ('ondeviceorientationabsolute' in window) { + // window.addEventListener('deviceorientationabsolute', function(event) + // {that._orientationUpdate(event);} ); } else + if ("ondeviceorientation" in window) { + window.addEventListener("deviceorientation", event => + this.storeOrientation(event) + ); + } + } + + @computed + get scene(): Scene | undefined { + return this.terria.cesium && this.terria.cesium.scene; + } + + @computed + get camera(): Camera | undefined { + return this.terria.cesium && this.terria.cesium.scene.camera; + } + + @computed + get isEnabled() { + return this.isEventLoopRunning || this.manualAlignment; + } + + @action + setEnabled(enable: boolean) { + if (enable === false) this.resetAlignment(); + if (enable !== this.isEnabled) { + this.manualAlignment = false; + this.startEventLoop(enable); + } + } + + toggleEnabled() { + this.setEnabled(!this.isEnabled); + } + + /** + * Toggles whether manual alignement is enabled or disabled. + */ + @action + toggleManualAlignment() { + this.manualAlignment = !this.manualAlignment; + } + + @computed + private get isEventLoopRunning() { + return this.eventLoopState.intervalId !== undefined; + } + + /** + * Toggles the viewer between a range of predefined heights, setting the + * cameras orientation so that it matches the * correct orientation. + */ + @action + toggleHoverHeight() { + this.hoverLevel = + (this.hoverLevel + 1) % AugmentedVirtuality.PRESET_HEIGHTS.length; + this.hover(AugmentedVirtuality.PRESET_HEIGHTS[this.hoverLevel]); + } + + /** Moves the viewer to a specified height, setting the orientation so that + * it matches the correct Augmented Virtuality * orientation. + * + * @param height The height in Meters above the globe surface. Note: If height is below + * {@link AugmentedVirtuality.MINIMUM_HOVER_HEIGHT} the height will be set to + * {@link AugmentedVirtuality.MINIMUM_HOVER_HEIGHT} to avoid visual artifacts when the viewer + * becomes to close to the surface. + * + * @param [position] The location to hover over. If not specified the current camera location will be used. + * @param [flyTo=true] Whether to fly to the location (true) or whether to jump to the location (false). + */ + private hover(height: number, position?: Cartographic, flyTo?: boolean) { + // Get access to the camera...if it is not avaliable we can't set the new height so just return now. + if (!this.camera) return; + const camera = this.camera; + const hoverPosition = position + ? position + : camera.positionCartographic.clone(); + + flyTo = flyTo === undefined ? true : flyTo; + + // Clamp the minimum hover height (heights below this value could lead to poor visual artifacts). + if (height < AugmentedVirtuality.MINIMUM_HOVER_HEIGHT) { + height = AugmentedVirtuality.MINIMUM_HOVER_HEIGHT; + } + + // Reset the viewer height. + const flyToHeight = (surfaceHeight: number) => { + height += surfaceHeight; + const newPosition = CesiumCartesian3.fromRadians( + hoverPosition.longitude, + hoverPosition.latitude, + height + ); + const pose = { + destination: newPosition, + ...this.getCurrentOrientation() + }; + if (flyTo) { + camera.flyTo(pose); + } else { + camera.setView(pose); + } + + // Needed on mobile to make sure that the render is marked as dirty so + // that once AV mode has been disabled for a while and then is reenabled + // the .setView() function still has effect (otherwise dispite the call + // the .setView() the view orientation does not visually update until the + // user manualy moves the camera position). + this.terria.currentViewer.notifyRepaintRequired(); + }; + + // Get the ground surface height at this location and offset the height by it. + if ( + !this.scene || + !this.scene.terrainProvider || + this.scene.terrainProvider instanceof EllipsoidTerrainProvider + ) { + // If we can't get access to the terrain provider or we can get access to + // the terrain provider and the provider is just the Ellipsoid then use + // the height of 0. + flyToHeight(0); + } else { + const terrainProvider = this.scene.terrainProvider; + sampleTerrainMostDetailed(terrainProvider, [position]).then(function( + updatedPosition: Cartographic[] + ) { + flyToHeight(updatedPosition[0].height); + }); + } + } + + /** + * Moves the viewer to a specified location while maintaining the current + * height and the correct Augmented Virtuality * orientation. + * + * @param position The location to hover move to. + + * @param [maximumHeight] The maximum height (in meters) to cap the current + * camera height to (if this value is specified and the viewer is above this + * height the camera will be restricted to this height). + + * @param [flyTo] Whether to fly to the location (true) or whether to jump to + * the location (false). + * + * When the manual alignment is enabled this function has no effect. + */ + moveTo(position: Cartographic, maximumHeight: number, flyTo: boolean) { + // If we are in manual alignment mode we don't allow the viewer to move + // (since this would create a jaring UX for most use cases). + if (this.manualAlignment) return; + + // Get access to the camera...if it is not avaliable we can't get the + // current height (or set the new location) so just return now. + if (this.camera === undefined) return; + const camera = this.camera; + const cameraPosition = camera.positionCartographic.clone(); + const viewerHeight = cameraPosition.height; + + // Reset the viewer height. + const moveToLocation = (surfaceHeight: number) => { + let hoverHeight = viewerHeight - surfaceHeight; + if (hoverHeight > maximumHeight) hoverHeight = maximumHeight; + this.hover(hoverHeight, position, flyTo); + }; + + const scene = this.scene; + // Get the ground surface height at this location and offset the height by it. + if ( + scene === undefined || + scene.terrainProvider === undefined || + scene.terrainProvider instanceof EllipsoidTerrainProvider + ) { + // If we can't get access to the terrain provider or we can get access to + // the terrain provider and the provider is just the Ellipsoid then use + // the height of 0. + moveToLocation(0); + } else { + const terrainProvider = scene.terrainProvider; + sampleTerrainMostDetailed(terrainProvider, [cameraPosition]).then( + function(updatedPosition: Array) { + moveToLocation(updatedPosition[0].height); + } + ); + } + } + + /** + * Whether the user is currently setting a manual alignment. + * + * @return Whether the user is currently setting a manual alignment (true) or not (false). + */ + private getManualAlignment(): boolean { + return this.isEnabled && this.manualAlignment; + } + + /** + * Starts / stops manual alignment. + * + * When manual realignment is enabled it allows the user to specify a new + * origin for the alignment between the devices physical and virtual + * alignment. When manual alignment is enabled the orientation is locked, to + * allow the user to realign a visual landmark with a physical landmark. + * + * Note: Manual alignment is only done for the heading axis, this is because in + * practice we have found that the heading axis is often out as mobile devices + * seem to have difficulty obtaining the compass direction, but seem to perform + * relatively well in the other axes. + * + * Note: Realignment is only possible when AugmentedVirtuality is enabled. If + * AugmentedVirtuality is disabled while manual alignment is in progress + * it will be cancelled. + * + * + * @param startEnd Whether the user is starting (true) or ending (false) the realignment. + */ + private setManualAlignment(startEnd: boolean) { + // Only allow manual alignment changes when the module is enabled. + if (this.isEnabled === false) return; + + if (startEnd === false && this.camera !== undefined) { + this.realignAlpha = this.alpha; + this.realignHeading = CesiumMath.toDegrees(this.camera.heading); + } + + if (this.manualAlignment !== startEnd) { + this.manualAlignment = startEnd; + this.startEventLoop(!this.manualAlignment); + } + } + + /** + * Resets the alignment so that the alignement matches the devices absolute alignment. + */ + private resetAlignment() { + this.orientationUpdated = true; + this.realignAlpha = 0; + this.realignHeading = 0; + } + + /** + * Start or stop the Augmented Virutuality mode event loop. When enabled the + * orientation will effect the cameras view and when disabled the device + * orientation will not effect the cameras view. + * + * @param enable Whether to start the event loop (true) or stop the event loop (false). + */ + private startEventLoop(enable: boolean) { + if (this.isEventLoopRunning === enable) return; + if (enable === true) { + this.orientationUpdated = true; + const intervalMs = 1000 / this.maximumUpdatesPerSecond; + const id: any = setInterval(() => this.updateOrientation(), intervalMs); + this.eventLoopState = { intervalId: id }; + } else { + clearInterval(this.eventLoopState.intervalId); + this.eventLoopState = {}; + } + } + + /** + * Device orientation update event callback function. Stores the updated + * orientation into the object state. + * + * @param event Contains the updated device orientation (in .alpha, .beta, .gamma). + */ + storeOrientation(event: DeviceOrientationEvent) { + const { alpha, beta, gamma } = event; + if (alpha !== null && beta !== null && gamma !== null) { + this.alpha = alpha; + this.beta = beta; + this.gamma = gamma; + this.orientationUpdated = true; + } + } + + /** + * This function updates the cameras orientation using the last orientation + * recorded and the current screen orientation. + * + */ + private updateOrientation() { + // Check if the screen orientation has changed and mark the orientation updated if it has. + const screenOrientation = getCurrentScreenOrientation(); + if (screenOrientation !== this.lastScreenOrientation) + this.orientationUpdated = true; + this.lastScreenOrientation = screenOrientation; + + // Optimize by only updating the camera view if some part of the + // orientation calculation has changed. + if (!this.orientationUpdated) { + // The orientation has not been updated so don't waste time changing the + // orientation. + return; + } + this.orientationUpdated = false; + + // Get access to the camera...if it is not avaliable we can't set the orientation so just return now. + if (this.camera) { + this.camera.setView(this.getCurrentOrientation(screenOrientation)); + + // Needed on mobile to make sure that the render is marked as dirty so that + // once AV mode has been disabled for a while and then is reenabled the + // .setView() function still has effect (otherwise dispite the call the + // .setView() the view orientation does not visually update until the user + // manualy moves the camera position). + this.terria.currentViewer.notifyRepaintRequired(); + } + } + + /** + * Gets the current orientation stored in the object state and returns the + * roll, pitch and heading which can be used to set the cameras orientation. + * + * @param screenOrientation The screen orientation in degrees. Note: This + * field is optional, if supplied this value will be used for the screen + * orientation, otherwise the screen orientation will be obtained during the + * execution of this function. + * + * @return A object with the roll, pitch and heading stored into the orientation. + */ + private getCurrentOrientation(screenOrientation?: number) { + const alpha = this.alpha; + const beta = this.beta; + const gamma = this.gamma; + + const realignAlpha = this.realignAlpha; + const realignHeading = this.realignHeading; + + if (screenOrientation === undefined) + screenOrientation = getCurrentScreenOrientation(); + return computeTerriaOrientation( + alpha, + beta, + gamma, + screenOrientation, + realignAlpha, + realignHeading + ); + } +} + +function getCurrentScreenOrientation(): number { + if (screen.orientation && screen.orientation.angle !== undefined) + return screen.orientation.angle; + return 0; +} + +/** + * Turns the orientation in the device frame of reference into an orientation + * suitable for specifying the Terria camera orientation. + * + * @param alpha The alpha value of the device orientation in degrees (this is + * the alpha value in the device's frame of reference). + * + * @param beta The beta value of the device orientation in degrees (this is the + * beta value in the device's frame of reference). + * + * @param gamma The gamma value of the device orientation in degrees (this is + * the gamma value in the device's frame of reference). + * + * @param screenOrientation The screen orientation in degrees. + * + * @param realignAlpha The value of the alpha value the last time realignment + * was completed (supply zero if realignment is not supported). + * + * @param realignHeading The value of the heading value the last time + * realignment was completed (supply zero if realignment is not supported). + * + * @return An object with the roll, pitch and heading stored into the orientation. + */ +function computeTerriaOrientation( + alpha: number, + beta: number, + gamma: number, + screenOrientation: number, + realignAlpha: number, + realignHeading: number +): TerriaOrientation { + // Note: The algorithmic formulation in this function is for simplicity of + // mathematical expression, readability, maintainability and + // modification (i.e. it is easy to understand how to update or insert + // new offsets or features). This is not the simplest form which + // clearly flows from the current formuleation and clearly simplify the + // logic and operations but would increase the cost of future + // modifications and reduce the readability of the expression. It is + // not anticipated that the current verbose implementation would have a + // significant impact on performance or accuracy, but obviously there + // will be some impact on both and it can be simplified in future if + // needed. + + const rotation = CesiumMatrix3.clone(CesiumMatrix3.IDENTITY); + let rotationIncrement; + + // Roll - Counteract the change in the (orientation) frame of reference when + // the screen is rotated and the rotation lock is not on (the browser + // reorients the frame of reference to align with the new screen + // orientation - where as we want it of the device relative to the + // world). + rotationIncrement = CesiumMatrix3.fromRotationZ( + CesiumMath.toRadians(screenOrientation) + ); + CesiumMatrix3.multiply(rotation, rotationIncrement, rotation); + + // Pitch - Align the device orientation frame with the ceasium orientation frame. + rotationIncrement = CesiumMatrix3.fromRotationX(CesiumMath.toRadians(90)); + CesiumMatrix3.multiply(rotation, rotationIncrement, rotation); + + // Roll - Apply the deivce roll. + rotationIncrement = CesiumMatrix3.fromRotationZ(CesiumMath.toRadians(gamma)); + CesiumMatrix3.multiply(rotation, rotationIncrement, rotation); + + // Pitch - Apply the deivce pitch. + rotationIncrement = CesiumMatrix3.fromRotationX(CesiumMath.toRadians(-beta)); + CesiumMatrix3.multiply(rotation, rotationIncrement, rotation); + + // Heading - Apply the incremental deivce heading (from when start was last triggered). + rotationIncrement = CesiumMatrix3.fromRotationY( + CesiumMath.toRadians(-(alpha - realignAlpha)) + ); + CesiumMatrix3.multiply(rotation, rotationIncrement, rotation); + + // Heading - Use the offset when the orientation was last started. Note: + // This is logically different from the alpha value and can only be + // applied here in the same way since Cesium camera is RPH (Heading + // last - most local). See Cesium camera rotation decomposition for + // more information on the Cesium camera formuleation. + rotationIncrement = CesiumMatrix3.fromRotationY( + CesiumMath.toRadians(realignHeading) + ); + CesiumMatrix3.multiply(rotation, rotationIncrement, rotation); + + // Decompose rotation matrix into roll, pitch and heading to supply to Cesium camera. + // + // Use notation: + // R = Roll, P = Pitch, H = Heading + // SH = Sin(Heading), CH = Cos(Heading) + // + // Ceasium camera rotation = RPH: + // [ CR, -SR, 0][ 1, 0, 0][ CH, 0, SH] [CRCH-SRSPSH, -SRCP, CRSH-SRSPCH] + // [ SR, CR, 0][ 0, CP, SP][ 0, 1, 0] = [SRCH-CRSPSH, CRCP, SRSH+CRSPCH] + // [ 0, 0, 1][ 0, -SP, CP][-SH, 0, CH] [ -CPSH , -SP, CPCH ] + // Note: The sign difference of the Sin terms in pitch is different to the standard right handed rotation since + // Cesium rotates pitch in the left handed direction. Both heading and roll are right handed rotations. + // + // Use the following notation to refer to elements in the Cesium camera rotation matrix: + // [R00, R10, R20] + // [R01, R11, R21] + // [R02, R12, R22] + // + // Also note: Tan(X) = Sin(X) / Cos(X) + // + // Decompose matrix: + // H = ATan(Tan(H)) = ATan(Sin(H)/Cos(H)) = ATan (SH / CH) = ATan(CPSH/CPCH) = ATan (-R02 / R22) + // R = ATan(Tan(R)) = ATan(Sin(R)/Cos(R)) = ATan (SR / CR) = ATan(SRCP/CRCP) = ATan (-R10 / R11) + // P = ATan(Tan(P)) = ATan(Sin(P)/Cos(P)) = ATan (SP / CP) + // SP = -R12 + // Need to find CP: + // CP = Sqrt(CP^2) + // = Sqrt(CP^2*(CH^2+SH^2)) Since: (Cos@^2 + Sin@^2) = 1 + // = Sqrt((CP^2)*(CH^2) + (CP^2)*(SH^2)) Expand + // = Sqrt((CPCH)^2 + (CPSH)^2) Since: N^2*M^2 = (NM)^2 + // = Sqrt(R22^2 + (-R02)^2) Substitute + // = Sqrt(R22^2 + R02^2) Since: (-N)^2 = N^2 + // So P = ATan (-R12 / Sqrt(R22^2 + R02^2)) + + const getColumn = (mat: CesiumMatrix3, col: number): number => { + return (mat)[col]; + }; + + // Simplify notation for readability: + const r10: number = getColumn(rotation, CesiumMatrix3.COLUMN1ROW0); + const r11: number = getColumn(rotation, CesiumMatrix3.COLUMN1ROW1); + const r02: number = getColumn(rotation, CesiumMatrix3.COLUMN0ROW2); + const r12: number = getColumn(rotation, CesiumMatrix3.COLUMN1ROW2); + const r22: number = getColumn(rotation, CesiumMatrix3.COLUMN2ROW2); + + const heading = CesiumMath.toDegrees(Math.atan2(-r02, r22)); + const roll = CesiumMath.toDegrees(Math.atan2(-r10, r11)); + const pitch = CesiumMath.toDegrees( + Math.atan2(-r12, Math.sqrt(r02 * r02 + r22 * r22)) + ); + + // Create an object with the roll, pitch and heading we just computed. + return { + orientation: { + roll: CesiumMath.toRadians(roll), + pitch: CesiumMath.toRadians(pitch), + heading: CesiumMath.toRadians(heading) + } + }; +} + +// import { observable, action } from "mobx"; +// import Terria from "./Terria"; +// import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; +// import EllipsoidTerrainProvider from "terriajs-cesium/Source/Core/EllipsoidTerrainProvider"; +// import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; +// import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; +// import CesiumMath from "terriajs-cesium/Source/Core/Math"; +// import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; + +// export default class AugmentedVirtuality { +// /* The minimum height that the viewer is allowed to hover at. */ +// static readonly MINIMUM_HOVER_HEIGHT = 20.0; + +// /* These are the heights that we can toggle through (in meters - above the +// * surface height). */ +// static readonly PRESET_HEIGHTS = [1000, 250, 20]; + +// @observable private _manualAlignment = false; + +// @observable private alpha = 0; +// @observable private beta = 0; +// @observable private gamma = 0; +// @observable private realignAlpha = 0; +// @observable private realignHeading = 0; +// @observable private hoverLevel = +// AugmentedVirtuality.PRESET_HEIGHTS.length - 1; + +// constructor(readonly terria: Terria) {} + +// @computed +// get enabled() { +// return this._eventLoopRunning() || this._manualAlignment; +// } + +// @action +// set enabled(enable) { +// if (enable !== true) { +// enable = false; +// this.resetAlignment(); +// } + +// if (enable !== this.enabled) { +// // If we are changing the enabled state then disable manual alignment. +// // We only do this if we are changing the enabled state so that the client can repeatedly call the +// // setting without having any effect if they aren't changing the enabled state, but so that every time +// // that the state is changed that the manual alignment is turned back off initally. +// this._manualAlignment = false; +// this._startEventLoop(enable); +// } +// } + +// @computed +// get manualAlignment() { +// return this.enabled && this._manualAlignment; +// } + +// /** +// * Gets the current orientation stored in the object state and returns the +// * roll, pitch and heading which can be used to set the cameras orientation. +// * +// * @param screenOrientation The screen orientation in degrees. Note: +// * This field is optional, if supplied this value will be used for the screen +// * orientation, otherwise the screen orientation will be obtained during the +// * execution of this function. + +// * @return A object with the roll, pitch and heading stored into the +// * orientation. @private +// */ +// private getCurrentOrientation(screenOrientation?: number) { +// const alpha = this.alpha; +// const beta = this.beta; +// const gamma = this.gamma; + +// const realignAlpha = this.realignAlpha; +// const realignHeading = this.realignHeading; + +// if (screenOrientation === undefined) +// screenOrientation = getCurrentScreenOrientation(); + +// return computeTerriaOrientation( +// alpha, +// beta, +// gamma, +// screenOrientation, +// realignAlpha, +// realignHeading +// ); +// } + +// /** +// * Toggles whether the AugmentedVirutuality mode is enabled or disabled. +// */ +// @action +// toggleEnabled() { +// this.enabled = !this.enabled; +// } + +// /** +// * Toggles the viewer between a range of predefined heights, setting the +// * cameras orientation so that it matches the correct orientation. +// */ +// @action +// toggleHoverHeight() { +// this.hoverLevel = +// (this.hoverLevel + 1) % AugmentedVirtuality.PRESET_HEIGHTS.length; +// this.hover(AugmentedVirtuality.PRESET_HEIGHTS[this.hoverLevel]); +// } + +// /** +// * Toggles whether manual alignement is enabled or disabled. +// */ +// @action +// toggleManualAlignment() { +// this.manualAlignment = !this.manualAlignment; +// } + +// /** +// * Moves the viewer to a specified height, setting the orientation so that it +// * matches the correct Augmented Virtuality orientation. +// * +// * @param height The height in Meters above the globe surface. Note: If height is below +// * {@link AugmentedVirtuality.MINIMUM_HOVER_HEIGHT} the height will be set to +// * {@link AugmentedVirtuality.MINIMUM_HOVER_HEIGHT} to avoid visual artifacts +// * when the viewer becomes to close to the surface. +// * @param [position] The location to hover over. If not specified the current +// * camera location will be used. +// * @param [flyTo=true] Whether to fly to the location (true) or whether to jump to the location (false). +// */ +// private async hover( +// height: number, +// position?: Cartographic, +// flyTo: boolean = true +// ) { +// // Get access to the camera...if it is not avaliable we can't set the new +// // height so just return now. +// const camera = this.terria.cesium && this.terria.cesium.scene.camera; +// if (!camera) return; + +// if (!position) position = camera.positionCartographic.clone(); + +// // Clamp the minimum hover height (heights below this value could lead to +// // poor visual artifacts). +// height = Math.max(height, AugmentedVirtuality.MINIMUM_HOVER_HEIGHT); + +// // Reset the viewer height. +// const flyToHeight = (surfaceHeight?: number) => { +// height += surfaceHeight || 0; + +// const newPosition = Cartesian3.fromRadians( +// position.longitude, +// position.latitude, +// height +// ); +// const pose = { +// destination: newPosition, +// ...this.getCurrentOrientation() +// }; + +// if (flyTo) camera.flyTo(pose); +// else camera.setView(pose); + +// // Needed on mobile to make sure that the render is marked as dirty so +// // that once AV mode has been disabled for a while and then is reenabled +// // the .setView() function still has effect (otherwise dispite the call +// // the .setView() the view orientation does not visually update until the +// // user manualy moves the camera position). +// this.terria.currentViewer.notifyRepaintRequired(); +// }; + +// const terrainProvider = this.terria.cesium.scene.terrainProvider; +// if ( +// !terrainProvider || +// terrainProvider instanceof EllipsoidTerrainProvider +// ) { +// // If we can't get access to the terrain provider or we can get access to +// // the terrain provider and the provider is just the Ellipsoid then use +// // the height of 0. +// flyToHeight(0); +// } else { +// // Get the ground surface height at this location and offset the height by it. +// const updatedPosition = await sampleTerrainMostDetailed(terrainProvider, [ +// position +// ]); +// flyToHeight(updatedPosition[0].height); +// } +// } +// } + +// /** +// * Gets the current screen orientation. +// * +// * @return The current screen orientation in degrees. +// * @private +// */ +// function getCurrentScreenOrientation() { +// return screen.orientation && screen.orientation.angle !== undefined +// ? screen.orientation.angle +// : 0; +// } + +// /** +// * Turns the orientation in the device frame of reference into an orientation +// * suitable for specifying the Terria camera orientation. +// * +// * @param alpha The alpha value of the device orientation in degrees +// * (this is the alpha value in the device's frame of reference). +// * @param beta The beta value of the device orientation in degrees (this is the +// * beta value in the device's frame of reference). +// * @param gamma The gamma value of the device orientation in degrees (this is +// * the gamma value in the device's frame of reference). +// * @param screenOrientation The screen orientation in degrees. +// * @param realignAlpha The value of the alpha value the last time realignment +// * was completed (supply zero if realignment is not supported). +// * @param realignHeading The value of the heading value the last time +// * realignment was completed (supply zero if realignment is not supported). +// * @return An object with the roll, pitch and heading stored into the orientation. +// */ +// function computeTerriaOrientation( +// alpha: number, +// beta: number, +// gamma: number, +// screenOrientation: number, +// realignAlpha: number, +// realignHeading: number +// ) { +// // Note: The algorithmic formulation in this function is for simplicity of mathematical expression, readability, +// // maintainability and modification (i.e. it is easy to understand how to update or insert new offsets or features). +// // This is not the simplest form which clearly flows from the current formuleation and clearly simplify the +// // logic and operations but would increase the cost of future modifications and reduce the readability of the +// // expression. It is not anticipated that the current verbose implementation would have a significant impact +// // on performance or accuracy, but obviously there will be some impact on both and it can be simplified in +// // future if needed. + +// const rotation = Matrix3.clone(Matrix3.IDENTITY); +// let rotationIncrement: Matrix3; + +// // Roll - Counteract the change in the (orientation) frame of reference when the screen is rotated and the +// // rotation lock is not on (the browser reorients the frame of reference to align with the new screen +// // orientation - where as we want it of the device relative to the world). +// rotationIncrement = Matrix3.fromRotationZ( +// CesiumMath.toRadians(screenOrientation) +// ); +// Matrix3.multiply(rotation, rotationIncrement, rotation); + +// // Pitch - Align the device orientation frame with the ceasium orientation frame. +// rotationIncrement = Matrix3.fromRotationX(CesiumMath.toRadians(90)); +// Matrix3.multiply(rotation, rotationIncrement, rotation); + +// // Roll - Apply the deivce roll. +// rotationIncrement = Matrix3.fromRotationZ(CesiumMath.toRadians(gamma)); +// Matrix3.multiply(rotation, rotationIncrement, rotation); + +// // Pitch - Apply the deivce pitch. +// rotationIncrement = Matrix3.fromRotationX(CesiumMath.toRadians(-beta)); +// Matrix3.multiply(rotation, rotationIncrement, rotation); + +// // Heading - Apply the incremental deivce heading (from when start was last triggered). +// rotationIncrement = Matrix3.fromRotationY( +// CesiumMath.toRadians(-(alpha - realignAlpha)) +// ); +// Matrix3.multiply(rotation, rotationIncrement, rotation); + +// // Heading - Use the offset when the orientation was last started. +// // Note: This is logically different from the alpha value and can only be applied here in the same way +// // since Cesium camera is RPH (Heading last - most local). See Cesium camera rotation decomposition +// // for more information on the Cesium camera formuleation. +// rotationIncrement = Matrix3.fromRotationY( +// CesiumMath.toRadians(realignHeading) +// ); +// Matrix3.multiply(rotation, rotationIncrement, rotation); + +// // Decompose rotation matrix into roll, pitch and heading to supply to Cesium camera. +// // +// // Use notation: +// // R = Roll, P = Pitch, H = Heading +// // SH = Sin(Heading), CH = Cos(Heading) +// // +// // Ceasium camera rotation = RPH: +// // [ CR, -SR, 0][ 1, 0, 0][ CH, 0, SH] [CRCH-SRSPSH, -SRCP, CRSH-SRSPCH] +// // [ SR, CR, 0][ 0, CP, SP][ 0, 1, 0] = [SRCH-CRSPSH, CRCP, SRSH+CRSPCH] +// // [ 0, 0, 1][ 0, -SP, CP][-SH, 0, CH] [ -CPSH , -SP, CPCH ] +// // Note: The sign difference of the Sin terms in pitch is different to the standard right handed rotation since +// // Cesium rotates pitch in the left handed direction. Both heading and roll are right handed rotations. +// // +// // Use the following notation to refer to elements in the Cesium camera rotation matrix: +// // [R00, R10, R20] +// // [R01, R11, R21] +// // [R02, R12, R22] +// // +// // Also note: Tan(X) = Sin(X) / Cos(X) +// // +// // Decompose matrix: +// // H = ATan(Tan(H)) = ATan(Sin(H)/Cos(H)) = ATan (SH / CH) = ATan(CPSH/CPCH) = ATan (-R02 / R22) +// // R = ATan(Tan(R)) = ATan(Sin(R)/Cos(R)) = ATan (SR / CR) = ATan(SRCP/CRCP) = ATan (-R10 / R11) +// // P = ATan(Tan(P)) = ATan(Sin(P)/Cos(P)) = ATan (SP / CP) +// // SP = -R12 +// // Need to find CP: +// // CP = Sqrt(CP^2) +// // = Sqrt(CP^2*(CH^2+SH^2)) Since: (Cos@^2 + Sin@^2) = 1 +// // = Sqrt((CP^2)*(CH^2) + (CP^2)*(SH^2)) Expand +// // = Sqrt((CPCH)^2 + (CPSH)^2) Since: N^2*M^2 = (NM)^2 +// // = Sqrt(R22^2 + (-R02)^2) Substitute +// // = Sqrt(R22^2 + R02^2) Since: (-N)^2 = N^2 +// // So P = ATan (-R12 / Sqrt(R22^2 + R02^2)) + +// // Simplify notation for readability: +// const r10 = rotation[Matrix3.COLUMN1ROW0]; +// const r11 = rotation[Matrix3.COLUMN1ROW1]; +// const r02 = rotation[Matrix3.COLUMN0ROW2]; +// const r12 = rotation[Matrix3.COLUMN1ROW2]; +// const r22 = rotation[Matrix3.COLUMN2ROW2]; + +// const heading = CesiumMath.toDegrees(Math.atan2(-r02, r22)); +// const roll = CesiumMath.toDegrees(Math.atan2(-r10, r11)); +// const pitch = CesiumMath.toDegrees( +// Math.atan2(-r12, Math.sqrt(r02 * r02 + r22 * r22)) +// ); + +// // Create an object with the roll, pitch and heading we just computed. +// return { +// orientation: { +// roll: CesiumMath.toRadians(roll), +// pitch: CesiumMath.toRadians(pitch), +// heading: CesiumMath.toRadians(heading) +// } +// }; +// } diff --git a/lib/ReactViews/Map/Navigation/AugmentedVirtualityTool.jsx b/lib/ReactViews/Map/Navigation/AugmentedVirtualityTool.jsx index 47821dcefdd..be0e6f2b09b 100644 --- a/lib/ReactViews/Map/Navigation/AugmentedVirtualityTool.jsx +++ b/lib/ReactViews/Map/Navigation/AugmentedVirtualityTool.jsx @@ -1,47 +1,46 @@ -"use strict"; - import React from "react"; -import createReactClass from "create-react-class"; import PropTypes from "prop-types"; -import ObserveModelMixin from "../../ObserveModelMixin"; import Styles from "./augmented_virtuality_tool.scss"; import Icon from "../../Icon"; import ViewerMode from "../../../Models/ViewerMode"; -import defined from "terriajs-cesium/Source/Core/defined"; import { withTranslation } from "react-i18next"; - import AugmentedVirtuality from "../../../Models/AugmentedVirtuality"; +import { observer } from "mobx-react"; +import { action, observable } from "mobx"; -const AugmentedVirtualityTool = createReactClass({ - displayName: "AugmentedVirtualityTool", - mixins: [ObserveModelMixin], +@withTranslation() +@observer +class AugmentedVirtualityTool extends React.Component { + @observable experimentalWarningShown = false; + @observable realignHelpShown = false; + @observable resetRealignHelpShown = false; - propTypes: { + static propTypes = { terria: PropTypes.object.isRequired, viewState: PropTypes.object.isRequired, experimentalWarning: PropTypes.bool, t: PropTypes.func.isRequired - }, + }; - getInitialState() { - return { - augmentedVirtuality: new AugmentedVirtuality(this.props.terria), - experimentalWarningShown: false, - realignHelpShown: false, - resetRealignHelpShown: false - }; - }, + static defaultProps = { + experimentalWarning: true + }; + constructor(props) { + super(props); + this.augmentedVirtuality = new AugmentedVirtuality(this.props.terria); + } + + @action.bound handleClickAVTool() { // Make the AugmentedVirtuality module avaliable elsewhere. - this.props.terria.augmentedVirtuality = this.state.augmentedVirtuality; + this.props.terria.augmentedVirtuality = this.augmentedVirtuality; if ( - defined(this.props.experimentalWarning) && this.props.experimentalWarning !== false && - !this.state.experimentalWarningShown + !this.experimentalWarningShown ) { - this.setState({ experimentalWarningShown: true }); + this.experimentalWarningShown = true; const { t } = this.props; this.props.viewState.notifications.push({ title: t("AR.title"), @@ -49,13 +48,13 @@ const AugmentedVirtualityTool = createReactClass({ confirmText: t("AR.confirmText") }); } + this.augmentedVirtuality.toggleEnabled(); + } - this.state.augmentedVirtuality.toggleEnabled(); - }, - + @action.bound handleClickRealign() { - if (!this.state.realignHelpShown) { - this.setState({ realignHelpShown: true }); + if (!this.realignHelpShown) { + this.realignHelpShown = true; const { t } = this.props; this.props.viewState.notifications.push({ title: t("AR.manualAlignmentTitle"), @@ -67,12 +66,13 @@ const AugmentedVirtualityTool = createReactClass({ }); } - this.state.augmentedVirtuality.toggleManualAlignment(); - }, + this.augmentedVirtuality.toggleManualAlignment(); + } + @action.bound handleClickResetRealign() { - if (!this.state.resetRealignHelpShown) { - this.setState({ resetRealignHelpShown: true }); + if (!this.resetRealignHelpShown) { + this.resetRealignHelpShown = true; const { t } = this.props; this.props.viewState.notifications.push({ title: t("AR.resetAlignmentTitle"), @@ -81,15 +81,15 @@ const AugmentedVirtualityTool = createReactClass({ }); } - this.state.augmentedVirtuality.resetAlignment(); - }, + this.augmentedVirtuality.resetAlignment(); + } handleClickHover() { - this.state.augmentedVirtuality.toggleHoverHeight(); - }, + this.augmentedVirtuality.toggleHoverHeight(); + } render() { - const enabled = this.state.augmentedVirtuality.enabled; + const enabled = this.augmentedVirtuality.isEnabled; let toggleImage = Icon.GLYPHS.arOff; let toggleStyle = Styles.btn; if (enabled) { @@ -97,13 +97,13 @@ const AugmentedVirtualityTool = createReactClass({ toggleStyle = Styles.btnPrimary; } const { t } = this.props; - const realignment = this.state.augmentedVirtuality.manualAlignment; + const realignment = this.augmentedVirtuality.manualAlignment; let realignmentStyle = Styles.btn; if (realignment) { realignmentStyle = Styles.btnBlink; } - const hoverLevel = this.state.augmentedVirtuality.hoverLevel; + const hoverLevel = this.augmentedVirtuality.hoverLevel; let hoverImage = Icon.GLYPHS.arHover0; // Note: We use the image of the next level that we will be changing to, not the level the we are currently at. switch (hoverLevel) { @@ -135,17 +135,17 @@ const AugmentedVirtualityTool = createReactClass({ type="button" className={Styles.btn} title={t("AR.btnHover")} - onClick={this.handleClickHover} + onClick={() => this.handleClickHover()} > - + @@ -153,8 +153,7 @@ const AugmentedVirtualityTool = createReactClass({