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(['./CoordinateSystem', './RenderContext','./TileManager','./Tile' , './VectorRendererManager', './Numeric', './GeoBound' ], 
 21 	function(CoordinateSystem, RenderContext, TileManager, Tile, VectorRendererManager, Numeric, GeoBound) {
 22 
 23 /**************************************************************************************************************/
 24 
 25 /** 
 26 	@name Globe
 27 	@class
 28 	Create a virtual globe in a HTML canvas element, passed in options parameter.
 29 	The virtual globe data is set using setBaseImage/addLayer methods.
 30 	
 31 	@param options Configuration properties for the Globe :
 32 		<ul>
 33 			<li>canvas : the canvas for WebGL, can be string (id) or a canvas element</li>
 34 			<li>contextAttribs : the attributes when creating WebGL context, see WebGL specification</li>
 35 			<li>backgroundColor : the background color of the canvas (an array of 4 floats)</li>
 36 			<li>shadersPath : the path to shaders file</li>
 37 			<li>continuousRendering: if true rendering is done continuously, otherwise it is done only if needed</li>
 38 		</ul>
 39 	
 40  */
 41 var Globe = function(options)
 42 {
 43 	this.renderContext = new RenderContext(options);
 44 	this.tileManager = new TileManager( this );
 45 	this.vectorRendererManager = new VectorRendererManager( this );
 46 	this.attributionHandler = null;
 47 	this.activeAnimations = [];
 48 	this.preRenderers = [];
 49 	this.nbCreatedLayers = 0;
 50 	
 51 	// Event callbacks
 52 	this.callbacks = {};
 53 	
 54 	var glob = this;	
 55 	this.renderContext.frame = function() 
 56 	{		
 57 		// Resest frame requested flag first
 58 		glob.renderContext.frameRequested = false;
 59 				
 60 		// Render the globe
 61 		glob.render();
 62 		
 63 		// Request next frame
 64 		if ( glob.renderContext.continuousRendering )
 65 		{
 66 			glob.renderContext.requestFrame();
 67 		}
 68 		else if ( glob.activeAnimations.length > 0 )
 69 		{
 70 			glob.renderContext.requestFrame();
 71 		}
 72 		
 73 	};
 74 	
 75 	this.renderContext.requestFrame();
 76 }
 77 
 78 /**************************************************************************************************************/
 79 
 80 /** 
 81 	Dispose the globe and all its ressources
 82  */
 83 Globe.prototype.dispose = function()
 84 {	
 85 	this.tileManager.tilePool.disposeAll();
 86 	this.tileManager.reset();
 87 }
 88 
 89 
 90 /**************************************************************************************************************/
 91 
 92 /** 
 93   Refresh rendering, must be called when canvas size is modified
 94  */
 95 Globe.prototype.refresh = function()
 96 {
 97 	this.renderContext.requestFrame();
 98 }
 99 
100 /**************************************************************************************************************/
101 
102 /** 
103   Set the base imagery layer for the globe
104   
105   @param {RasterLayer} layer the layer to use, must be an imagery RasterLayer
106 */
107 Globe.prototype.setBaseImagery = function(layer)
108 {
109 	if ( this.tileManager.imageryProvider )
110 	{
111 		this.removeLayer( this.tileManager.imageryProvider );
112 	}
113 	this.tileManager.setImageryProvider(layer);
114 	if ( layer )
115 	{
116 		layer._overlay = false;
117 		this.addLayer(layer);
118 	}
119 }
120 
121 /**************************************************************************************************************/
122 
123 /** 
124   Set the base elevation layer for the globe
125   
126   @param {RasterLayer} layer the layer to use, must be an elevation RasterLayer
127 */
128 Globe.prototype.setBaseElevation = function(layer)
129 {
130 	if ( this.tileManager.elevationProvider )
131 	{
132 		this.removeLayer( this.tileManager.elevationProvider );
133 	}
134 	this.tileManager.setElevationProvider(layer);
135 	if ( layer )
136 	{
137 		layer._overlay = false;
138 		this.addLayer(layer);
139 	}
140 }
141 
142 
143 /**************************************************************************************************************/
144 
145 /** 
146   Add a layer to the globe.
147   A layer must be added to be visualized on the globe.
148   
149   @param layer the layer to add
150 */
151 Globe.prototype.addLayer = function(layer)
152 {
153 	layer.id = this.nbCreatedLayers;
154 	layer._attach(this);
155 	this.renderContext.requestFrame();
156 	this.nbCreatedLayers++;
157 }
158 
159 /**************************************************************************************************************/
160 
161 /** 
162   Remove a layer
163   
164   @param layer the layer to remove
165 */
166 Globe.prototype.removeLayer = function(layer)
167 {
168 	layer._detach();
169 	this.renderContext.requestFrame();
170 }
171 
172 /**************************************************************************************************************/
173 
174 /** 
175   Add an animation
176   
177   @param anim the animation to add
178 */
179 Globe.prototype.addAnimation = function(anim)
180 {
181 	anim.globe = this;
182 }
183 
184 /**************************************************************************************************************/
185 
186 /** 
187   Remove an animation
188   
189   @param anim the animation to remove
190 */
191 Globe.prototype.removeAnimation = function(anim)
192 {
193 	anim.globe = null;
194 }
195 
196 /**************************************************************************************************************/
197 
198 /** 
199   Get the elevation at a geo position
200   
201   @param lon the longitude in degree
202   @param lat  the latitude in degree
203   @return the elevation in meter at the position [lon,lat]
204 */
205 Globe.prototype.getElevation = function(lon,lat)
206 {
207 	var tiling = this.tileManager.imageryProvider.tiling;
208 	var levelZeroTile = this.tileManager.level0Tiles[ tiling.lonlat2LevelZeroIndex(lon,lat) ];
209 	if ( levelZeroTile.state == Tile.State.LOADED )
210 		return levelZeroTile.getElevation(lon,lat);
211 	else
212 		return 0.0;
213 }
214 
215 /**************************************************************************************************************/
216 
217 /** 
218 	Get the viewport geo bound
219     @return the geo bound of the viewport
220 */
221 Globe.prototype.getViewportGeoBound = function()
222 {
223 	var rc = this.renderContext;
224 	var tmpMat = mat4.create();
225 	
226 	// Compute eye in world space
227 	mat4.inverse(rc.viewMatrix, tmpMat);
228 	var eye = [tmpMat[12], tmpMat[13], tmpMat[14]];
229 	
230 	// Compute the inverse of view/proj matrix
231 	mat4.multiply(rc.projectionMatrix, rc.viewMatrix, tmpMat);
232 	mat4.inverse(tmpMat);
233 	
234 	// Transform the four corners of the frustum into world space
235 	// and then for each corner compute the intersection of ray starting from the eye with the earth
236 	var points = [ [ -1, -1, 1, 1 ], [ 1, -1, 1, 1 ], [ -1, 1, 1, 1 ], [ 1, 1, 1, 1 ] ];
237 	var tmpPt = vec3.create();
238 	var earthCenter = [ 0, 0, 0 ];
239 	for ( var i = 0; i < 4; i++ )
240 	{
241 		mat4.multiplyVec4( tmpMat, points[i] );
242 		vec3.scale( points[i], 1.0 / points[i][3] );
243 		vec3.subtract(points[i], eye, points[i]);
244 		vec3.normalize( points[i] );
245 		
246 		var t = Numeric.raySphereIntersection( eye, points[i], earthCenter, CoordinateSystem.radius);
247 		if ( t < 0.0 )
248 			return null;
249 			
250 		points[i] = CoordinateSystem.from3DToGeo( Numeric.pointOnRay(eye, points[i], t, tmpPt) );
251 	}
252 
253 	var geoBound = new GeoBound();
254 	geoBound.computeFromCoordinates( points );
255 
256 	return geoBound;
257 }
258 
259 /**************************************************************************************************************/
260 
261 /** 
262 	Get the lon-lat from a pixel.
263 	The pixel is expressed in the canvas frame, i.e. (0,0) corresponds to the lower-left corner of the pixel
264 	
265 	@param 	x the pixel x coordinate
266 	@param 	y the pixel y coordinate
267 	@return	an array of two numbers [lon,lat] or null if the pixel is not on the globe
268  */
269 Globe.prototype.getLonLatFromPixel = function(x,y)
270 {	
271 	var pos3d = this.renderContext.get3DFromPixel(x,y);
272 	if ( pos3d )
273 	{
274 		return CoordinateSystem.from3DToGeo(pos3d);
275 	}
276 	else
277 	{
278 		return null;
279 	}
280 }
281 
282 /**************************************************************************************************************/
283 
284 /** 
285 	Get pixel from lon-lat
286 	The pixel is expressed in the canvas frame, i.e. (0,0) corresponds to the lower-left corner of the pixel
287 	
288 	@param lon	the longitude
289 	@param lat	the latitude
290 	@return	an array of two numbers [x,y] or null if the pixel is not on the globe
291  */
292 Globe.prototype.getPixelFromLonLat = function(lon,lat)
293 {	
294 	var pos3d = vec3.create();
295 	CoordinateSystem.fromGeoTo3D([lon,lat], pos3d);
296 	var pixel = this.renderContext.getPixelFrom3D(pos3d[0],pos3d[1],pos3d[2]);
297 	return pixel
298 }
299 
300 /**************************************************************************************************************/
301 
302 /**
303 	Render the globe
304 	TODO : private for now because it is automatically called in requestAnimationFrame.
305 	
306 	@private
307  */
308 Globe.prototype.render = function()
309 {
310 	var rc = this.renderContext;
311 	var stats = rc.stats;
312 	var gl = rc.gl;
313 		
314 	if (stats) stats.start("globalRenderTime");
315 	
316 	// Update active animations
317 	if ( this.activeAnimations.length > 0)
318 	{
319 		var time = Date.now();
320 		for (var i = 0; i < this.activeAnimations.length; i++)
321 		{
322 			this.activeAnimations[i].update(time);
323 		}
324 	}
325 	
326 	// Clear the buffer
327 	gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
328 	
329 	// Check canvas size is valid
330 	if ( rc.canvas.width == 0 || rc.canvas.height == 0 )
331 		return;
332 		
333 	gl.viewport(0, 0, rc.canvas.width, rc.canvas.height);
334 
335 	// Update view dependent properties to be used during rendering : view matrix, frustum, projection, etc...
336 	rc.updateViewDependentProperties();
337 	
338 	// Call pre-renderers
339 	for ( var i = 0 ; i < this.preRenderers.length; i++ )
340 		this.preRenderers[i].preRender();
341 		
342 	// Render tiles
343 	this.tileManager.render();
344 	
345 	if ( this.tileManager.tilesToRender.length == 0 )
346 		return;
347 		
348 	if (stats) stats.end("globalRenderTime");
349 }
350 
351 /**************************************************************************************************************/
352 
353 /** 
354 	Subscribe to an event
355 	
356 	@param name Event name
357 		<ul>
358 			<li>startNavigation : called when navigation is started (by the user or through animation)</li>
359 			<li>endNavigation : called when navigation is ended (by the user or through animation)t</li>
360 			<li>baseLayersReady : called when the base layers are ready to be displayed</li>
361 			<li>baseLayersError : called when the base layers are not valid, or not accessible, in that case nothing is displayed so this event is useful to provide an error message to the user</li>
362 			<li>startBackgroundLoad : called when background layers (imagery and/or elevation) start to be loaded by GlobWeb</li>
363 			<li>endBackgroundLoad : called when background layers (imagery and/or elevation) end loadinh by GlobWeb</li>
364 		</ul>
365 	@param callback Callback function
366 */
367 Globe.prototype.subscribe = function(name,callback)
368 {
369 	if( !this.callbacks[name] ) {
370 		this.callbacks[name] = [ callback ];
371 	} else {
372 		this.callbacks[name].push( callback );
373 	}
374 }
375 
376 /**************************************************************************************************************/
377 
378 /** 
379 	Unsubscribe to an event 
380 	
381 	@param name Event name {@link Globe#subscribe}
382 	@param callback Callback function
383 */
384 Globe.prototype.unsubscribe = function(name,callback)
385 {
386 	if( this.callbacks[name] ) {
387 		var i = this.callbacks[name].indexOf( callback );
388 		if ( i != -1 ) {
389 			this.callbacks[name].splice(i,1);
390 		}
391 	}
392 }
393 
394 /**************************************************************************************************************/
395 
396 /**
397 	Publish an event
398 	
399 	@param name Event name
400 	@param context Context
401 	
402 	@private
403 */
404 Globe.prototype.publish = function(name,context)
405 {
406 	if ( this.callbacks[name] ) {
407 		var cbs = this.callbacks[name];
408 		for ( var i = 0; i < cbs.length; i++ ) {
409 			cbs[i](context);
410 		}
411 	}
412 }
413 
414 return Globe;
415 
416 });
417