1 /***************************************
  2  * Copyright 2011, 2012 GlobWeb contributors.
  3  *
  4  * This file is part of GlobWeb.
  5  *
  6  * GlobWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU Lesser General Public License as published by
  8  * the Free Software Foundation, version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * GlobWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 14  * GNU Lesser General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with GlobWeb. If not, see <http://www.gnu.org/licenses/>.
 18  ***************************************/
 19 
 20  define(['./Utils', './CoordinateSystem', './BaseNavigation', './SegmentedAnimation', './Numeric', './glMatrix'], function(Utils,CoordinateSystem,BaseNavigation,SegmentedAnimation,Numeric) {
 21 
 22 /**************************************************************************************************************/
 23 
 24 /** @name Navigation
 25 	@class
 26 	@augments BaseNavigation
 27 
 28 	Manages the navigation in the Globe.
 29 	
 30 	@param globe Globe
 31 	@param options Configuration properties for the Navigation :
 32 		<ul>
 33 			<li>minDistance : The minimum distance</li>
 34 			<li>maxDistance : The maximum distance</li>
 35 		</ul>
 36  */
 37 var Navigation = function(globe,options)
 38 {
 39 	// Default values for min and max distance (in meter)
 40 	this.minDistance = 1.0;
 41 	this.maxDistance = 3.0 * CoordinateSystem.realEarthRadius;
 42 	
 43 	BaseNavigation.prototype.constructor.call( this, globe, options );
 44 	
 45 	// Initialize the navigation
 46 	this.geoCenter = [0.0, 0.0, 0.0];
 47 	this.heading = 0.0;
 48 	this.tilt = 90.0;
 49 	this.distance = 3.0 * CoordinateSystem.radius;
 50 		
 51 	// Scale min and max distance from meter to internal ratio
 52 	this.minDistance *= CoordinateSystem.heightScale;
 53 	this.maxDistance *= CoordinateSystem.heightScale;
 54 
 55 	this.inverseViewMatrix = mat4.create();
 56 
 57 	// Update the view matrix now
 58 	this.computeViewMatrix();
 59 }
 60 
 61 /**************************************************************************************************************/
 62 
 63 Utils.inherits( BaseNavigation,Navigation );
 64 
 65 /**************************************************************************************************************/
 66 
 67 /** @export
 68 	Zoom to a 3d position
 69 	@param {Float[]} geoPos Array of two floats corresponding to final Longitude and Latitude(in this order) to zoom
 70 	@param {Int} distance Final zooming distance in meters
 71 	@param {Int} duration Duration of animation in milliseconds
 72 	@param {Int} tilt Defines the tilt in the end of animation
 73 */
 74 Navigation.prototype.zoomTo = function(geoPos, distance, duration, tilt )
 75 {
 76 	var navigation = this;
 77 	
 78 	var destDistance = distance || this.distance / (4.0 * CoordinateSystem.heightScale);
 79 	duration = duration || 5000;
 80 	var destTilt = tilt || 90;
 81 	
 82 	// Create a single animation to animate geoCenter, distance and tilt
 83 	var startValue = [this.geoCenter[0], this.geoCenter[1], this.distance, this.tilt];
 84 	var endValue = [geoPos[0], geoPos[1], destDistance * CoordinateSystem.heightScale, destTilt];
 85 	this.zoomToAnimation = new SegmentedAnimation(
 86 		duration,
 87 		// Value setter
 88 		function(value) {
 89 			navigation.geoCenter[0] = value[0];
 90 			navigation.geoCenter[1] = value[1];
 91 			navigation.distance = value[2];
 92 			navigation.tilt = value[3];
 93 			navigation.computeViewMatrix();
 94 		});
 95 
 96 	// Compute a max altitude for the animation
 97 	var worldStart = CoordinateSystem.fromGeoTo3D(this.geoCenter);
 98 	var worldEnd   = CoordinateSystem.fromGeoTo3D(geoPos);
 99 	var vec = vec3.subtract(worldStart, worldEnd);
100 	var len = vec3.length(vec);
101 	var canvas = this.globe.renderContext.canvas;
102 	var minFov = Math.min(Numeric.toRadian(45.0),
103 				Numeric.toRadian(45.0 * canvas.width / canvas.height));
104 	var maxAltitude = 1.1 * ((len / 2.0) / Math.tan(minFov / 2.0));
105 	if (maxAltitude > this.distance)
106 	{
107 		// Compute the middle value
108 		var midValue = [startValue[0]*0.5 + endValue[0]*0.5,
109 				startValue[1]*0.5 + endValue[1]*0.5,
110 				maxAltitude, destTilt];
111 
112 		// Add two segments
113 		this.zoomToAnimation.addSegment(
114 		0.0, startValue,
115 		0.5, midValue,
116 		function(t, a, b) {
117 			var pt = Numeric.easeInQuad(t);
118 			var dt = Numeric.easeOutQuad(t);
119 			return [Numeric.lerp(pt, a[0], b[0]), // geoPos.long
120 				Numeric.lerp(pt, a[1], b[1]), // geoPos.lat
121 				Numeric.lerp(dt, a[2], b[2]), // distance
122 				Numeric.lerp(t, a[3], b[3])]; // tilt
123 		});
124 
125 		this.zoomToAnimation.addSegment(
126 		0.5, midValue,
127 		1.0, endValue,
128 		function(t, a, b) {
129 			var pt = Numeric.easeOutQuad(t);
130 			var dt = Numeric.easeInQuad(t);
131 			return [Numeric.lerp(pt, a[0], b[0]), // geoPos.long
132 				Numeric.lerp(pt, a[1], b[1]), // geoPos.lat
133 				Numeric.lerp(dt, a[2], b[2]), // distance
134 				Numeric.lerp(t, a[3], b[3])]; // tilt
135 		});
136 	}
137 	else
138 	{
139 		// Add only one segments
140 		this.zoomToAnimation.addSegment(
141 		0.0, startValue,
142 		1.0, endValue,
143 		function(t, a, b) {
144 			var pt = Numeric.easeOutQuad(t);
145 			var dt = Numeric.easeInQuad(t);
146 			return [Numeric.lerp(pt, a[0], b[0]),  // geoPos.long
147 				Numeric.lerp(pt, a[1], b[1]),  // geoPos.lat
148 				Numeric.lerp(dt, a[2], b[2]),  // distance
149 				Numeric.lerp(t, a[3], b[3])]; // tilt
150 		});
151 	}
152 
153 	this.zoomToAnimation.onstop = function() {
154 		navigation.globe.publish("endNavigation");
155 	}
156 	this.globe.addAnimation(this.zoomToAnimation);
157 	this.zoomToAnimation.start();
158 	
159 	this.globe.publish("startNavigation");
160 }
161 
162 /**************************************************************************************************************/
163 
164 /**
165 	Compute the inverse view matrix
166  */
167 Navigation.prototype.applyLocalRotation = function(matrix)
168 {
169 	mat4.rotate( matrix, (this.heading) * Math.PI / 180.0, [ 0.0, 0.0, 1.0 ] );
170 	mat4.rotate( matrix, (90 - this.tilt) * Math.PI / 180.0, [ 1.0, 0.0, 0.0 ] );
171 }
172 
173 /**************************************************************************************************************/
174 
175 /**
176 	Compute the view matrix
177  */
178 Navigation.prototype.computeViewMatrix = function()
179 {
180     this.computeInverseViewMatrix();
181 	mat4.inverse( this.inverseViewMatrix, this.globe.renderContext.viewMatrix );
182 }
183 
184 /**************************************************************************************************************/
185 
186 /**
187 	Compute the inverse view matrix
188  */
189 Navigation.prototype.computeInverseViewMatrix = function()
190 {
191     CoordinateSystem.getLHVTransform(this.geoCenter, this.inverseViewMatrix);
192 	this.applyLocalRotation(this.inverseViewMatrix);
193 	mat4.translate( this.inverseViewMatrix, [0.0, 0.0, this.distance] );
194 }
195 
196 /**************************************************************************************************************/
197 
198 /**
199 	Zoom to the current observed location
200 	@param delta Delta zoom
201  */
202 Navigation.prototype.zoom = function(delta)
203 {
204 	var previousDistance = this.distance;
205 	
206 	this.distance *= (1 + delta * 0.1);
207 		
208 	if ( this.distance > this.maxDistance )
209 	{
210 		this.distance = this.maxDistance;
211 	}
212 	if ( this.distance < this.minDistance )
213 	{
214 		this.distance = this.minDistance;
215 	}
216 
217 	this.computeViewMatrix();
218 	
219 	if ( this.hasCollision() )
220 	{
221 		this.distance = previousDistance;
222 		this.computeViewMatrix();
223 	}
224 }
225 
226 /**************************************************************************************************************/
227 
228 /**
229 	Check for collision
230  */
231 Navigation.prototype.hasCollision = function()
232 {
233 	var eye = [ this.inverseViewMatrix[12], this.inverseViewMatrix[13], this.inverseViewMatrix[14] ];
234 	var geoEye = vec3.create();
235 	CoordinateSystem.from3DToGeo(eye, geoEye);
236 	var elevation = this.globe.getElevation( geoEye[0], geoEye[1] );
237 	
238 	return geoEye[2] < elevation + 50;
239 }
240 
241 /**************************************************************************************************************/
242 
243 /**
244 	Pan the navigation
245 	@param dx Window delta x
246 	@param dy Window delta y
247 */
248 Navigation.prototype.pan = function(dx, dy)
249 {
250 	var previousGeoCenter = vec3.create();
251 	vec3.set( this.geoCenter, previousGeoCenter );
252 	
253 	// Get geographic frame
254 	var local2World = mat4.create();
255 	CoordinateSystem.getLocalTransform(this.geoCenter, local2World);
256 	// Then corresponding vertical axis and north
257 	var z = vec3.create(); var previousNorth = vec3.create([0.0, 1.0, 0.0]);
258 	CoordinateSystem.getUpVector( local2World, z );
259 	//CoordinateSystem.getFrontVector( local2World, previousNorth );
260 	mat4.multiplyVec3(local2World, previousNorth, previousNorth);
261 	
262 	// Then apply local transform
263 	this.applyLocalRotation(local2World);
264 	// Retrieve corresponding axes
265 	var x = vec3.create(); var y = vec3.create();
266 	CoordinateSystem.getSideVector( local2World, x );
267 	CoordinateSystem.getFrontVector( local2World, y );
268 	// According to our local configuration, up is y and side is x
269 	
270 	// Compute direction axes
271 	vec3.cross(z, x, y);
272 	vec3.cross(y, z, x);
273 	vec3.normalize(x, x);
274 	vec3.normalize(y, y);
275 	
276 	//Normalize dx and dy
277 	dx = dx / this.globe.renderContext.canvas.width;
278 	dy = dy / this.globe.renderContext.canvas.height;
279 	
280 	// Move accordingly
281 	var position = vec3.create();
282 	CoordinateSystem.fromGeoTo3D(this.geoCenter, position);
283 	vec3.scale(x, dx * this.distance, x);
284 	vec3.scale(y, dy * this.distance, y);
285 	vec3.subtract(position, x, position);
286 	vec3.add(position, y, position);
287 	
288 	// Clamp onto sphere
289 	vec3.normalize(position);
290 	vec3.scale(position, CoordinateSystem.radius);
291 	
292 	// Update geographic center
293 	CoordinateSystem.from3DToGeo(position, this.geoCenter);
294 
295 	// Compute new north axis
296 	var newNorth = vec3.create([0.0, 1.0, 0.0]);
297 	CoordinateSystem.getLocalTransform(this.geoCenter, local2World);
298 	mat4.multiplyVec3(local2World, newNorth, newNorth);
299 	
300 	// Take care if we traverse the pole, ie the north is inverted
301 	if ( vec3.dot(previousNorth, newNorth) < 0 )
302 	{
303 		this.heading = (this.heading + 180.0) % 360.0;
304 	}
305 		
306 	// Check for collision with terrain
307 	this.computeViewMatrix();
308 	
309 	if ( this.hasCollision() )
310 	{
311 		this.geoCenter = previousGeoCenter;
312 		this.computeViewMatrix();
313 	}
314 }
315 
316 /**************************************************************************************************************/
317 
318 /**
319 	Rotate the navigation
320 	@param dx Window delta x
321 	@param dy Window delta y
322  */
323 Navigation.prototype.rotate = function(dx,dy)
324 {
325 	var previousHeading = this.heading;
326 	var previousTilt = this.tilt;
327 	
328 	this.heading += dx * 0.1;
329 	this.tilt += dy * 0.1;
330 	
331 	this.computeViewMatrix();
332 
333 	if ( this.hasCollision() )
334 	{
335 		this.heading = previousHeading;
336 		this.tilt = previousTilt;
337 		this.computeViewMatrix();
338 	}
339 }
340 
341 /**************************************************************************************************************/
342 
343 return Navigation;
344 
345 });
346