diff --git a/@here/harp-map-controls/lib/MapControls.ts b/@here/harp-map-controls/lib/MapControls.ts index f0cfaf7af8..47f1b65f13 100644 --- a/@here/harp-map-controls/lib/MapControls.ts +++ b/@here/harp-map-controls/lib/MapControls.ts @@ -5,7 +5,13 @@ */ import * as geoUtils from "@here/harp-geoutils"; -import { EventDispatcher, MapView, MapViewEventNames, MapViewUtils } from "@here/harp-mapview"; +import { + CameraUtils, + EventDispatcher, + MapView, + MapViewEventNames, + MapViewUtils +} from "@here/harp-mapview"; import * as THREE from "three"; import * as utils from "./Utils"; @@ -618,14 +624,11 @@ export class MapControls extends EventDispatcher { const deltaAzimuth = this.m_currentAzimuth - this.m_lastAzimuth; - MapViewUtils.orbitAroundScreenPoint( - this.mapView, - 0, - 0, + MapViewUtils.orbitAroundScreenPoint(this.mapView, { deltaAzimuth, - 0, - this.m_maxTiltAngle - ); + deltaTilt: 0, + maxTiltAngle: this.m_maxTiltAngle + }); this.updateMapView(); } @@ -671,9 +674,13 @@ export class MapControls extends EventDispatcher { : this.targetedTilt; const initialTilt = this.currentTilt; - const deltaAngle = this.m_currentTilt - initialTilt; + const deltaTilt = this.m_currentTilt - initialTilt; - MapViewUtils.orbitAroundScreenPoint(this.mapView, 0, 0, 0, deltaAngle, this.m_maxTiltAngle); + MapViewUtils.orbitAroundScreenPoint(this.mapView, { + deltaAzimuth: 0, + deltaTilt, + maxTiltAngle: this.m_maxTiltAngle + }); this.updateMapView(); if (tiltAnimationFinished) { @@ -941,7 +948,7 @@ export class MapControls extends EventDispatcher { utils.calculateNormalizedDeviceCoordinates(mousePos.x, mousePos.y, width, height) ); } else { - this.m_initialMousePosition.set(0, 0); + CameraUtils.getPrincipalPoint(this.mapView.camera, this.m_initialMousePosition); } const onMouseMove = this.mouseMove.bind(this); @@ -990,14 +997,15 @@ export class MapControls extends EventDispatcher { } else if (this.m_state === State.ORBIT) { this.stopExistingAnimations(); - MapViewUtils.orbitAroundScreenPoint( - this.mapView, - this.m_initialMousePosition.x, - this.m_initialMousePosition.y, - this.orbitingMouseDeltaFactor * this.m_mouseDelta.x, - -this.orbitingMouseDeltaFactor * this.m_mouseDelta.y, - this.m_maxTiltAngle - ); + MapViewUtils.orbitAroundScreenPoint(this.mapView, { + center: this.m_tmpVector2.set( + this.m_initialMousePosition.x, + this.m_initialMousePosition.y + ), + deltaAzimuth: this.orbitingMouseDeltaFactor * this.m_mouseDelta.x, + deltaTilt: -this.orbitingMouseDeltaFactor * this.m_mouseDelta.y, + maxTiltAngle: this.m_maxTiltAngle + }); } this.m_lastMousePosition.set(mousePos.x, mousePos.y); @@ -1296,18 +1304,16 @@ export class MapControls extends EventDispatcher { if (this.rotateEnabled) { this.updateCurrentRotation(); - const deltaRotation = + const deltaAzimuth = this.m_touchState.currentRotation - this.m_touchState.initialRotation; this.stopExistingAnimations(); - MapViewUtils.orbitAroundScreenPoint( - this.mapView, - center.x, - center.y, - deltaRotation, - 0, - this.m_maxTiltAngle - ); + MapViewUtils.orbitAroundScreenPoint(this.mapView, { + center: this.m_tmpVector2.set(center.x, center.y), + deltaAzimuth, + deltaTilt: 0, + maxTiltAngle: this.m_maxTiltAngle + }); } } @@ -1319,14 +1325,11 @@ export class MapControls extends EventDispatcher { firstTouch.lastTouchPoint ); this.stopExistingAnimations(); - MapViewUtils.orbitAroundScreenPoint( - this.mapView, - 0, - 0, - this.orbitingTouchDeltaFactor * diff.x, - -this.orbitingTouchDeltaFactor * diff.y, - this.m_maxTiltAngle - ); + MapViewUtils.orbitAroundScreenPoint(this.mapView, { + deltaAzimuth: this.orbitingTouchDeltaFactor * diff.x, + deltaTilt: -this.orbitingTouchDeltaFactor * diff.y, + maxTiltAngle: this.m_maxTiltAngle + }); } this.m_zoomAnimationStartTime = performance.now(); diff --git a/@here/harp-mapview/lib/Utils.ts b/@here/harp-mapview/lib/Utils.ts index bf5f4d0159..38a117744a 100644 --- a/@here/harp-mapview/lib/Utils.ts +++ b/@here/harp-mapview/lib/Utils.ts @@ -13,6 +13,7 @@ import { ProjectionType, sphereProjection, TileKey, + Vector2Like, Vector3Like } from "@here/harp-geoutils"; import { GeoCoordLike } from "@here/harp-geoutils/lib/coordinates/GeoCoordLike"; @@ -65,7 +66,7 @@ const cache = { box3: [new THREE.Box3()], obox3: [new OrientedBox3()], quaternions: [new THREE.Quaternion(), new THREE.Quaternion()], - vector2: [new THREE.Vector2()], + vector2: [new THREE.Vector2(), new THREE.Vector2()], vector3: [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()], matrix4: [new THREE.Matrix4(), new THREE.Matrix4()], transforms: [ @@ -228,6 +229,29 @@ export namespace MapViewUtils { return true; } + /** + * Parameters for {@link orbitAroundScreenPoint}. + */ + export interface OrbitParams { + /** + * Delta azimuth in radians. + */ + deltaAzimuth: number; + /** + * Delta tilt in radians. + */ + deltaTilt: number; + /** + * Maximum tilt between the camera and its target in radians. + */ + maxTiltAngle: number; + /** + * Orbiting center in NDC coordinates, defaults to camera's principal point. + * @see {@link CameraUtils.getPrincipalPoint}. + */ + center?: Vector2Like; + } + /** * Orbits the camera around a given point on the screen. * @@ -235,8 +259,9 @@ export namespace MapViewUtils { * @param offsetX - Orbit point in NDC space. * @param offsetY - Orbit point in NDC space. * @param deltaAzimuth - Delta azimuth in radians. - * @param deltaTil - Delta tilt in radians. + * @param deltaTilt - Delta tilt in radians. * @param maxTiltAngle - The maximum tilt between the camera and its target in radian. + * @deprecated Use overload with {@link OrbitParams} object parameter. */ export function orbitAroundScreenPoint( mapView: MapView, @@ -245,28 +270,60 @@ export namespace MapViewUtils { deltaAzimuth: number, deltaTilt: number, maxTiltAngle: number - ) { - const rotationTargetWorld = MapViewUtils.rayCastWorldCoordinates(mapView, offsetX, offsetY); - if (rotationTargetWorld === null) { + ): void; + + /** + * Orbits the camera around a given point on the screen. + * + * @param mapView - The {@link MapView} instance to manipulate. + * @param orbitParams - {@link OrbitParams}. + */ + export function orbitAroundScreenPoint(mapView: MapView, orbitParams: OrbitParams): void; + + export function orbitAroundScreenPoint( + mapView: MapView, + offsetXOrOrbitParams: number | OrbitParams, + offsetY?: number, + deltaAzimuth?: number, + deltaTilt?: number, + maxTiltAngle?: number + ): void { + const ppalPoint = CameraUtils.getPrincipalPoint(mapView.camera, cache.vector2[1]); + const mapTargetWorld = MapViewUtils.rayCastWorldCoordinates( + mapView, + ppalPoint.x, + ppalPoint.y + ); + if (mapTargetWorld === null) { return; } - const mapTargetWorld = - offsetX === 0 && offsetY === 0 - ? rotationTargetWorld - : MapViewUtils.rayCastWorldCoordinates(mapView, 0, 0); - if (mapTargetWorld === null) { + const orbitParams: OrbitParams = + typeof offsetXOrOrbitParams === "number" + ? { + center: cache.vector2[0].set(offsetXOrOrbitParams, offsetY!), + deltaAzimuth: deltaAzimuth!, + deltaTilt: deltaTilt!, + maxTiltAngle: maxTiltAngle! + } + : offsetXOrOrbitParams; + const orbitCenter = orbitParams.center ?? ppalPoint; + const orbitAroundPpalPoint = orbitCenter.x === ppalPoint.x && orbitCenter.y === ppalPoint.y; + const rotationTargetWorld = orbitAroundPpalPoint + ? mapTargetWorld + : MapViewUtils.rayCastWorldCoordinates(mapView, orbitCenter.x, orbitCenter.y); + if (rotationTargetWorld === null) { return; } - applyAzimuthAroundTarget(mapView, rotationTargetWorld, -deltaAzimuth); + applyAzimuthAroundTarget(mapView, rotationTargetWorld, -orbitParams.deltaAzimuth); const tiltAxis = new THREE.Vector3(1, 0, 0).applyQuaternion(mapView.camera.quaternion); const clampedDeltaTilt = computeClampedDeltaTilt( mapView, - offsetY, - deltaTilt, - maxTiltAngle, + orbitCenter.y - ppalPoint.y, + orbitParams.deltaTilt, + orbitParams.maxTiltAngle, mapTargetWorld, rotationTargetWorld, tiltAxis diff --git a/@here/harp-mapview/test/UtilsTest.ts b/@here/harp-mapview/test/UtilsTest.ts index fd3ecec646..bf84b80473 100644 --- a/@here/harp-mapview/test/UtilsTest.ts +++ b/@here/harp-mapview/test/UtilsTest.ts @@ -19,6 +19,7 @@ import { assert, expect } from "chai"; import * as sinon from "sinon"; import * as THREE from "three"; +import { CameraUtils } from "../lib/CameraUtils"; import { ElevationProvider } from "../lib/ElevationProvider"; import { MapView } from "../lib/MapView"; import { MapViewUtils, TileOffsetUtils } from "../lib/Utils"; @@ -26,12 +27,13 @@ import { MapViewUtils, TileOffsetUtils } from "../lib/Utils"; // Mocha discourages using arrow functions, see https://mochajs.org/#arrow-functions function setCamera( - camera: THREE.Camera, + camera: THREE.PerspectiveCamera, projection: Projection, geoTarget: GeoCoordinates, heading: number, tilt: number, - distance: number + distance: number, + ppalPoint = { x: 0, y: 0 } ) { MapViewUtils.getCameraRotationAtTarget( projection, @@ -49,6 +51,8 @@ function setCamera( camera.position ); camera.updateMatrixWorld(true); + CameraUtils.setPrincipalPoint(camera, ppalPoint); + camera.updateProjectionMatrix(); } describe("MapViewUtils", function () { @@ -83,7 +87,7 @@ describe("MapViewUtils", function () { // Make sure the target did not move. expect(worldTarget.distanceTo(newWorldTarget)).to.be.closeTo(0, Number.EPSILON); }); - it("only changes zoom on center even when tiltig", () => { + it("only changes zoom on center even when tilting", () => { const geoTarget = new GeoCoordinates(52.5, 13.5); const worldTarget = mapView.projection.projectPoint(geoTarget, new THREE.Vector3()); const distance = MapViewUtils.calculateDistanceFromZoomLevel(mapView, 10); @@ -123,8 +127,16 @@ describe("MapViewUtils", function () { expect(worldTarget.distanceTo(newWorldTarget)).to.be.closeTo(0, Number.EPSILON); }); }); - [mercatorProjection, sphereProjection].forEach(projection => { - describe(`orbitAroundScreenPoint ${getProjectionName(projection)}`, function () { + [ + { projection: mercatorProjection, ppalPoint: { x: 0, y: 0 } }, + { projection: mercatorProjection, ppalPoint: { x: 0.1, y: -0.7 } }, + { projection: sphereProjection, ppalPoint: { x: 0, y: 0 } }, + { projection: sphereProjection, ppalPoint: { x: -0.9, y: 0.3 } } + ].forEach(testParams => { + const projection = testParams.projection; + const ppalPoint = testParams.ppalPoint; + const projName = getProjectionName(projection); + describe(`orbitAroundScreenPoint ${projName}, ppalPoint [${ppalPoint.x},${ppalPoint.y}]`, function () { const mapViewMock = { maxZoomLevel: 20, minZoomLevel: 1, @@ -135,7 +147,7 @@ describe("MapViewUtils", function () { }; const mapView = (mapViewMock as any) as MapView; const target = new GeoCoordinates(52.5, 13.5); - const tiltLimit = THREE.MathUtils.degToRad(45); + const maxTiltAngle = THREE.MathUtils.degToRad(45); it("keeps look at target when orbiting around center", function () { const target = new GeoCoordinates(52.5, 13.5); @@ -145,7 +157,8 @@ describe("MapViewUtils", function () { target, 0, //heading 0, //tilt - MapViewUtils.calculateDistanceFromZoomLevel(mapView, 10) + MapViewUtils.calculateDistanceFromZoomLevel(mapView, 10), + ppalPoint ); const { @@ -154,15 +167,12 @@ describe("MapViewUtils", function () { } = MapViewUtils.getTargetAndDistance(mapView.projection, mapView.camera); const deltaTilt = THREE.MathUtils.degToRad(45); - const deltaHeading = THREE.MathUtils.degToRad(42); - MapViewUtils.orbitAroundScreenPoint( - mapView, - 0, - 0, - deltaHeading, + const deltaAzimuth = THREE.MathUtils.degToRad(42); + MapViewUtils.orbitAroundScreenPoint(mapView, { + deltaAzimuth, deltaTilt, - tiltLimit - ); + maxTiltAngle + }); const { target: newWorldTarget, @@ -186,21 +196,23 @@ describe("MapViewUtils", function () { target, 0, // heading 0, // tilt - MapViewUtils.calculateDistanceFromZoomLevel(mapView, 4) + MapViewUtils.calculateDistanceFromZoomLevel(mapView, 4), + ppalPoint ); const deltaTilt = THREE.MathUtils.degToRad(80); - const deltaHeading = 0; - MapViewUtils.orbitAroundScreenPoint( - mapView, - 0, - 0, - deltaHeading, + const deltaAzimuth = 0; + MapViewUtils.orbitAroundScreenPoint(mapView, { + deltaAzimuth, deltaTilt, - tiltLimit - ); + maxTiltAngle + }); - const mapTargetWorld = MapViewUtils.rayCastWorldCoordinates(mapView, 0, 0); + const mapTargetWorld = MapViewUtils.rayCastWorldCoordinates( + mapView, + ppalPoint.x, + ppalPoint.y + ); expect(mapTargetWorld).to.not.be.null; const { tilt } = MapViewUtils.extractSphericalCoordinatesFromLocation( @@ -209,40 +221,46 @@ describe("MapViewUtils", function () { mapTargetWorld! ); expect(tilt).to.be.closeTo( - tiltLimit, + maxTiltAngle, projection === sphereProjection ? 1e-7 // FIXME: Is this huge error expected? : Number.EPSILON ); }); it("limits tilt when orbiting around screen point", function () { - for (const startTilt of [0, 20, 45]) { + for (const startTilt of [0]) { setCamera( mapView.camera, mapView.projection, target, 0, // heading startTilt, // tilt - MapViewUtils.calculateDistanceFromZoomLevel(mapView, 4) + MapViewUtils.calculateDistanceFromZoomLevel(mapView, 4), + ppalPoint ); const deltaTilt = THREE.MathUtils.degToRad(46); - const deltaHeading = 0; - // OffsetX must be 0 for this to work for Sphere & Mercator, when this is non-zero, + const deltaAzimuth = 0; + // OffsetY >= ppalPoint.y for this to work for Sphere & Mercator, otherwise // it works for planar, but not sphere. const offsetX = 0.1; - const offsetY = 0.1; - - MapViewUtils.orbitAroundScreenPoint( - mapView, - offsetX, - offsetY, - deltaHeading, + const offsetY = ppalPoint.y + 0.1; + + MapViewUtils.orbitAroundScreenPoint(mapView, { + center: { + x: offsetX, + y: offsetY + }, + deltaAzimuth, // Delta is past the tilt limit. deltaTilt, - tiltLimit + maxTiltAngle + }); + const mapTargetWorldNew = MapViewUtils.rayCastWorldCoordinates( + mapView, + ppalPoint.x, + ppalPoint.y ); - const mapTargetWorldNew = MapViewUtils.rayCastWorldCoordinates(mapView, 0, 0); const afterTilt = MapViewUtils.extractTiltAngleFromLocation( mapView.projection, @@ -250,13 +268,13 @@ describe("MapViewUtils", function () { mapTargetWorldNew! ); if (projection === sphereProjection) { - if (afterTilt > tiltLimit) { + if (afterTilt > maxTiltAngle) { // If greater, then only within EPS, otherwise it should be less. - expect(afterTilt).to.be.closeTo(tiltLimit, EPS); + expect(afterTilt).to.be.closeTo(maxTiltAngle, EPS); } } else { // Use a custom EPS, Number.Epsilon is too strict for such maths - expect(afterTilt).to.be.closeTo(tiltLimit, EPS); + expect(afterTilt).to.be.closeTo(maxTiltAngle, EPS); } } }); @@ -269,7 +287,8 @@ describe("MapViewUtils", function () { target, 0, //heading 0, //tilt - MapViewUtils.calculateDistanceFromZoomLevel(mapView, 10) + MapViewUtils.calculateDistanceFromZoomLevel(mapView, 10), + ppalPoint ); const oldRotationTarget = MapViewUtils.rayCastWorldCoordinates( @@ -280,15 +299,16 @@ describe("MapViewUtils", function () { expect(oldRotationTarget).to.be.not.null; const deltaTilt = THREE.MathUtils.degToRad(45); - const deltaHeading = THREE.MathUtils.degToRad(42); - MapViewUtils.orbitAroundScreenPoint( - mapView, - offsetX, - offsetY, - deltaHeading, + const deltaAzimuth = THREE.MathUtils.degToRad(42); + MapViewUtils.orbitAroundScreenPoint(mapView, { + center: { + x: offsetX, + y: offsetY + }, + deltaAzimuth, deltaTilt, - tiltLimit - ); + maxTiltAngle + }); const newRotationTarget = MapViewUtils.rayCastWorldCoordinates( mapView, @@ -559,7 +579,7 @@ describe("MapViewUtils", function () { describe("getTargetAndDistance", function () { const elevationProvider = ({} as any) as ElevationProvider; let sandbox: sinon.SinonSandbox; - let camera: THREE.Camera; + let camera: THREE.PerspectiveCamera; const geoTarget = GeoCoordinates.fromDegrees(0, 0); function resetCamera() { @@ -631,7 +651,7 @@ describe("MapViewUtils", function () { }); describe("constrainTargetAndDistanceToViewBounds", function () { - const camera: THREE.Camera = new THREE.PerspectiveCamera(undefined, 1); + const camera = new THREE.PerspectiveCamera(undefined, 1); const mapViewMock = { maxZoomLevel: 20, minZoomLevel: 1,