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','./VectorRendererManager','./FeatureStyle','./Program'], 21 function(CoordinateSystem,VectorRendererManager,FeatureStyle,Program) { 22 23 /**************************************************************************************************************/ 24 25 /** @constructor 26 Basic module to generate texture from text 27 */ 28 var Text = (function() 29 { 30 var fontSize = 18; 31 var margin = 1; 32 var canvas2d = null; 33 34 var initialize = function() 35 { 36 canvas2d = document.createElement("canvas"); 37 canvas2d.width = 512; 38 canvas2d.height = fontSize + 2 * margin; 39 } 40 41 var generateImageData = function(text, textColor) 42 { 43 if (!canvas2d) 44 initialize(); 45 46 var fillColor = textColor; 47 if (!fillColor) 48 fillColor = '#fff'; 49 else if ( fillColor instanceof Array ) 50 fillColor = FeatureStyle.fromColorToString(textColor); 51 52 var ctx = canvas2d.getContext("2d"); 53 ctx.clearRect(0,0,canvas2d.width,canvas2d.height); 54 ctx.fillStyle = fillColor; 55 ctx.font = fontSize + 'px sans-serif'; 56 ctx.textBaseline = 'top'; 57 ctx.shadowColor = '#000'; 58 ctx.shadowOffsetX = 1; 59 ctx.shadowOffsetY = 1; 60 ctx.shadowBlur = 2; 61 ctx.fillText(text, margin, margin); 62 //ctx.lineWidth = 1.0; 63 //ctx.strokeText(text, margin, margin); 64 65 var metrics = ctx.measureText(text); 66 return ctx.getImageData(0,0, Math.floor(metrics.width)+2*margin,canvas2d.height) 67 } 68 69 70 return { generateImageData: generateImageData }; 71 })(); 72 73 74 /**************************************************************************************************************/ 75 76 /** @constructor 77 POI Renderer constructor 78 */ 79 var PointRenderer = function(tileManager) 80 { 81 // Store object for rendering 82 this.renderContext = tileManager.renderContext; 83 this.tileConfig = tileManager.tileConfig; 84 85 // Bucket management for rendering : a bucket is a texture with its points 86 this.buckets = []; 87 88 // For stats 89 this.numberOfRenderPoints = 0; 90 91 var vertexShader = "\ 92 attribute vec3 vertex; // vertex have z = 0, spans in x,y from -0.5 to 0.5 \n\ 93 uniform mat4 viewProjectionMatrix; \n\ 94 uniform vec3 poiPosition; // world position \n\ 95 uniform vec2 poiScale; // x,y scale \n\ 96 uniform vec2 tst; \n\ 97 \n\ 98 varying vec2 texCoord; \n\ 99 \n\ 100 void main(void) \n\ 101 { \n\ 102 // Generate texture coordinates, input vertex goes from -0.5 to 0.5 (on x,y) \n\ 103 texCoord = vertex.xy + vec2(0.5) + tst; \n\ 104 // Invert y \n\ 105 texCoord.y = 1.0 - texCoord.y; \n\ 106 \n\ 107 // Compute poi position in clip coordinate \n\ 108 gl_Position = viewProjectionMatrix * vec4(poiPosition, 1.0); \n\ 109 gl_Position.xy += vertex.xy * gl_Position.w * poiScale; \n\ 110 } \n\ 111 "; 112 113 var fragmentShader = "\ 114 precision lowp float; \n\ 115 varying vec2 texCoord; \n\ 116 uniform sampler2D texture; \n\ 117 uniform float alpha; \n\ 118 uniform vec3 color; \n\ 119 \n\ 120 void main(void) \n\ 121 { \n\ 122 vec4 textureColor = texture2D(texture, texCoord); \n\ 123 gl_FragColor = vec4(textureColor.rgb * color, textureColor.a * alpha); \n\ 124 if (gl_FragColor.a <= 0.0) discard; \n\ 125 } \n\ 126 "; 127 128 this.program = new Program(this.renderContext); 129 this.program.createFromSource(vertexShader, fragmentShader); 130 131 var vertices = new Float32Array([-0.5, -0.5, 0.0, 132 -0.5, 0.5, 0.0, 133 0.5, 0.5, 0.0, 134 0.5, -0.5, 0.0]); 135 136 var gl = this.renderContext.gl; 137 this.vertexBuffer = gl.createBuffer(); 138 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 139 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 140 141 this.defaultTexture = null; 142 } 143 144 /**************************************************************************************************************/ 145 146 /* 147 Build a default texture 148 */ 149 PointRenderer.prototype._buildDefaultTexture = function(bucket) 150 { 151 if ( !this.defaultTexture ) 152 { 153 var gl = this.renderContext.gl; 154 this.defaultTexture = gl.createTexture(); 155 gl.bindTexture(gl.TEXTURE_2D, this.defaultTexture); 156 var whitePixel = new Uint8Array([255, 255, 255, 255]); 157 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, whitePixel); 158 } 159 160 bucket.texture = this.defaultTexture; 161 bucket.textureWidth = 10; 162 bucket.textureHeight = 10; 163 } 164 165 /**************************************************************************************************************/ 166 167 /* 168 Build a texture from an image and store in a bucket 169 */ 170 PointRenderer.prototype._buildTextureFromImage = function(bucket,image) 171 { 172 bucket.texture = this.renderContext.createNonPowerOfTwoTextureFromImage(image); 173 bucket.textureWidth = image.width; 174 bucket.textureHeight = image.height; 175 } 176 177 /**************************************************************************************************************/ 178 179 /* 180 Add a point to the renderer 181 */ 182 PointRenderer.prototype.addGeometry = function(geometry,layer,style) 183 { 184 if ( style ) 185 { 186 var bucket = this.getOrCreateBucket( layer,style ); 187 188 var posGeo = geometry['coordinates']; 189 var pos3d = CoordinateSystem.fromGeoTo3D( posGeo ); 190 var vertical = vec3.create(); 191 vec3.normalize(pos3d, vertical); 192 193 var pointRenderData = { pos3d: pos3d, 194 vertical: vertical, 195 geometry: geometry, 196 color: style.fillColor }; 197 198 bucket.points.push( pointRenderData ); 199 } 200 } 201 202 /**************************************************************************************************************/ 203 204 /* 205 Remove a point from renderer 206 */ 207 PointRenderer.prototype.removeGeometry = function(geometry,layer) 208 { 209 for ( var i = 0; i < this.buckets.length; i++ ) 210 { 211 var bucket = this.buckets[i]; 212 if ( bucket.layer == layer ) 213 { 214 for ( var j = 0; j < bucket.points.length; j++ ) 215 { 216 if ( bucket.points[j].geometry == geometry ) 217 { 218 bucket.points.splice( j, 1 ); 219 220 if ( bucket.points.length == 0 ) 221 { 222 this.buckets.splice( i, 1 ); 223 } 224 return; 225 } 226 } 227 } 228 } 229 } 230 231 /**************************************************************************************************************/ 232 233 /* 234 Get or create bucket to render a point 235 */ 236 PointRenderer.prototype.getOrCreateBucket = function(layer,style) 237 { 238 // Find an existing bucket for the given style, except if label is set, always create a new one 239 for ( var i = 0; i < this.buckets.length; i++ ) 240 { 241 var bucket = this.buckets[i]; 242 if ( bucket.layer == layer && bucket.style.isEqualForPoint(style) ) 243 { 244 return bucket; 245 } 246 } 247 248 249 // Create a bucket 250 var bucket = { 251 texture: null, 252 points: [], 253 style: style, 254 layer: layer 255 }; 256 257 // Initialize bucket : create the texture 258 if ( style['label'] ) 259 { 260 var imageData = Text.generateImageData(style['label'], style['textColor']); 261 this._buildTextureFromImage(bucket,imageData); 262 } 263 else if ( style['iconUrl'] ) 264 { 265 var image = new Image(); 266 var self = this; 267 image.onload = function() {self._buildTextureFromImage(bucket,image); self.renderContext.requestFrame(); } 268 image.onerror = function() { self._buildDefaultTexture(bucket); } 269 image.src = style.iconUrl; 270 } 271 else if ( style['icon'] ) 272 { 273 this._buildTextureFromImage(bucket,style.icon); 274 } 275 else 276 { 277 this._buildDefaultTexture(bucket); 278 } 279 280 this.buckets.push( bucket ); 281 282 return bucket; 283 } 284 285 /**************************************************************************************************************/ 286 287 /* 288 Render all the POIs 289 */ 290 PointRenderer.prototype.render = function() 291 { 292 if (this.buckets.length == 0) 293 { 294 return; 295 } 296 297 this.numberOfRenderPoints = 0; 298 299 var renderContext = this.renderContext; 300 var gl = this.renderContext.gl; 301 302 // Setup states 303 // gl.disable(gl.DEPTH_TEST); 304 gl.enable(gl.BLEND); 305 gl.blendEquation(gl.FUNC_ADD); 306 gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 307 308 // Setup program 309 this.program.apply(); 310 311 // The shader only needs the viewProjection matrix, use modelViewMatrix as a temporary storage 312 mat4.multiply(renderContext.projectionMatrix, renderContext.viewMatrix, renderContext.modelViewMatrix) 313 gl.uniformMatrix4fv(this.program.uniforms["viewProjectionMatrix"], false, renderContext.modelViewMatrix); 314 gl.uniform1i(this.program.uniforms["texture"], 0); 315 316 // Compute eye direction from inverse view matrix 317 mat4.inverse(renderContext.viewMatrix, renderContext.modelViewMatrix); 318 var camZ = [renderContext.modelViewMatrix[8], renderContext.modelViewMatrix[9], renderContext.modelViewMatrix[10]]; 319 vec3.normalize(camZ); 320 vec3.scale(camZ, this.tileConfig.cullSign, camZ); 321 322 // Compute pixel size vector to offset the points from the earth 323 var pixelSizeVector = renderContext.computePixelSizeVector(); 324 325 // Warning : use quoted strings to access properties of the attributes, to work correclty in advanced mode with closure compiler 326 gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 327 gl.vertexAttribPointer(this.program.attributes['vertex'], 3, gl.FLOAT, false, 0, 0); 328 329 for ( var n = 0; n < this.buckets.length; n++ ) 330 { 331 var bucket = this.buckets[n]; 332 333 if ( bucket.texture == null || bucket.points.length == 0 334 || !bucket.layer._visible || bucket.layer._opactiy <= 0.0 ) 335 continue; 336 337 // Bind point texture 338 gl.activeTexture(gl.TEXTURE0); 339 gl.bindTexture(gl.TEXTURE_2D, bucket.texture); 340 341 // 2.0 * because normalized device coordinates goes from -1 to 1 342 var scale = [2.0 * bucket.textureWidth / renderContext.canvas.width, 343 2.0 * bucket.textureHeight / renderContext.canvas.height]; 344 gl.uniform2fv(this.program.uniforms["poiScale"], scale); 345 gl.uniform2fv(this.program.uniforms["tst"], [ 0.5 / (bucket.textureWidth), 0.5 / (bucket.textureHeight) ]); 346 347 for (var i = 0; i < bucket.points.length; ++i) 348 { 349 // Poi culling 350 var worldPoi = bucket.points[i].pos3d; 351 var poiVec = bucket.points[i].vertical; 352 var scale = bucket.textureHeight * ( pixelSizeVector[0] * worldPoi[0] + pixelSizeVector[1] * worldPoi[1] + pixelSizeVector[2] * worldPoi[2] + pixelSizeVector[3] ); 353 scale *= this.tileConfig.cullSign; 354 var scaleInKm = (scale / CoordinateSystem.heightScale) * 0.001; 355 if ( scaleInKm > bucket.style.pointMaxSize ) 356 continue; 357 358 if ( vec3.dot(poiVec, camZ) > 0 359 && renderContext.worldFrustum.containsSphere(worldPoi,scale) >= 0 ) 360 { 361 var x = poiVec[0] * scale + worldPoi[0]; 362 var y = poiVec[1] * scale + worldPoi[1]; 363 var z = poiVec[2] * scale + worldPoi[2]; 364 365 gl.uniform3f(this.program.uniforms["poiPosition"], x, y, z); 366 gl.uniform1f(this.program.uniforms["alpha"], bucket.layer._opacity); 367 gl.uniform3f(this.program.uniforms["color"], bucket.points[i].color[0], bucket.points[i].color[1], bucket.points[i].color[2] ); 368 369 gl.drawArrays(gl.TRIANGLE_FAN, 0, 4); 370 371 this.numberOfRenderPoints++; 372 } 373 } 374 } 375 376 // gl.enable(gl.DEPTH_TEST); 377 gl.disable(gl.BLEND); 378 } 379 380 /**************************************************************************************************************/ 381 382 // Register the renderer 383 VectorRendererManager.registerRenderer({ 384 creator: function(globe) { return new PointRenderer(globe.tileManager); }, 385 canApply: function(type,style) {return type == "Point"; } 386 }); 387 388 return PointRenderer; 389 390 }); 391