-
Notifications
You must be signed in to change notification settings - Fork 208
/
GlobeAnimator.ts
222 lines (189 loc) · 10.5 KB
/
GlobeAnimator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Views
*/
import { Arc3d, Geometry, Point3d, SmoothTransformBetweenFrusta } from "@itwin/core-geometry";
import { Cartographic, Easing, Frustum, GlobeMode, Interpolation, Tweens } from "@itwin/core-common";
import {
areaToEyeHeight, areaToEyeHeightFromGcs, eyeToCartographicOnGlobe, GlobalLocation, metersToRange, ViewGlobalLocationConstants,
} from "./ViewGlobalLocation";
import { ScreenViewport } from "./Viewport";
import { Animator } from "./ViewAnimation";
/** Animates the transition of a [[Viewport]] to view a location on the Earth. The animation traces a flight path from the viewport's current [Frustum]($common) to the destination.
* The duration of the animation varies based on the distance traversed.
* @see [[Viewport.animateFlyoverToGlobalLocation]].
* @public
* @extensions
*/
export class GlobeAnimator implements Animator {
protected _flightTweens = new Tweens();
protected _viewport: ScreenViewport;
protected _startCartographic?: Cartographic;
protected _ellipsoidArc?: Arc3d;
protected _columbusLine: Point3d[] = [];
protected _flightLength = 0;
protected _endLocation: GlobalLocation;
protected _endHeight?: number;
protected _midHeight?: number;
protected _startHeight?: number;
protected _fixTakeoffInterpolator?: SmoothTransformBetweenFrusta;
protected _fixTakeoffFraction?: number;
protected _fixLandingInterpolator?: SmoothTransformBetweenFrusta;
protected _afterLanding: Frustum;
protected _afterFocusDistance: number;
protected readonly _fixLandingFraction: number = 0.9;
protected readonly _scratchFrustum = new Frustum();
protected _moveFlightToFraction(fraction: number): boolean {
const vp = this._viewport;
const view = vp.view;
if (!(view.is3d()) || !vp.iModel.isGeoLocated) // This animation only works for 3d views and geolocated models
return true;
// If we're done, set the final state directly
if (fraction >= 1.0) {
if (vp.view.is3d()) // Need to reset focus as well -- setupViewFromFustum does not set this and it will remain at flight distance.
vp.view.camera.setFocusDistance(this._afterFocusDistance);
vp.setupViewFromFrustum(this._afterLanding);
vp.synchWithView();
return true;
}
// Possibly smooth the takeoff
if (fraction < this._fixTakeoffFraction! && this._fixTakeoffInterpolator !== undefined) {
this._moveFixToFraction((1.0 / this._fixTakeoffFraction!) * fraction, this._fixTakeoffInterpolator);
return false;
}
// Possibly smooth the landing
if (fraction >= this._fixLandingFraction && fraction < 1.0) {
if (this._fixLandingInterpolator === undefined) {
const beforeLanding = vp.getWorldFrustum();
this._fixLandingInterpolator = SmoothTransformBetweenFrusta.create(beforeLanding.points, this._afterLanding.points);
}
this._moveFixToFraction((1.0 / (1.0 - this._fixLandingFraction)) * (fraction - this._fixLandingFraction), this._fixLandingInterpolator!);
return false;
}
// Set the camera based on a fraction along the flight arc
const height: number = Interpolation.Bezier([this._startHeight, this._midHeight, this._endHeight], fraction);
let targetPoint: Point3d;
if (view.globeMode === GlobeMode.Plane)
targetPoint = this._columbusLine[0].interpolate(fraction, this._columbusLine[1]);
else
targetPoint = this._ellipsoidArc!.fractionToPoint(fraction);
view.lookAtGlobalLocation(height, ViewGlobalLocationConstants.birdPitchAngleRadians, undefined, targetPoint);
vp.setupFromView();
return false;
}
/** Apply a SmoothTransformBetweenFrusta interpolator to the view based on a fraction. */
protected _moveFixToFraction(fract: number, interpolator: SmoothTransformBetweenFrusta): boolean {
let done = false;
if (fract >= 1.0) {
fract = 1.0;
done = true;
}
interpolator.fractionToWorldCorners(Math.max(fract, 0), this._scratchFrustum.points);
this._viewport.setupViewFromFrustum(this._scratchFrustum);
return done;
}
/** Create an animator to transition to the specified destination.
* @param viewport The viewport to animate.
* @param destination The destination to travel to.
* @returns An animator, or undefined if the viewport's iModel is not geolocated or its view is not 3d.
*/
public static async create(viewport: ScreenViewport, destination: GlobalLocation): Promise<GlobeAnimator | undefined> {
const view = viewport.view;
if (!(view.is3d()) || !viewport.iModel.isGeoLocated) // This animation only works for 3d views and geolocated models
return undefined;
const endHeight = destination.area !== undefined ? await areaToEyeHeightFromGcs(view, destination.area, destination.center.height) : ViewGlobalLocationConstants.birdHeightAboveEarthInMeters;
const beforeFrustum = viewport.getWorldFrustum();
await view.lookAtGlobalLocationFromGcs(endHeight, ViewGlobalLocationConstants.birdPitchAngleRadians, destination);
viewport.setupFromView();
const afterLanding = viewport.getWorldFrustum();
const afterFocus = view.camera.focusDist;
viewport.setupViewFromFrustum(beforeFrustum); // revert old frustum
return new GlobeAnimator(viewport, destination, afterLanding, afterFocus);
}
protected constructor(viewport: ScreenViewport, destination: GlobalLocation, afterLanding: Frustum, afterFocus: number) {
this._viewport = viewport;
this._endLocation = destination;
this._afterLanding = afterLanding;
this._afterFocusDistance = afterFocus;
const view = viewport.view;
if (!(view.is3d()) || !viewport.iModel.isGeoLocated) // This animation only works for 3d views and geolocated models
return;
// Calculate start height as the height of the current eye above the earth.
// Calculate end height from the destination area (if specified); otherwise, use a constant value.
const backgroundMapGeometry = view.displayStyle.getBackgroundMapGeometry();
if (undefined === backgroundMapGeometry)
return;
this._startHeight = eyeToCartographicOnGlobe(this._viewport, true)!.height;
this._endHeight = destination.area !== undefined ? areaToEyeHeight(view, destination.area, destination.center.height) : ViewGlobalLocationConstants.birdHeightAboveEarthInMeters;
// Starting cartographic position is the eye projected onto the globe.
let startCartographic = eyeToCartographicOnGlobe(viewport);
if (startCartographic === undefined) {
startCartographic = Cartographic.createZero();
}
this._startCartographic = startCartographic;
let maxFlightDuration: number;
if (view.globeMode === GlobeMode.Plane) {
// Calculate a line segment going from the starting cartographic coordinate to the ending cartographic coordinate
this._columbusLine.push(view.cartographicToRoot(startCartographic)!);
this._columbusLine.push(view.cartographicToRoot(this._endLocation.center)!);
this._flightLength = this._columbusLine[0].distance(this._columbusLine[1]);
// Set a shorter flight duration in Plane mode
maxFlightDuration = 7000.0;
} else {
// Calculate a flight arc from the ellipsoid of the Earth and the starting and ending cartographic coordinates.
const earthEllipsoid = backgroundMapGeometry.getEarthEllipsoid();
this._ellipsoidArc = earthEllipsoid.radiansPairToGreatArc(this._startCartographic.longitude, this._startCartographic.latitude, this._endLocation.center.longitude, this._endLocation.center.latitude)!;
if (this._ellipsoidArc !== undefined)
this._flightLength = this._ellipsoidArc.curveLength();
// Set a longer flight duration in 3D mode
maxFlightDuration = 13000.0;
}
if (Geometry.isSmallMetricDistance(this._flightLength))
return;
// The peak of the flight varies based on total distance to travel. The larger the distance, the higher the peak of the flight will be.
this._midHeight = metersToRange(this._flightLength,
ViewGlobalLocationConstants.birdHeightAboveEarthInMeters,
ViewGlobalLocationConstants.satelliteHeightAboveEarthInMeters * 4,
ViewGlobalLocationConstants.largestEarthArc);
// We will "fix" the initial frustum so it smoothly transitions to some point along the travel arc depending on the starting height.
// Alternatively, if the distance to travel is small enough, we will _only_ do a frustum transition to the destination location - ignoring the flight arc.
const beforeTakeoff = viewport.getWorldFrustum();
if (view.globeMode === GlobeMode.Plane) {
/// Do not "fix" the take-off for plane mode; SmoothTransformBetweenFrusta can behave wrongly.
// However, if within driving distance, still use SmoothTransformBetweenFrusta to navigate there without flight.
this._fixTakeoffFraction = this._flightLength <= ViewGlobalLocationConstants.maximumDistanceToDrive ? 1.0 : 0.0;
} else {
this._fixTakeoffFraction = this._flightLength <= ViewGlobalLocationConstants.maximumDistanceToDrive ? 1.0 : metersToRange(this._startHeight, 0.1, 0.4, ViewGlobalLocationConstants.birdHeightAboveEarthInMeters);
}
if (this._fixTakeoffFraction > 0.0) {
this._moveFlightToFraction(this._fixTakeoffFraction);
const afterTakeoff = viewport.getWorldFrustum();
this._fixTakeoffInterpolator = SmoothTransformBetweenFrusta.create(beforeTakeoff.points, afterTakeoff.points);
}
// The duration of the animation will increase the larger the distance to travel.
const flightDurationInMilliseconds = metersToRange(this._flightLength, 1000, maxFlightDuration, ViewGlobalLocationConstants.largestEarthArc);
// Specify the tweening behavior for this animation.
this._flightTweens.create({ fraction: 0.0 }, {
to: { fraction: 1.0 },
duration: flightDurationInMilliseconds,
easing: Easing.Cubic.InOut,
start: true,
onUpdate: (obj: any) => this._moveFlightToFraction(obj.fraction),
});
}
/** @internal */
public animate() {
if (this._flightLength <= 0) {
this._moveFlightToFraction(1.0); // Skip to final frustum
return true;
}
return !this._flightTweens.update();
}
/** @internal */
public interrupt() {
this._moveFlightToFraction(1.0); // Skip to final frustum
}
}