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