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( ['./BaseLayer', './Utils', './Program', './Mesh', './CoordinateSystem'],
 21 		function(BaseLayer, Utils, Program, Mesh, CoordinateSystem) {
 22  
 23 /**************************************************************************************************************/
 24 
 25 /** 
 26 	@constructor
 27 	Function constructor for EquatorialGridLayer
 28  */
 29 var EquatorialGridLayer = function( options )
 30 {
 31 	BaseLayer.prototype.constructor.call( this, options );
 32 	this.globe = null;
 33 
 34 	// Equatorial coordinates label renderables
 35 	this.labels = {};
 36 
 37 	// WebGL textures
 38 	this.texturePool = null;
 39 	
 40 	this.longitudeSample = options.longitudeSample || 15; // *24 = 360
 41 	this.latitudeSample = options.latitudeSample || 10; // *18 = 180
 42 
 43 	// Canvas for generation of equatorial coordinate labels
 44 	this.canvas2d = document.createElement("canvas");
 45 	this.canvas2d.width = 100;
 46 	this.canvas2d.height = 20;
 47 
 48 	// Grid buffers
 49 	this.vertexBuffer = null;
 50 	this.indexBuffer = null;
 51 }
 52 
 53 /**************************************************************************************************************/
 54 
 55 Utils.inherits( BaseLayer, EquatorialGridLayer );
 56 
 57 /**************************************************************************************************************/
 58 
 59 /**
 60  *	Generate image data from text
 61  *
 62  *	@param {String} text Text generated in canvas
 63  */
 64 EquatorialGridLayer.prototype.generateImageData = function(text)
 65 {
 66 	var ctx = this.canvas2d.getContext("2d");
 67 	ctx.clearRect(0,0, this.canvas2d.width, this.canvas2d.height);
 68 	ctx.fillStyle = '#fff';
 69 	ctx.font = '18px sans-serif';
 70 	ctx.textBaseline = 'top';
 71 	ctx.textAlign = 'center';
 72 	var x = this.canvas2d.width / 2;
 73 
 74 	ctx.fillText(text, x, 0);
 75 
 76 	return ctx.getImageData(0,0, this.canvas2d.width,this.canvas2d.height);
 77 }
 78 
 79 /**************************************************************************************************************/
 80 
 81 /** 
 82 	Attach the layer to the globe
 83  */
 84 EquatorialGridLayer.prototype._attach = function( g )
 85 {
 86 	BaseLayer.prototype._attach.call( this, g );
 87 	
 88 	if ( this._visible )
 89 	{
 90 		this.globe.tileManager.addPostRenderer(this);
 91 	}
 92 
 93 	if (!this.gridProgram)
 94 	{
 95 		var vertexShader = "\
 96 		attribute vec3 vertex;\n\
 97 		uniform mat4 viewProjectionMatrix;\n\
 98 		void main(void) \n\
 99 		{\n\
100 			gl_Position = viewProjectionMatrix * vec4(vertex, 1.0);\n\
101 		}\n\
102 		";
103 
104 		var fragmentShader = "\
105 		precision highp float; \n\
106 		uniform float alpha; \n\
107 		void main(void)\n\
108 		{\n\
109 			gl_FragColor = vec4(1.0,1.0,1.0,alpha);\n\
110 		}\n\
111 		";
112 		
113 		var vertexTextShader = "\
114 		attribute vec3 vertex; // vertex have z = 0, spans in x,y from -0.5 to 0.5 \n\
115 		uniform mat4 viewProjectionMatrix; \n\
116 		uniform vec3 poiPosition; // world position \n\
117 		uniform vec2 poiScale; // x,y scale \n\
118 		\n\
119 		varying vec2 texCoord; \n\
120 		\n\
121 		void main(void)  \n\
122 		{ \n\
123 			// Generate texture coordinates, input vertex goes from -0.5 to 0.5 (on x,y) \n\
124 			texCoord = vertex.xy + vec2(0.5); \n\
125 			// Invert y \n\
126 			texCoord.y = 1.0 - texCoord.y; \n\
127 			\n\
128 			// Compute poi position in clip coordinate \n\
129 			gl_Position = viewProjectionMatrix * vec4(poiPosition, 1.0); \n\
130 			gl_Position.xy += vertex.xy * gl_Position.w * poiScale; \n\
131 		} \n\
132 		";
133 	
134 		var fragmentTextShader = "\
135 		#ifdef GL_ES \n\
136 		precision highp float; \n\
137 		#endif \n\
138 		\n\
139 		varying vec2 texCoord; \n\
140 		uniform sampler2D texture; \n\
141 		uniform float alpha; \n\
142 		\n\
143 		void main(void) \n\
144 		{ \n\
145 			vec4 textureColor = texture2D(texture, texCoord); \n\
146 			gl_FragColor = vec4(textureColor.rgb, textureColor.a * alpha); \n\
147 		} \n\
148 		";
149 		
150 		this.gridProgram = new Program(this.globe.renderContext);
151 		this.textProgram = new Program(this.globe.renderContext);
152 		this.gridProgram.createFromSource( vertexShader, fragmentShader );
153 		this.textProgram.createFromSource( vertexTextShader, fragmentTextShader );
154 	}
155 	
156 	// Texture used to show the equatorial coordinates
157 	this.textMesh = new Mesh(this.globe.renderContext);
158 	var vertices = [-0.5, -0.5, 0.0,
159 			-0.5,  0.5, 0.0,
160 			0.5,  0.5, 0.0,
161 			0.5, -0.5, 0.0];
162 	var indices = [0, 3, 1, 1, 3, 2];
163 	this.textMesh.setVertices(vertices);
164 	this.textMesh.setIndices(indices);
165 
166 	// Init grid buffers	
167 	var gl = this.globe.renderContext.gl;
168 	this.vertexBuffer = gl.createBuffer();
169 	this.indexBuffer = gl.createBuffer();
170 
171 	// Init texture pool
172 	if ( !this.texturePool )
173 		this.texturePool = new TexturePool(gl);
174 }
175 
176 /**************************************************************************************************************/
177 
178 /** 
179 	Detach the layer from the globe
180  */
181 EquatorialGridLayer.prototype._detach = function()
182 {
183 	var gl = this.globe.renderContext.gl;
184 	gl.deleteBuffer( this.vertexBuffer );
185 	gl.deleteBuffer( this.indexBuffer );
186 
187 	this.texturePool.disposeAll();
188 	for ( var i in this.labels )
189 	{
190 		delete this.labels[i];
191 	}
192 
193 	this.globe.tileManager.removePostRenderer(this);
194 	BaseLayer.prototype._detach.call(this);
195 
196 }
197 
198 /**************************************************************************************************************/
199 
200 /**
201 	Render the grid
202  */
203 EquatorialGridLayer.prototype.render = function( tiles )
204 {
205 	var renderContext = this.globe.renderContext;
206 	var gl = renderContext.gl;
207 	
208 	gl.disable(gl.DEPTH_TEST);
209 	gl.enable(gl.BLEND);
210 	gl.blendEquation(gl.FUNC_ADD);
211 	gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
212 
213 	/*** Render grid ***/
214 	var geoBound = this.globe.getViewportGeoBound();
215 	this.computeSamples(geoBound)
216 	this.generateGridBuffers(geoBound);
217 	
218 	this.gridProgram.apply();
219 	mat4.multiply(renderContext.projectionMatrix, renderContext.viewMatrix, renderContext.modelViewMatrix)
220 	gl.uniformMatrix4fv(this.gridProgram.uniforms["viewProjectionMatrix"], false, renderContext.modelViewMatrix);
221 	gl.uniform1f(this.gridProgram.uniforms["alpha"], this._opacity );
222 	
223 	gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
224 	gl.vertexAttribPointer(this.gridProgram.attributes['vertex'], this.vertexBuffer.itemSize, gl.FLOAT, false, 0, 0);
225 	
226 	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
227 	gl.drawElements( gl.LINES, this.indexBuffer.numItems, gl.UNSIGNED_SHORT, 0);
228 	
229 	/*** Render label ***/
230 	this.generateText(geoBound);
231 	this.textProgram.apply();
232 	
233 	mat4.multiply(renderContext.projectionMatrix, renderContext.viewMatrix, renderContext.modelViewMatrix)
234 	gl.uniformMatrix4fv(this.textProgram.uniforms["viewProjectionMatrix"], false, renderContext.modelViewMatrix);
235 	gl.uniform1i(this.textProgram.uniforms["texture"], 0);
236 	
237 	var pixelSizeVector = renderContext.computePixelSizeVector();
238 	// for ( var n = 0; n < this.labels.length; n++ )
239 	for ( var n in this.labels )
240 	{
241 		var label = this.labels[n];
242 		// Bind point texture
243 		gl.activeTexture(gl.TEXTURE0);
244 		gl.bindTexture(gl.TEXTURE_2D, label.texture);
245 
246 		// 2.0 * because normalized device coordinates goes from -1 to 1
247 		var scale = [2.0 * label.textureWidth / renderContext.canvas.width,
248 					 2.0 * label.textureHeight / renderContext.canvas.height];
249 					 
250 		gl.uniform2fv(this.textProgram.uniforms["poiScale"], scale);
251 		// gl.uniform2fv(this.textProgram.uniforms["tst"], [ 0.5 / (label.textureWidth), 0.5 / (label.textureHeight)  ]);
252 		
253 		// Poi culling
254 		var worldPoi = label.pos3d;
255 		var poiVec = label.vertical;
256 		scale = label.textureHeight * ( pixelSizeVector[0] * worldPoi[0] + pixelSizeVector[1] * worldPoi[1] + pixelSizeVector[2] * worldPoi[2] + pixelSizeVector[3] );
257 
258 		var x = poiVec[0] * scale + worldPoi[0];
259 		var y = poiVec[1] * scale + worldPoi[1];
260 		var z = poiVec[2] * scale + worldPoi[2];
261 			
262 		gl.uniform3f(this.textProgram.uniforms["poiPosition"], x, y, z);
263 		gl.uniform1f(this.textProgram.uniforms["alpha"], 1.);
264 			
265 		this.textMesh.render(this.textProgram.attributes);
266 		label.needed = false;	
267 	}
268 	gl.enable(gl.DEPTH_TEST);
269 	gl.disable(gl.BLEND);
270 }
271 
272 /**************************************************************************************************************/
273 
274 /**
275  * 	Set visibility of the layer
276  */
277 EquatorialGridLayer.prototype.visible = function( arg )
278 {
279 	if ( typeof arg == "boolean" && this._visible != arg )
280 	{
281 		this._visible = arg;
282 		
283 		if ( arg )
284 		{
285 			this.globe.tileManager.addPostRenderer(this);
286 		}
287 		else
288 		{
289 			this.globe.tileManager.removePostRenderer(this);
290 		}
291 	}
292 	
293 	return this._visible;
294 }
295 
296 /**************************************************************************************************************/
297 
298 /**
299  * 	Set opacity of the layer
300  */
301 EquatorialGridLayer.prototype.opacity = function( arg )
302 {
303 	return BaseLayer.prototype.opacity.call( this, arg );
304 }
305 
306 /**************************************************************************************************************/
307 
308 /**
309  * 	Compute samples depending on geoBound
310  */
311 EquatorialGridLayer.prototype.computeSamples = function(geoBound)
312 {
313 	var dlong = geoBound.east - geoBound.west;
314 	var dlat = geoBound.north - geoBound.south;
315 	
316 	// if under-sampled and not divergent
317 	if ( dlong / this.longitudeSample < 3. && this.longitudeSample > 1. )
318 	{
319 		this.longitudeSample /= 2;
320 		this.latitudeSample /= 2;
321 	}
322 	
323 	// if over-sampled and not exceed the initial value
324 	if ( dlong / this.longitudeSample > 7. && this.longitudeSample < 15. )
325 	{
326 		this.longitudeSample *= 2;
327 		this.latitudeSample *= 2;
328 	}
329 }
330 
331 /**************************************************************************************************************/
332 
333 /**
334  * 	Generate buffers object of the grid
335  */
336 EquatorialGridLayer.prototype.generateGridBuffers = function(geoBound)
337 {
338 	// Clamp min/max longitudes to sample
339 	var west = (Math.floor(geoBound.west / this.longitudeSample))*this.longitudeSample;
340 	var east = (Math.ceil(geoBound.east / this.longitudeSample))*this.longitudeSample;
341 
342 	var phiStart = Math.min( west, east );
343 	var phiStop = Math.max( west, east );
344 	
345 	// Difference is larger than hemisphere
346 	if ( (east - west) > 180. )
347 	{
348 		// pole in the viewport
349 		phiStart = 0;
350 		phiStop = 360;
351 	}
352 	else
353 	{
354 		phiStart = west;
355 		phiStop = east;
356 	}
357 
358 
359 	// TODO adaptative generation of theta value
360 	// for (var theta = geoBound.south; theta <= geoBound.north; theta+=latStep) {
361 
362 	var vertexPositionData = [];
363 	var latitudeBands = 180. / this.latitudeSample;
364 
365 	for ( var latNumber = 0; latNumber <= latitudeBands; latNumber++ )
366 	{
367 		var theta = latNumber * Math.PI / latitudeBands;
368 		var sinTheta = Math.sin(theta);
369 		var cosTheta = Math.cos(theta);
370 		
371 		for ( var phi = phiStart; phi <= phiStop ; phi+=this.longitudeSample )
372 		{
373 			var radPhi = phi * Math.PI / 180;
374 			
375 			var sinPhi = Math.sin(radPhi);
376 			var cosPhi = Math.cos(radPhi);
377 			
378 			// z is the up vector
379 			var x = cosPhi * sinTheta;
380 			var y = sinPhi * sinTheta;
381 			var z = cosTheta;
382 
383 			vertexPositionData.push(x);
384 			vertexPositionData.push(y);
385 			vertexPositionData.push(z);
386 		}
387 	}
388 
389 	var gl = this.globe.renderContext.gl;
390 	gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer);
391 	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPositionData), gl.STATIC_DRAW);
392 	this.vertexBuffer.itemSize = 3;
393 	this.vertexBuffer.numItems = vertexPositionData.length/3;
394 
395 	
396 	var indexData = [];
397 	var longitudeBands = (phiStop - phiStart)/this.longitudeSample + 1;
398 
399 	for ( var latNumber = 0; latNumber < latitudeBands; latNumber++ )
400 	{
401 		for ( var phi = phiStart, longNumber = 0; phi < phiStop ; phi+=this.longitudeSample, longNumber++ )
402 		{
403 			var first = (latNumber * (longitudeBands)) + longNumber % (longitudeBands - 1);
404 			var second = first + longitudeBands;
405 			indexData.push(first);
406 			indexData.push(first + 1);
407 			
408 			indexData.push(first + 1);
409 			indexData.push(second + 1);
410 			
411 			indexData.push(second + 1);
412 			indexData.push(second);
413 			
414 			indexData.push(second);
415 			indexData.push(first);
416 		}
417 	}
418 
419 	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer);
420 	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indexData), gl.STATIC_DRAW);
421 	this.indexBuffer.itemSize = 1;
422 	this.indexBuffer.numItems = indexData.length;
423 }
424 
425 /**************************************************************************************************************/
426 
427 /**
428  * 	Generate text of the grid
429  */
430 EquatorialGridLayer.prototype.generateText = function(geoBound)
431 {
432 	// Clamp min/max longitudes to sample
433 	var west = (Math.floor(geoBound.west / this.longitudeSample))*this.longitudeSample;
434 	var east = (Math.ceil(geoBound.east / this.longitudeSample))*this.longitudeSample;
435 
436 	phiStart = Math.min( west, east );
437 	phiStop = Math.max( west, east );
438 	
439 	// Difference is larger than hemisphere
440 	if ( (east - west) > 180. )
441 	{
442 		// pole in the viewport => generate all longitude bands
443 // 		phiStart = east - 360;
444 // 		phiStop = west;
445 		phiStart = 0;
446 		phiStop = 360;
447 	}
448 	else
449 	{
450 		phiStart = west;
451 		phiStop = east;
452 	}
453 
454 	// Compute geographic position of center of canvas
455 	var posX3d = this.globe.renderContext.get3DFromPixel( this.globe.renderContext.canvas.width / 2. , this.globe.renderContext.canvas.height / 2. );
456 	var posXgeo = [];
457 	CoordinateSystem.from3DToGeo( posX3d, posXgeo );
458 
459 	for ( var phi = phiStart; phi <= phiStop; phi+=this.longitudeSample )
460 	{
461 		// convert to RA [0..360]
462 		var RA = (phi < 0) ? phi+360 : phi;
463 		var stringRA = CoordinateSystem.fromDegreesToHMS( RA );
464 
465 		if ( !this.labels[stringRA] )
466 		{
467 			this.labels[stringRA] = {};
468 			var imageData = this.generateImageData( stringRA );
469 			this._buildTextureFromImage(this.labels[stringRA],imageData);
470 		}
471 		
472 		// Compute position of label
473 		var posGeo = [ phi, posXgeo[1] ];
474 		var pos3d = CoordinateSystem.fromGeoTo3D( posGeo );
475 		var vertical = vec3.create();
476 		vec3.normalize(pos3d, vertical);
477 		
478 		this.labels[stringRA].pos3d = pos3d;
479 		this.labels[stringRA].vertical = vertical;
480 		this.labels[stringRA].needed = true;
481 	}
482 	
483 	// TODO <!> Adaptative rendering isn't totally implemented for theta due to difficulty to compute extrem latitude using geoBound <!>
484 	var north = (Math.ceil(geoBound.north / this.latitudeSample))*this.latitudeSample;
485 	var south = (Math.floor(geoBound.south / this.latitudeSample))*this.latitudeSample;
486 	
487 	thetaStart = Math.min( north, south );
488 	thetaStop = Math.max( north, south );
489 	
490 	for ( var theta = thetaStart; theta <= thetaStop; theta+=this.latitudeSample )
491 	{
492 // 	for (var theta = -90; theta < 90; theta+=this.latitudeSample) {
493 
494 		var stringTheta = CoordinateSystem.fromDegreesToDMS( theta );
495 		if ( !this.labels[stringTheta] )
496 		{
497 			this.labels[stringTheta] = {};
498 			var imageData = this.generateImageData( stringTheta );
499 			this._buildTextureFromImage(this.labels[stringTheta], imageData);
500 		}
501 		
502 		// Compute position of label
503 		var posGeo = [ posXgeo[0], theta ];
504 		var pos3d = CoordinateSystem.fromGeoTo3D( posGeo );
505 		var vertical = vec3.create();
506 		vec3.normalize(pos3d, vertical);
507 		
508 		this.labels[stringTheta].pos3d = pos3d;
509 		this.labels[stringTheta].vertical = vertical;
510 		this.labels[stringTheta].needed = true;
511 	}
512 
513 	// Dispose texture if not needed
514 	for ( var x in this.labels )
515 	{
516 		if( !this.labels[x].needed )
517 		{
518 			this.texturePool.disposeGLTexture(this.labels[x].texture);
519 			delete this.labels[x];
520 		}
521 	}
522 	
523 }
524 
525 /**************************************************************************************************************/
526 
527 /*
528 	Build a texture from an image and store in a renderable
529  */
530 EquatorialGridLayer.prototype._buildTextureFromImage = function(renderable,image)
531 {  	
532 	renderable.texture = this.texturePool.createGLTexture(image);
533 	renderable.textureWidth = image.width;
534 	renderable.textureHeight = image.height;
535 }
536 
537 /**************************************************************************************************************/
538 
539 /**
540  *	@constructor
541  *	GL Textures pool
542  */
543 var TexturePool = function(gl)
544 {
545 	var gl = gl;
546 	var glTextures = [];
547 
548 	/**
549 		Create a GL texture
550 	 */
551 	this.createGLTexture = function(image)
552 	{
553 		if ( glTextures.length > 0 )
554 		{
555 			return reuseGLTexture(image);
556 		}
557 		else
558 		{
559 			return createNewGLTexture(image);
560 		}
561 	};
562 
563 
564 	/**
565 	 	Dispose a GL texture
566 	 */
567 	this.disposeGLTexture = function( texture )
568 	{
569 		glTextures.push(texture);
570 	}
571 
572 	this.disposeAll = function()
573 	{
574 		for ( var i=0; i<glTextures.length; i++ )
575 		{
576 			gl.deleteTexture(glTextures[i]);
577 		}
578 		glTextures.length = 0;
579 	}
580 
581 	/** 
582 		Create a non power of two texture from an image
583 	*/
584 	var createNewGLTexture = function(image)
585 	{	
586 		var tex = gl.createTexture();
587 		gl.bindTexture(gl.TEXTURE_2D, tex);
588 		gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
589 		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
590 		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
591 		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
592 		gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
593 		return tex;
594 	}
595 
596 
597 	/**
598 		Reuse a GL texture
599 	 */
600 	var reuseGLTexture = function(image)
601 	{
602 		var glTexture = glTextures.pop();
603 		gl.bindTexture(gl.TEXTURE_2D, glTexture);
604 		gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);	
605 		return glTexture;
606 	}
607 }
608 
609 /**************************************************************************************************************/
610 
611 return EquatorialGridLayer;
612 
613 });