diff options
Diffstat (limited to 'src/client/views/nodes/MapBox/AnimationUtility.ts')
-rw-r--r-- | src/client/views/nodes/MapBox/AnimationUtility.ts | 508 |
1 files changed, 223 insertions, 285 deletions
diff --git a/src/client/views/nodes/MapBox/AnimationUtility.ts b/src/client/views/nodes/MapBox/AnimationUtility.ts index 11b335a96..a5cff4668 100644 --- a/src/client/views/nodes/MapBox/AnimationUtility.ts +++ b/src/client/views/nodes/MapBox/AnimationUtility.ts @@ -1,12 +1,13 @@ -import mapboxgl from "mapbox-gl"; -import { MercatorCoordinate } from "mapbox-gl"; -import { MapRef } from "react-map-gl"; +import mapboxgl from 'mapbox-gl'; +import { MercatorCoordinate } from 'mapbox-gl'; +import { MapRef } from 'react-map-gl'; import * as React from 'react'; -import * as d3 from "d3"; +import * as d3 from 'd3'; import * as turf from '@turf/turf'; -import { Position } from "@turf/turf"; -import { Feature, GeoJsonProperties, Geometry } from "geojson"; -import { action, computed, observable, runInAction } from "mobx"; +import { Position } from '@turf/turf'; +import { Feature, FeatureCollection, GeoJsonProperties, Geometry } from 'geojson'; +import { observer } from 'mobx-react'; +import { action, computed, observable, runInAction } from 'mobx'; export enum AnimationStatus { START = 'start', @@ -21,15 +22,15 @@ export enum AnimationSpeed { } export class AnimationUtility { - private SMOOTH_FACTOR = 0.95 - private ROUTE_COORDINATES: Position[] = []; + private SMOOTH_FACTOR = 0.95; + private ROUTE_COORDINATES: Position[] = []; @observable private PATH: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>; - + private PATH_DISTANCE: number; private FLY_IN_START_PITCH = 40; - private FIRST_LNG_LAT: {lng: number, lat: number}; + private FIRST_LNG_LAT: { lng: number; lat: number }; private START_ALTITUDE = 3_000_000; private MAP_REF: MapRef | null; @@ -37,27 +38,27 @@ export class AnimationUtility { @observable private animationSpeed: AnimationSpeed; @observable - private previousLngLat: {lng: number, lat: number}; + private previousLngLat: { lng: number; lat: number }; private previousAltitude: number | null = null; private previousPitch: number | null = null; - + private currentStreetViewBearing: number = 0; private terrainDisplayed: boolean; @computed get flyInEndBearing() { - return this.isStreetViewAnimation ? - this.calculateBearing( - { - lng: this.ROUTE_COORDINATES[0][0], - lat: this.ROUTE_COORDINATES[0][1] - }, - { - lng: this.ROUTE_COORDINATES[1][0], - lat: this.ROUTE_COORDINATES[1][1] - } - ) + return this.isStreetViewAnimation + ? this.calculateBearing( + { + lng: this.ROUTE_COORDINATES[0][0], + lat: this.ROUTE_COORDINATES[0][1], + }, + { + lng: this.ROUTE_COORDINATES[1][0], + lat: this.ROUTE_COORDINATES[1][1], + } + ) : -20; } @@ -67,15 +68,14 @@ export class AnimationUtility { const coords: mapboxgl.LngLatLike = [this.previousLngLat.lng, this.previousLngLat.lat]; // console.log('MAP REF: ', this.MAP_REF) // console.log("current elevation: ", this.MAP_REF?.queryTerrainElevation(coords)); - let altitude = (this.MAP_REF ? (this.MAP_REF.queryTerrainElevation(coords) ?? 0) : 0); - if (altitude === 0){ - altitude+=50; + let altitude = this.MAP_REF ? this.MAP_REF.queryTerrainElevation(coords) ?? 0 : 0; + if (altitude === 0) { + altitude += 50; } - if (this.previousAltitude){ + if (this.previousAltitude) { return this.lerp(altitude, this.previousAltitude, 0.02); } return altitude; - } @computed get flyInStartBearing() { @@ -96,16 +96,15 @@ export class AnimationUtility { const horizontalDistance = 500; let pitch; - if (heightAboveGround >= 0){ - pitch = (90- Math.atan(heightAboveGround/horizontalDistance) * (180/Math.PI)); - } - else { + if (heightAboveGround >= 0) { + pitch = 90 - Math.atan(heightAboveGround / horizontalDistance) * (180 / Math.PI); + } else { pitch = 80; } console.log(Math.max(50, Math.min(pitch, 85))); - if (this.previousPitch){ + if (this.previousPitch) { return this.lerp(Math.max(50, Math.min(pitch, 85)), this.previousPitch, 0.02); } return Math.max(50, Math.min(pitch, 85)); @@ -138,7 +137,7 @@ export class AnimationUtility { const normalizedDistance = Math.min(1, (this.PATH_DISTANCE - MIN_DISTANCE) / (MAX_DISTANCE - MIN_DISTANCE)); const easedDistance = d3.easeExpOut(Math.min(normalizedDistance, 1)); - switch (this.animationSpeed){ + switch (this.animationSpeed) { case AnimationSpeed.SLOW: scalingFactor = 250; break; @@ -148,13 +147,13 @@ export class AnimationUtility { case AnimationSpeed.FAST: scalingFactor = 85; break; - default: + default: scalingFactor = 150; break; } - const duration = Math.min(MAX_DURATION, (easedDistance * MAX_DISTANCE) * (this.isStreetViewAnimation ? scalingFactor*1.12 : scalingFactor)); - + const duration = Math.min(MAX_DURATION, easedDistance * MAX_DISTANCE * (this.isStreetViewAnimation ? scalingFactor * 1.12 : scalingFactor)); + return duration; } @@ -163,26 +162,18 @@ export class AnimationUtility { // calculate new flyToDuration and animationDuration this.animationSpeed = speed; } - + @action public updateIsStreetViewAnimation(isStreetViewAnimation: boolean) { this.isStreetViewAnimation = isStreetViewAnimation; } - @action + @action public setPath = (path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>) => { this.PATH = path; - } + }; - - constructor( - firstLngLat: {lng: number, lat: number}, - routeCoordinates: Position[], - isStreetViewAnimation: boolean, - animationSpeed: AnimationSpeed, - terrainDisplayed: boolean, - mapRef: MapRef | null - ) { + constructor(firstLngLat: { lng: number; lat: number }, routeCoordinates: Position[], isStreetViewAnimation: boolean, animationSpeed: AnimationSpeed, terrainDisplayed: boolean, mapRef: MapRef | null) { this.FIRST_LNG_LAT = firstLngLat; this.previousLngLat = firstLngLat; this.isStreetViewAnimation = isStreetViewAnimation; @@ -196,11 +187,11 @@ export class AnimationUtility { const bearing = this.calculateBearing( { lng: routeCoordinates[0][0], - lat: routeCoordinates[0][1] + lat: routeCoordinates[0][1], }, { lng: routeCoordinates[1][0], - lat: routeCoordinates[1][1] + lat: routeCoordinates[1][1], } ); this.currentStreetViewBearing = bearing; @@ -216,295 +207,242 @@ export class AnimationUtility { currentAnimationPhase, updateAnimationPhase, updateFrameId, - }: { - map: MapRef, + }: { + map: MapRef; // path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties>, // startBearing: number, // startAltitude: number, // pitch: number, - currentAnimationPhase: number, - updateAnimationPhase: ( - newAnimationPhase: number, - ) => void, + currentAnimationPhase: number; + updateAnimationPhase: (newAnimationPhase: number) => void; updateFrameId: (newFrameId: number) => void; + }) => { + return new Promise<void>(async resolve => { + let startTime: number | null = null; + + const frame = async (currentTime: number) => { + if (!startTime) startTime = currentTime; + const elapsedSinceLastFrame = currentTime - startTime; + const phaseIncrement = elapsedSinceLastFrame / this.animationDuration; + const animationPhase = currentAnimationPhase + phaseIncrement; + + // when the duration is complete, resolve the promise and stop iterating + if (animationPhase > 1) { + resolve(); + return; + } - }) => { - return new Promise<void>(async (resolve) => { - let startTime: number | null = null; - - const frame = async (currentTime: number) => { - if (!startTime) startTime = currentTime; - const elapsedSinceLastFrame = currentTime - startTime; - const phaseIncrement = elapsedSinceLastFrame / this.animationDuration; - const animationPhase = currentAnimationPhase + phaseIncrement; - - // when the duration is complete, resolve the promise and stop iterating - if (animationPhase > 1) { - resolve(); - return; - } + // calculate the distance along the path based on the animationPhase + const alongPath = turf.along(this.PATH, this.PATH_DISTANCE * animationPhase).geometry.coordinates; + + const lngLat = { + lng: alongPath[0], + lat: alongPath[1], + }; + + let bearing: number; + if (this.isStreetViewAnimation) { + bearing = this.lerp(this.currentStreetViewBearing, this.calculateBearing(this.previousLngLat, lngLat), 0.032); + this.currentStreetViewBearing = bearing; + // bearing = this.calculateBearing(this.previousLngLat, lngLat); // TODO: Calculate actual bearing + } else { + // slowly rotate the map at a constant rate + bearing = this.flyInEndBearing - animationPhase * 200.0; + // bearing = startBearing - animationPhase * 200.0; + } + + runInAction(() => { + this.previousLngLat = lngLat; + }); + + updateAnimationPhase(animationPhase); - - // calculate the distance along the path based on the animationPhase - const alongPath = turf.along(this.PATH, this.PATH_DISTANCE * animationPhase).geometry - .coordinates; - - const lngLat = { - lng: alongPath[0], - lat: alongPath[1], + // compute corrected camera ground position, so that he leading edge of the path is in view + var correctedPosition = this.computeCameraPosition( + this.isStreetViewAnimation, + this.currentPitch, + bearing, + lngLat, + this.currentAnimationAltitude, + true // smooth + ); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(this.currentPitch, bearing); + + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, this.currentAnimationAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); + + this.previousAltitude = this.currentAnimationAltitude; + this.previousPitch = this.previousPitch; + + // repeat! + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); }; - - let bearing: number; - if (this.isStreetViewAnimation){ - bearing = this.lerp( - this.currentStreetViewBearing, - this.calculateBearing(this.previousLngLat, lngLat), - 0.032 - ); - this.currentStreetViewBearing = bearing; - // bearing = this.calculateBearing(this.previousLngLat, lngLat); // TODO: Calculate actual bearing - } else { - // slowly rotate the map at a constant rate - bearing = this.flyInEndBearing - animationPhase * 200.0; - // bearing = startBearing - animationPhase * 200.0; - } - runInAction(() => { - this.previousLngLat = lngLat; - }) - - updateAnimationPhase(animationPhase); - - // compute corrected camera ground position, so that he leading edge of the path is in view - var correctedPosition = this.computeCameraPosition( - this.isStreetViewAnimation, - this.currentPitch, - bearing, - lngLat, - this.currentAnimationAltitude, - true // smooth - ); - - // set the pitch and bearing of the camera - const camera = map.getFreeCameraOptions(); - camera.setPitchBearing(this.currentPitch, bearing); - - - // set the position and altitude of the camera - camera.position = MercatorCoordinate.fromLngLat( - correctedPosition, - this.currentAnimationAltitude - ); - - - // apply the new camera options - map.setFreeCameraOptions(camera); - - this.previousAltitude = this.currentAnimationAltitude; - this.previousPitch = this.previousPitch; - - // repeat! - const innerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(innerFrameId); - }; - - const outerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(outerFrameId); + const outerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(outerFrameId); }); - }; + }; - public flyInAndRotate = async ({ - map, - updateFrameId - }: - { - map: MapRef, - updateFrameId: (newFrameId: number) => void - } - ) => { - return new Promise<{bearing: number, altitude: number}>(async (resolve) => { - let start: number | null; - - var currentAltitude; - var currentBearing; - var currentPitch; - - // the animation frame will run as many times as necessary until the duration has been reached - const frame = async (time: number) => { - if (!start) { - start = time; - } - - // otherwise, use the current time to determine how far along in the duration we are - let animationPhase = (time - start) / this.flyToDuration; - - // because the phase calculation is imprecise, the final zoom can vary - // if it ended up greater than 1, set it to 1 so that we get the exact endAltitude that was requested - if (animationPhase > 1) { - animationPhase = 1; - } - - currentAltitude = this.START_ALTITUDE + (this.flyInEndAltitude - this.START_ALTITUDE) * d3.easeCubicOut(animationPhase) - // rotate the camera between startBearing and endBearing - currentBearing = this.flyInStartBearing + (this.flyInEndBearing - this.flyInStartBearing) * d3.easeCubicOut(animationPhase) - - currentPitch = this.FLY_IN_START_PITCH + (this.flyInEndPitch - this.FLY_IN_START_PITCH) * d3.easeCubicOut(animationPhase) - - // compute corrected camera ground position, so the start of the path is always in view - var correctedPosition = this.computeCameraPosition( - false, - currentPitch, - currentBearing, - this.FIRST_LNG_LAT, - currentAltitude - ); - - // set the pitch and bearing of the camera - const camera = map.getFreeCameraOptions(); - camera.setPitchBearing(currentPitch, currentBearing); - - // set the position and altitude of the camera - camera.position = MercatorCoordinate.fromLngLat( - correctedPosition, - currentAltitude - ); - - // apply the new camera options - map.setFreeCameraOptions(camera); - - // when the animationPhase is done, resolve the promise so the parent function can move on to the next step in the sequence - if (animationPhase === 1) { - resolve({ - bearing: currentBearing, - altitude: currentAltitude, - }); - - // return so there are no further iterations of this frame - return; - } - - const innerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(innerFrameId); - - }; - - const outerFrameId = await window.requestAnimationFrame(frame); - updateFrameId(outerFrameId); + public flyInAndRotate = async ({ map, updateFrameId }: { map: MapRef; updateFrameId: (newFrameId: number) => void }) => { + return new Promise<{ bearing: number; altitude: number }>(async resolve => { + let start: number | null; + + var currentAltitude; + var currentBearing; + var currentPitch; + + // the animation frame will run as many times as necessary until the duration has been reached + const frame = async (time: number) => { + if (!start) { + start = time; + } + + // otherwise, use the current time to determine how far along in the duration we are + let animationPhase = (time - start) / this.flyToDuration; + + // because the phase calculation is imprecise, the final zoom can vary + // if it ended up greater than 1, set it to 1 so that we get the exact endAltitude that was requested + if (animationPhase > 1) { + animationPhase = 1; + } + + currentAltitude = this.START_ALTITUDE + (this.flyInEndAltitude - this.START_ALTITUDE) * d3.easeCubicOut(animationPhase); + // rotate the camera between startBearing and endBearing + currentBearing = this.flyInStartBearing + (this.flyInEndBearing - this.flyInStartBearing) * d3.easeCubicOut(animationPhase); + + currentPitch = this.FLY_IN_START_PITCH + (this.flyInEndPitch - this.FLY_IN_START_PITCH) * d3.easeCubicOut(animationPhase); + + // compute corrected camera ground position, so the start of the path is always in view + var correctedPosition = this.computeCameraPosition(false, currentPitch, currentBearing, this.FIRST_LNG_LAT, currentAltitude); + + // set the pitch and bearing of the camera + const camera = map.getFreeCameraOptions(); + camera.setPitchBearing(currentPitch, currentBearing); + + // set the position and altitude of the camera + camera.position = MercatorCoordinate.fromLngLat(correctedPosition, currentAltitude); + + // apply the new camera options + map.setFreeCameraOptions(camera); + + // when the animationPhase is done, resolve the promise so the parent function can move on to the next step in the sequence + if (animationPhase === 1) { + resolve({ + bearing: currentBearing, + altitude: currentAltitude, + }); + + // return so there are no further iterations of this frame + return; + } + + const innerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(innerFrameId); + }; + + const outerFrameId = await window.requestAnimationFrame(frame); + updateFrameId(outerFrameId); }); - }; + }; - previousCameraPosition: {lng: number, lat: number} | null = null; + previousCameraPosition: { lng: number; lat: number } | null = null; - lerp = (start: number, end: number, amt: number) => { + lerp = (start: number, end: number, amt: number) => { return (1 - amt) * start + amt * end; - } - - computeCameraPosition = ( - isStreetViewAnimation: boolean, - pitch: number, - bearing: number, - targetPosition: {lng: number, lat: number}, - altitude: number, - smooth = false - ) => { + }; + + computeCameraPosition = (isStreetViewAnimation: boolean, pitch: number, bearing: number, targetPosition: { lng: number; lat: number }, altitude: number, smooth = false) => { const bearingInRadian = (bearing * Math.PI) / 180; - const pitchInRadian = ((90 - pitch)* Math.PI) / 180; + const pitchInRadian = ((90 - pitch) * Math.PI) / 180; let correctedLng = targetPosition.lng; let correctedLat = targetPosition.lat; if (!isStreetViewAnimation) { - const lngDiff = - ((altitude / Math.tan(pitchInRadian)) * - Math.sin(-bearingInRadian)) / - 70000; // ~70km/degree longitude - const latDiff = - ((altitude / Math.tan(pitchInRadian)) * - Math.cos(-bearingInRadian)) / - 110000 // 110km/degree latitude + const lngDiff = ((altitude / Math.tan(pitchInRadian)) * Math.sin(-bearingInRadian)) / 70000; // ~70km/degree longitude + const latDiff = ((altitude / Math.tan(pitchInRadian)) * Math.cos(-bearingInRadian)) / 110000; // 110km/degree latitude correctedLng = targetPosition.lng + lngDiff; correctedLat = targetPosition.lat - latDiff; - } - + const newCameraPosition = { - lng: correctedLng, - lat: correctedLat + lng: correctedLng, + lat: correctedLat, }; - + if (smooth) { - if (this.previousCameraPosition) { - newCameraPosition.lng = this.lerp(newCameraPosition.lng, this.previousCameraPosition.lng, this.SMOOTH_FACTOR); - newCameraPosition.lat = this.lerp(newCameraPosition.lat, this.previousCameraPosition.lat, this.SMOOTH_FACTOR); - } + if (this.previousCameraPosition) { + newCameraPosition.lng = this.lerp(newCameraPosition.lng, this.previousCameraPosition.lng, this.SMOOTH_FACTOR); + newCameraPosition.lat = this.lerp(newCameraPosition.lat, this.previousCameraPosition.lat, this.SMOOTH_FACTOR); + } } - - this.previousCameraPosition = newCameraPosition - + + this.previousCameraPosition = newCameraPosition; + return newCameraPosition; - }; - - public static createGeoJSONCircle = (center: number[], radiusInKm: number, points = 64): Feature<Geometry, GeoJsonProperties>=> { + }; + + public static createGeoJSONCircle = (center: number[], radiusInKm: number, points = 64): Feature<Geometry, GeoJsonProperties> => { const coords = { - latitude: center[1], - longitude: center[0], + latitude: center[1], + longitude: center[0], }; const km = radiusInKm; const ret = []; - const distanceX = km / (111.320 * Math.cos((coords.latitude * Math.PI) / 180)); + const distanceX = km / (111.32 * Math.cos((coords.latitude * Math.PI) / 180)); const distanceY = km / 110.574; let theta; let x; let y; for (let i = 0; i < points; i += 1) { - theta = (i / points) * (2 * Math.PI); - x = distanceX * Math.cos(theta); - y = distanceY * Math.sin(theta); - ret.push([coords.longitude + x, coords.latitude + y]); + theta = (i / points) * (2 * Math.PI); + x = distanceX * Math.cos(theta); + y = distanceY * Math.sin(theta); + ret.push([coords.longitude + x, coords.latitude + y]); } ret.push(ret[0]); return { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [ret], - }, - properties: {} + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ret], + }, + properties: {}, }; - } + }; - private calculateBearing( - from: { lng: number; lat: number }, - to: { lng: number; lat: number } - ): number { + private calculateBearing(from: { lng: number; lat: number }, to: { lng: number; lat: number }): number { const lon1 = from.lng; const lat1 = from.lat; const lon2 = to.lng; const lat2 = to.lat; - + const lon1Rad = (lon1 * Math.PI) / 180; const lon2Rad = (lon2 * Math.PI) / 180; const lat1Rad = (lat1 * Math.PI) / 180; const lat2Rad = (lat2 * Math.PI) / 180; - + const y = Math.sin(lon2Rad - lon1Rad) * Math.cos(lat2Rad); - const x = - Math.cos(lat1Rad) * Math.sin(lat2Rad) - - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lon2Rad - lon1Rad); - - let bearing = Math.atan2(y,x); - + const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(lon2Rad - lon1Rad); + + let bearing = Math.atan2(y, x); + // Convert bearing from radians to degrees bearing = (bearing * 180) / Math.PI; - + // Ensure the bearing is within [0, 360) if (bearing < 0) { - bearing += 360; + bearing += 360; } - - return bearing; - } - -}
\ No newline at end of file + return bearing; + } +} |