From 473d70c8ea4f93841ee77b26885de93b3391d6c1 Mon Sep 17 00:00:00 2001 From: glughi Date: Wed, 20 Nov 2024 11:42:24 +0100 Subject: [PATCH] Done map navigation with keyboard --- .../MapNavigation/registerMapNavigations.tsx | 6 +- .../Tools/KeyboardMode/KeyboardMode.tsx | 7 +- .../Tools/KeyboardMode/MovementControls.tsx | 11 +- .../Tools/KeyboardMode/MovementsController.ts | 333 ++---------------- lib/Styled/Icon.tsx | 1 + wwwroot/images/icons/keyboard.svg | 60 ++++ wwwroot/images/keyboard_controls.svg | 262 ++++++++++++++ wwwroot/languages/en/translation.json | 4 + wwwroot/languages/it/translation.json | 4 + 9 files changed, 373 insertions(+), 315 deletions(-) create mode 100644 wwwroot/images/icons/keyboard.svg create mode 100644 wwwroot/images/keyboard_controls.svg diff --git a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx index 78438651cf9..8d8ec7667ae 100644 --- a/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx +++ b/lib/ReactViews/Map/MapNavigation/registerMapNavigations.tsx @@ -253,12 +253,12 @@ export const registerMapNavigations = (viewState: ViewState) => { toolName: KEYBOARD_MODE_ID, viewState: viewState, getToolComponent: () => KeyboardMode as any, - icon: GLYPHS.tour + icon: GLYPHS.keyboard }); mapNavigationModel.addItem({ id: KEYBOARD_MODE_ID, - name: "translate#pedestrianMode.toolButtonTitle", - title: "translate#pedestrianMode.toolButtonTitle", + name: "translate#keyboardControls.toolButtonTitle", + title: "translate#keyboardControls.toolButtonTitle", location: "TOP", screenSize: "medium", controller: keyboardModeToolController, diff --git a/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx index cb59cba008c..e3ed212f081 100644 --- a/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx +++ b/lib/ReactViews/Tools/KeyboardMode/KeyboardMode.tsx @@ -1,6 +1,5 @@ -import { reaction } from "mobx"; import { observer } from "mobx-react"; -import React, { useEffect, useState } from "react"; +import React from "react"; import styled from "styled-components"; import Cesium from "../../../Models/Cesium"; import ViewState from "../../../ReactViewModels/ViewState"; @@ -8,12 +7,12 @@ import PositionRightOfWorkbench from "../../Workbench/PositionRightOfWorkbench"; import MovementControls from "./MovementControls"; -type PedestrianModeProps = { +type KeyboardModeProps = { viewState: ViewState; }; export const KEYBOARD_MODE_ID = "keyboard-mode"; -const KeyboardMode: React.FC = observer((props) => { +const KeyboardMode: React.FC = observer((props) => { const { viewState } = props; const cesium = viewState.terria.currentViewer; diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx index 1b27800ad2f..81ab5c43eba 100644 --- a/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx +++ b/lib/ReactViews/Tools/KeyboardMode/MovementControls.tsx @@ -4,15 +4,11 @@ import styled from "styled-components"; import Cesium from "../../../Models/Cesium"; import Box from "../../../Styled/Box"; import Button from "../../../Styled/Button"; -import Spacing from "../../../Styled/Spacing"; import Text from "../../../Styled/Text"; import Icon, { StyledIcon } from "../../../Styled/Icon"; import MovementsController from "./MovementsController"; - -const mouseControlsImage = require("../../../../wwwroot/images/mouse-control.svg"); -const wasdControlsImage = require("../../../../wwwroot/images/wasd.svg"); -const heightControlsImage = require("../../../../wwwroot/images/height-controls.svg"); +const wasdControlsImage = require("../../../../wwwroot/images/keyboard_controls.svg"); type MovementControlsProps = { cesium: Cesium; @@ -36,7 +32,7 @@ const MovementControls: React.FC = (props) => { return ( - <Text medium>{t("pedestrianMode.controls.title")}</Text> + <Text medium>{t("keyboardControls.header")}</Text> <MinimizeMaximizeButton onClick={toggleMaximized} maximized={isMaximized} @@ -44,10 +40,7 @@ const MovementControls: React.FC<MovementControlsProps> = (props) => { {isMaximized && ( - Mouse controls Direction controls - - Height controls )} diff --git a/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts index 8cc86f48c0a..a1edfe8e7c4 100644 --- a/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts +++ b/lib/ReactViews/Tools/KeyboardMode/MovementsController.ts @@ -1,17 +1,5 @@ import { action, makeObservable } from "mobx"; -import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; -import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; -import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; -import EllipsoidTerrainProvider from "terriajs-cesium/Source/Core/EllipsoidTerrainProvider"; -import KeyboardEventModifier from "terriajs-cesium/Source/Core/KeyboardEventModifier"; -import CesiumMath from "terriajs-cesium/Source/Core/Math"; -import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; -import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; -import sampleTerrainMostDetailed from "terriajs-cesium/Source/Core/sampleTerrainMostDetailed"; import ScreenSpaceEventHandler from "terriajs-cesium/Source/Core/ScreenSpaceEventHandler"; -import ScreenSpaceEventType from "terriajs-cesium/Source/Core/ScreenSpaceEventType"; -import Scene from "terriajs-cesium/Source/Scene/Scene"; import Cesium from "../../../Models/Cesium"; type Movement = @@ -21,36 +9,28 @@ type Movement = | "right" | "up" | "down" - | "look"; - -export type Mode = "walk" | "fly"; + | "lookUp" + | "lookDown" + | "lookLeft" + | "lookRight"; const KeyMap: Record = { KeyW: "forward", KeyA: "left", KeyS: "backward", KeyD: "right", - Space: "up", - ShiftLeft: "down", - ShiftRight: "down" + KeyZ: "up", + KeyX: "down", + KeyQ: "lookLeft", + KeyE: "lookRight", + KeyR: "lookUp", + KeyF: "lookDown" }; export default class MovementsController { // Current active movements activeMovements: Set = new Set(); - // True if we are currently updating surface height estimate - isUpdatingSurfaceHeightEstimate = false; - - // True if we are currently animating surface height change - isAnimatingSurfaceHeightChange = false; - - // The position of the mouse when a mouse action is started - private startMousePosition?: Cartesian2; - - // The latest position of the mouse while the action is active - private currentMousePosition?: Cartesian2; - constructor( readonly cesium: Cesium, ) { @@ -69,166 +49,51 @@ export default class MovementsController { * moveAmount decides the motion speed. */ get moveAmount() { - //const baseAmount = 0.2; const cameraHeight = this.camera.positionCartographic.height; const moveRate = cameraHeight / 100.0; return moveRate; } - /** - * Moves the camera forward and parallel to the surface by moveAmount - */ - moveForward() { - const direction = projectVectorToSurface( - this.camera.direction, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, this.moveAmount); - - - console.log("forwww") - console.log(this.moveAmount) - console.log(direction) - - this.cesium.notifyRepaintRequired(); - } - - /** - * Moves the camera backward and parallel to the surface by moveAmount - */ - moveBackward() { - const direction = projectVectorToSurface( - this.camera.direction, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, -this.moveAmount); - } - - /** - * Moves the camera left and parallel to the surface by moveAmount/4 - */ - moveLeft() { - const direction = projectVectorToSurface( - this.camera.right, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, -this.moveAmount / 4); - } - - /** - * Moves the camera right and parallel to the surface by moveAmount/4 - */ - moveRight() { - const direction = projectVectorToSurface( - this.camera.right, - this.camera.position, - this.scene.globe.ellipsoid - ); - this.camera.move(direction, this.moveAmount / 4); - } - - /** - * Moves the camera up and perpendicular to the surface by moveAmount - */ - moveUp() { - const surfaceNormal = this.scene.globe.ellipsoid.geodeticSurfaceNormal( - this.camera.position, - new Cartesian3() - ); - this.camera.move(surfaceNormal, this.moveAmount); - } - - /** - * Moves the camera up and perpendicular to the surface by moveAmount - */ - moveDown() { - const surfaceNormal = this.scene.globe.ellipsoid.geodeticSurfaceNormal( - this.camera.position, - new Cartesian3() - ); - this.camera.move(surfaceNormal, -this.moveAmount); - } - - look() { - if ( - this.startMousePosition === undefined || - this.currentMousePosition === undefined - ) - return; - - const startMousePosition = this.startMousePosition; - const currentMousePosition = this.currentMousePosition; - - const camera = this.scene.camera; - const canvas = this.scene.canvas; - const width = canvas.width; - const height = canvas.height; - const x = (currentMousePosition.x - startMousePosition.x) / width; - const y = (currentMousePosition.y - startMousePosition.y) / height; - const lookFactor = 0.1; - - const ellipsoid = this.scene.globe.ellipsoid; - const surfaceNormal = ellipsoid.geodeticSurfaceNormal( - camera.position, - new Cartesian3() - ); - - const surfaceTangent = projectVectorToSurface( - camera.right, - camera.position, - this.scene.globe.ellipsoid - ); - - // Look left/right about the surface normal - camera.look(surfaceNormal, x * lookFactor); - - // Look up/down about the surface tangent - this.lookVertical(surfaceTangent, surfaceNormal, y * lookFactor); - } - - /** - * Look up/down limiting the maximum look angle to {@maxLookangle} - * - */ - lookVertical( - lookAxis: Cartesian3, - surfaceNormal: Cartesian3, - lookAmount: number - ) { - const camera = this.camera; - - const friction = 1; - camera.look(lookAxis, lookAmount * friction); - } - /** * Perform a move step */ move(movement: Movement) { switch (movement) { case "forward": - return this.moveForward(); + this.camera.moveForward(this.moveAmount); + break; case "backward": - return this.moveBackward(); + this.camera.moveBackward(this.moveAmount); + break; case "left": - return this.moveLeft(); + this.camera.moveLeft(this.moveAmount); + break; case "right": - return this.moveRight(); + this.camera.moveRight(this.moveAmount); + break; case "up": - return this.moveUp(); + this.camera.moveUp(this.moveAmount); + break; case "down": - return this.moveDown(); - case "look": - return this.look(); + this.camera.moveDown(this.moveAmount); + break; + case "lookUp": + this.camera.lookUp(); + break; + case "lookDown": + this.camera.lookDown(); + break; + case "lookLeft": + this.camera.lookLeft(); + break; + case "lookRight": + this.camera.lookRight(); + break; } } animate() { if (this.activeMovements.size > 0) { - console.log("aaaaaaaaaaa"); [...this.activeMovements].forEach((movement) => this.move(movement)); } } @@ -269,43 +134,6 @@ export default class MovementsController { setupMouseMap(): () => void { const eventHandler = new ScreenSpaceEventHandler(this.scene.canvas); - const startLook = (click: { position: Cartesian2 }) => { - this.currentMousePosition = this.startMousePosition = - click.position.clone(); - this.activeMovements.add("look"); - }; - - const look = (movement: { endPosition: Cartesian2 }) => { - this.currentMousePosition = movement.endPosition.clone(); - }; - - const stopLook = () => { - this.activeMovements.delete("look"); - this.currentMousePosition = this.startMousePosition = undefined; - }; - - // User might try to turn while moving down (by pressing SHIFT) - // so trigger look event even when SHIFT is pressed. - eventHandler.setInputAction(startLook, ScreenSpaceEventType.LEFT_DOWN); - eventHandler.setInputAction( - startLook, - ScreenSpaceEventType.LEFT_DOWN, - KeyboardEventModifier.SHIFT - ); - - eventHandler.setInputAction(look, ScreenSpaceEventType.MOUSE_MOVE); - eventHandler.setInputAction( - look, - ScreenSpaceEventType.MOUSE_MOVE, - KeyboardEventModifier.SHIFT - ); - - eventHandler.setInputAction(stopLook, ScreenSpaceEventType.LEFT_UP); - eventHandler.setInputAction( - stopLook, - ScreenSpaceEventType.LEFT_UP, - KeyboardEventModifier.SHIFT - ); const mouseMapDestroyer = () => eventHandler.destroy(); return mouseMapDestroyer; } @@ -365,99 +193,6 @@ export default class MovementsController { } } -const sampleScratch = new Cartographic(); - -/** - * Sample the terrain height at the given position - */ -async function sampleTerrainHeight( - scene: Scene, - position: Cartesian3 -): Promise { - const terrainProvider = scene.terrainProvider; - if (terrainProvider instanceof EllipsoidTerrainProvider) return 0; - - const [sample] = await sampleTerrainMostDetailed(terrainProvider, [ - Cartographic.fromCartesian(position, scene.globe.ellipsoid, sampleScratch) - ]); - return sample.height; -} - -/** - * Sample the scene height at the given position - * - * Scene height is the maximum height of a tileset feature or any other entity - * at the given position. - */ -function sampleSceneHeight( - scene: Scene, - position: Cartesian3 -): number | undefined { - if (scene.sampleHeightSupported === false) return; - return scene.sampleHeight( - Cartographic.fromCartesian(position, undefined, sampleScratch) - ); -} - -/** - * Projects the {@vector} to the surface plane containing {@position} - * - * @param vector The input vector to project - * @param position The position used to determine the surface plane - * @param ellipsoid The ellipsoid used to compute the surface plane - * @returns The projection of {@vector} on the surface plane at the given {@position} - */ -function projectVectorToSurface( - vector: Cartesian3, - position: Cartesian3, - ellipsoid: Ellipsoid -) { - const surfaceNormal = ellipsoid.geodeticSurfaceNormal( - position, - new Cartesian3() - ); - const magnitudeOfProjectionOnSurfaceNormal = Cartesian3.dot( - vector, - surfaceNormal - ); - const projectionOnSurfaceNormal = Cartesian3.multiplyByScalar( - surfaceNormal, - magnitudeOfProjectionOnSurfaceNormal, - new Cartesian3() - ); - const projectionOnSurface = Cartesian3.subtract( - vector, - projectionOnSurfaceNormal, - new Cartesian3() - ); - return projectionOnSurface; -} - -const rotateScratchQuaternion = new Quaternion(); -const rotateScratchMatrix = new Matrix3(); - -/** - * Rotates a vector about rotateAxis by rotateAmount - */ -function rotateVectorAboutAxis( - vector: Cartesian3, - rotateAxis: Cartesian3, - rotateAmount: number -) { - const quaternion = Quaternion.fromAxisAngle( - rotateAxis, - -rotateAmount, - rotateScratchQuaternion - ); - const rotation = Matrix3.fromQuaternion(quaternion, rotateScratchMatrix); - const rotatedVector = Matrix3.multiplyByVector( - rotation, - vector, - vector.clone() - ); - return rotatedVector; -} - // A regex matching input tag names const inputNodeRe = /input|textarea|select/i; diff --git a/lib/Styled/Icon.tsx b/lib/Styled/Icon.tsx index 8aaace722d0..d1eb2947ae1 100644 --- a/lib/Styled/Icon.tsx +++ b/lib/Styled/Icon.tsx @@ -42,6 +42,7 @@ export const GLYPHS = { help: require("../../wwwroot/images/icons/help.svg"), helpThick: require("../../wwwroot/images/icons/help-thick.svg"), increase: require("../../wwwroot/images/icons/increase.svg"), + keyboard: require("../../wwwroot/images/icons/keyboard.svg"), left: require("../../wwwroot/images/icons/left.svg"), lineChart: require("../../wwwroot/images/icons/line-chart.svg"), link: require("../../wwwroot/images/icons/link.svg"), diff --git a/wwwroot/images/icons/keyboard.svg b/wwwroot/images/icons/keyboard.svg new file mode 100644 index 00000000000..787b19cbc7b --- /dev/null +++ b/wwwroot/images/icons/keyboard.svg @@ -0,0 +1,60 @@ + + + + + + + + \ No newline at end of file diff --git a/wwwroot/images/keyboard_controls.svg b/wwwroot/images/keyboard_controls.svg new file mode 100644 index 00000000000..3b7842073d9 --- /dev/null +++ b/wwwroot/images/keyboard_controls.svg @@ -0,0 +1,262 @@ + + + + + graphic/keyboard_controls + + + + graphic/keyboard_controls + + + + + + + W + + + + D + + + + S + + + + A + + + + X + + + + Z + + + + + E + + + + Q + + + + R + + + + F + + + diff --git a/wwwroot/languages/en/translation.json b/wwwroot/languages/en/translation.json index b2275c6e2a0..cd9d644e0b9 100644 --- a/wwwroot/languages/en/translation.json +++ b/wwwroot/languages/en/translation.json @@ -2198,6 +2198,10 @@ "title": "Controls" } }, + "keyboardControls": { + "toolButtonTitle": "Keyboard navigation", + "header": "Keyboard map controls" + }, "notification": { "title": "Message" }, diff --git a/wwwroot/languages/it/translation.json b/wwwroot/languages/it/translation.json index 61b709330f8..c0c3a3cd4ad 100644 --- a/wwwroot/languages/it/translation.json +++ b/wwwroot/languages/it/translation.json @@ -1558,6 +1558,10 @@ "toolButtonTitle": "Modalità pedonale", "dropPedestrianTooltipMessage": "Clic sinistro per selezionare la posizione della goccia
Clic destro / Esc per annullare" }, + "keyboardControls": { + "toolButtonTitle": "Navigazione da tastiera", + "header": "Controlli mappa da tastiera" + }, "itemSearchTool": { "resetBtnText": "Cancella input", "searchBtnText": "Cerca",