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 * Copyright 2011, 2012 GlobWeb contributors. 21 * 22 * This file is part of GlobWeb. 23 * 24 * GlobWeb is free software: you can redistribute it and/or modify 25 * it under the terms of the GNU Lesser General Public License as published by 26 * the Free Software Foundation, version 3 of the License, or 27 * (at your option) any later version. 28 * 29 * GlobWeb is distributed in the hope that it will be useful, 30 * but WITHOUT ANY WARRANTY; without even the implied warranty of 31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 32 * GNU Lesser General Public License for more details. 33 * 34 * You should have received a copy of the GNU General Public License 35 * along with GlobWeb. If not, see <http://www.gnu.org/licenses/>. 36 ***************************************/ 37 38 define(['./FeatureStyle','./VectorRendererManager','./Utils','./BaseLayer','./RendererTileData'], 39 function(FeatureStyle,VectorRendererManager,Utils,BaseLayer,RendererTileData) { 40 41 /**************************************************************************************************************/ 42 43 /** @constructor 44 * @class 45 * OpenSearch dynamic layer 46 * 47 * @param options Configuration options 48 * <ul> 49 <li>serviceUrl : Url of OpenSearch description XML file(necessary option)</li> 50 <li>minOrder : Starting order for OpenSearch requests</li> 51 <li>displayProperties : Properties which will be shown in priority</li> 52 </ul> 53 */ 54 var OpenSearchLayer = function(options){ 55 BaseLayer.prototype.constructor.call( this, options ); 56 57 this.serviceUrl = options.serviceUrl; 58 this.minOrder = options.minOrder || 5; 59 this.maxRequests = options.maxRequests || 2; 60 this.requestProperties = ""; 61 62 // Set style 63 if ( options && options['style'] ) 64 { 65 this.style = options['style']; 66 } 67 else 68 { 69 this.style = new FeatureStyle(); 70 } 71 72 this.extId = "os"; 73 74 // Used for picking management 75 this.features = []; 76 // Counter set, indicates how many times the feature has been requested 77 this.featuresSet = {}; 78 79 // Maximum two requests for now 80 this.freeRequests = []; 81 82 // Build the request objects 83 for ( var i =0; i < this.maxRequests; i++ ) 84 { 85 var xhr = new XMLHttpRequest(); 86 this.freeRequests.push( xhr ); 87 } 88 89 // For rendering 90 this.pointBucket = null; 91 this.polygonBucket = null; 92 this.polygonRenderer = null; 93 this.pointRenderer = null; 94 } 95 96 /**************************************************************************************************************/ 97 98 Utils.inherits( BaseLayer, OpenSearchLayer ); 99 100 /**************************************************************************************************************/ 101 102 /** 103 * Attach the layer to the globe 104 * 105 * @param g The globe 106 */ 107 OpenSearchLayer.prototype._attach = function( g ) 108 { 109 BaseLayer.prototype._attach.call( this, g ); 110 111 this.extId += this.id; 112 113 g.tileManager.addPostRenderer(this); 114 } 115 116 /**************************************************************************************************************/ 117 118 /** 119 Detach the layer from the globe 120 */ 121 OpenSearchLayer.prototype._detach = function() 122 { 123 this.globe.tileManager.removePostRenderer(this); 124 this.pointRenderer = null; 125 this.pointBucket = null; 126 127 this.polygonRenderer = null; 128 this.polygonBucket = null; 129 130 BaseLayer.prototype._detach.call(this); 131 } 132 133 /**************************************************************************************************************/ 134 135 /** 136 * Update children state as inherited from parent 137 */ 138 OpenSearchLayer.prototype.updateChildrenState = function(tile) 139 { 140 if ( tile.children ) 141 { 142 for (var i = 0; i < 4; i++) 143 { 144 if ( tile.children[i].extension[this.extId] ) 145 { 146 tile.children[i].extension[this.extId].state = OpenSearchLayer.TileState.INHERIT_PARENT; 147 tile.children[i].extension[this.extId].complete = true; 148 } 149 this.updateChildrenState(tile.children[i]); 150 } 151 } 152 } 153 154 /**************************************************************************************************************/ 155 156 /** 157 * Launch request to the OpenSearch service 158 */ 159 OpenSearchLayer.prototype.launchRequest = function(tile, url) 160 { 161 var tileData = tile.extension[this.extId]; 162 var index = null; 163 164 if ( this.freeRequests.length == 0 ) 165 { 166 return; 167 } 168 169 // Set that the tile is loading its data for OpenSearch 170 tileData.state = OpenSearchLayer.TileState.LOADING; 171 172 // Add request properties to length 173 if ( this.requestProperties != "" ) 174 { 175 url += '&' + this.requestProperties; 176 } 177 178 // Pusblish the start load event, only if there is no pending requests 179 if ( this.maxRequests == this.freeRequests.length ) 180 { 181 this.globe.publish("startLoad",this); 182 } 183 184 var xhr = this.freeRequests.pop(); 185 186 var self = this; 187 xhr.onreadystatechange = function(e) 188 { 189 if ( xhr.readyState == 4 ) 190 { 191 if ( xhr.status == 200 ) 192 { 193 var response = JSON.parse(xhr.response); 194 195 tileData.complete = (response.totalResults == response.features.length); 196 197 // Update children state 198 if ( tileData.complete ) 199 { 200 self.updateChildrenState(tile); 201 } 202 203 self.updateFeatures(response.features); 204 205 if ( response.features.length > 0 ) 206 { 207 for ( var i=0; i < response.features.length; i++ ) 208 { 209 self.addFeature( response.features[i], tile ); 210 } 211 } 212 else 213 { 214 // HACK to avoid multiple rendering of parent features 215 tile.extension.pointSprite = new RendererTileData(); 216 } 217 } 218 else if ( xhr.status >= 400 ) 219 { 220 console.error( xhr.responseText ); 221 } 222 223 tileData.state = OpenSearchLayer.TileState.LOADED; 224 self.freeRequests.push( xhr ); 225 226 // Publish the end load event, only if there is no pending requests 227 if ( self.maxRequests == self.freeRequests.length ) 228 { 229 self.globe.publish("endLoad",self); 230 } 231 } 232 }; 233 xhr.open("GET", url ); 234 xhr.send(); 235 } 236 237 /**************************************************************************************************************/ 238 239 /** 240 * Set new request properties 241 */ 242 OpenSearchLayer.prototype.setRequestProperties = function(properties) 243 { 244 // clean renderers 245 for ( var x in this.featuresSet ) 246 { 247 var featureData = this.featuresSet[x]; 248 for ( var i=0; i<featureData.tiles.length; i++ ) 249 { 250 var tile = featureData.tiles[i]; 251 var feature = this.features[featureData.index]; 252 this.removeFeatureFromRenderer( feature, tile ); 253 } 254 } 255 256 // Clean old results 257 var self = this; 258 this.globe.tileManager.visitTiles( function(tile) { 259 if( tile.extension[self.extId] ) 260 { 261 tile.extension[self.extId].dispose(); 262 tile.extension[self.extId].featureIds = []; // exclusive parameter to remove from layer 263 tile.extension[self.extId].state = OpenSearchLayer.TileState.NOT_LOADED; 264 tile.extension[self.extId].complete = false; 265 } 266 }); 267 this.featuresSet = {}; 268 this.features = []; 269 270 // Set request properties 271 this.requestProperties = ""; 272 for (var key in properties) 273 { 274 if ( this.requestProperties != "" ) 275 this.requestProperties += '&' 276 this.requestProperties += key+'='+properties[key]; 277 } 278 279 } 280 281 /**************************************************************************************************************/ 282 283 /** 284 * Add feature to the layer and to the tile extension 285 */ 286 OpenSearchLayer.prototype.addFeature = function( feature, tile ) 287 { 288 var tileData = tile.extension[this.extId]; 289 var featureData; 290 291 // Add feature if it doesn't exist 292 if ( !this.featuresSet.hasOwnProperty(feature.properties.identifier) ) 293 { 294 this.features.push( feature ); 295 featureData = { index: this.features.length-1, 296 tiles: [tile] 297 }; 298 this.featuresSet[feature.properties.identifier] = featureData; 299 } 300 else 301 { 302 featureData = this.featuresSet[feature.properties.identifier]; 303 304 // Always use the base feature to manage geometry indices 305 feature = this.features[ featureData.index ]; 306 307 // DEBUG : check tile is only present one time 308 var isTileExists = false; 309 for ( var i = 0; i < featureData.tiles.length; i++ ) 310 { 311 if ( tile.order == featureData.tiles[i].order 312 && tile.pixelIndex == featureData.tiles[i].pixelIndex ) 313 { 314 isTileExists = true; 315 console.log('OpenSearchLayer internal error : tile already there! ' + tile.order + ' ' + tile.pixelIndex ); 316 } 317 } 318 319 if (!isTileExists) 320 { 321 // Store the tile 322 featureData.tiles.push(tile); 323 } 324 } 325 326 // Add feature id 327 tileData.featureIds.push( feature.properties.identifier ); 328 329 // Set the identifier on the geometry 330 feature.geometry.gid = feature.properties.identifier; 331 332 // Add to renderer 333 this.addFeatureToRenderer(feature, tile); 334 } 335 336 /**************************************************************************************************************/ 337 338 /** 339 * Add feature to renderer 340 */ 341 OpenSearchLayer.prototype.addFeatureToRenderer = function( feature, tile ) 342 { 343 if ( feature.geometry['type'] == "Point" ) 344 { 345 if (!this.pointRenderer) 346 { 347 this.pointRenderer = this.globe.vectorRendererManager.getRenderer("PointSprite"); 348 this.pointBucket = this.pointRenderer.getOrCreateBucket( this, this.style ); 349 } 350 this.pointRenderer.addGeometryToTile( this.pointBucket, feature.geometry, tile ); 351 } 352 else if ( feature.geometry['type'] == "Polygon" ) 353 { 354 if (!this.polygonRenderer) 355 { 356 this.polygonRenderer = this.globe.vectorRendererManager.getRenderer("ConvexPolygon"); 357 this.polygonBucket = this.polygonRenderer.getOrCreateBucket( this, this.style ); 358 } 359 this.polygonRenderer.addGeometryToTile( this.polygonBucket, feature.geometry, tile ); 360 } 361 } 362 363 /**************************************************************************************************************/ 364 365 /** 366 * Remove feature from renderer 367 */ 368 OpenSearchLayer.prototype.removeFeatureFromRenderer = function( feature, tile ) 369 { 370 if ( feature.geometry['type'] == "Point" ) 371 { 372 this.pointRenderer.removeGeometryFromTile( feature.geometry, tile ); 373 } 374 else if ( feature.geometry['type'] == "Polygon" ) 375 { 376 this.polygonRenderer.removeGeometryFromTile( feature.geometry, tile ); 377 } 378 } 379 380 /**************************************************************************************************************/ 381 382 /** 383 * Remove feature from Dynamic OpenSearch layer 384 */ 385 OpenSearchLayer.prototype.removeFeature = function( identifier, tile ) 386 { 387 var featureIt = this.featuresSet[identifier]; 388 389 if (!featureIt) { 390 return; 391 } 392 393 // Remove tile from array 394 var tileIndex = featureIt.tiles.indexOf(tile); 395 if ( tileIndex >= 0 ) 396 { 397 featureIt.tiles.splice(tileIndex,1); 398 } 399 else 400 { 401 console.log('OpenSearchLayer internal error : tile not found when removing feature'); 402 } 403 404 if ( featureIt.tiles.length == 0 ) 405 { 406 // Remove it from the set 407 delete this.featuresSet[identifier]; 408 409 // Remove it from the array by swapping it with the last feature to optimize removal. 410 var lastFeature = this.features.pop(); 411 if ( featureIt.index < this.features.length ) 412 { 413 // Set the last feature at the position of the removed feature 414 this.features[ featureIt.index ] = lastFeature; 415 // Update its index in the Set. 416 this.featuresSet[ lastFeature.properties.identifier ].index = featureIt.index; 417 } 418 } 419 } 420 421 /**************************************************************************************************************/ 422 423 /** 424 * Modify feature style 425 */ 426 OpenSearchLayer.prototype.modifyFeatureStyle = function( feature, style ) 427 { 428 feature.properties.style = style; 429 var featureData = this.featuresSet[feature.properties.identifier]; 430 if ( featureData ) 431 { 432 var renderer; 433 if ( feature.geometry.type == "Point" ) { 434 renderer = this.pointRenderer; 435 } 436 else if ( feature.geometry.type == "Polygon" ) { 437 renderer = this.polygonRenderer; 438 } 439 440 var newBucket = renderer.getOrCreateBucket(this,style); 441 for ( var i = 0; i < featureData.tiles.length; i++ ) 442 { 443 renderer.removeGeometryFromTile(feature.geometry,featureData.tiles[i]); 444 renderer.addGeometryToTile(newBucket,feature.geometry,featureData.tiles[i]); 445 } 446 447 } 448 } 449 450 OpenSearchLayer.TileState = { 451 LOADING: 0, 452 LOADED: 1, 453 NOT_LOADED: 2, 454 INHERIT_PARENT: 3 455 }; 456 457 458 /**************************************************************************************************************/ 459 460 /** 461 * Generate the tile data 462 */ 463 OpenSearchLayer.prototype.generate = function(tile) 464 { 465 // Create data for the layer 466 // Check that it has not been created before (it can happen with level 0 tile) 467 var osData = tile.extension[this.extId]; 468 if ( !osData ) 469 { 470 if ( tile.parent ) 471 { 472 var parentOSData = tile.parent.extension[this.extId]; 473 osData = new OSData(this,tile); 474 osData.state = parentOSData.complete ? OpenSearchLayer.TileState.INHERIT_PARENT : OpenSearchLayer.TileState.NOT_LOADED; 475 osData.complete = parentOSData.complete; 476 } 477 else 478 { 479 osData = new OSData(this,tile); 480 } 481 482 // Store in on the tile 483 tile.extension[this.extId] = osData; 484 } 485 486 }; 487 488 /**************************************************************************************************************/ 489 490 491 /** 492 * OpenSearch renderable 493 */ 494 495 var OSData = function(layer,tile) 496 { 497 this.layer = layer; 498 this.tile = tile; 499 this.featureIds = []; // exclusive parameter to remove from layer 500 this.state = OpenSearchLayer.TileState.NOT_LOADED; 501 this.complete = false; 502 } 503 504 /**************************************************************************************************************/ 505 506 /** 507 * Dispose renderable data from tile 508 */ 509 OSData.prototype.dispose = function( renderContext, tilePool ) 510 { 511 for( var i = 0; i < this.featureIds.length; i++ ) 512 { 513 this.layer.removeFeature( this.featureIds[i], this.tile ); 514 } 515 this.tile = null; 516 } 517 518 /**************************************************************************************************************/ 519 520 /** 521 * Build request url 522 */ 523 OpenSearchLayer.prototype.buildUrl = function( tile ) 524 { 525 return url = this.serviceUrl + "/search?order=" + tile.order + "&healpix=" + tile.pixelIndex; 526 } 527 528 /**************************************************************************************************************/ 529 530 /** 531 Render function 532 533 @param tiles The array of tiles to render 534 */ 535 OpenSearchLayer.prototype.render = function( tiles ) 536 { 537 if (!this._visible) 538 return; 539 540 // Load data for the tiles if needed 541 for ( var i = 0; i < tiles.length && this.freeRequests.length > 0; i++ ) 542 { 543 var tile = tiles[i]; 544 if ( tile.order >= this.minOrder ) 545 { 546 var osData = tile.extension[this.extId]; 547 if ( !osData || osData.state == OpenSearchLayer.TileState.NOT_LOADED ) 548 { 549 // Check if the parent is loaded or not, in that case load the parent first 550 while ( tile.parent 551 && tile.parent.order >= this.minOrder 552 && tile.parent.extension[this.extId] 553 && tile.parent.extension[this.extId].state == OpenSearchLayer.TileState.NOT_LOADED ) 554 { 555 tile = tile.parent; 556 } 557 558 if ( tile.extension[this.extId] && tile.extension[this.extId].state == OpenSearchLayer.TileState.NOT_LOADED ) 559 { 560 // Skip loading parent 561 if ( tile.parent && tile.parent.extension[this.extId].state == OpenSearchLayer.TileState.LOADING ) 562 continue; 563 564 var url = this.buildUrl(tile); 565 if ( url ) 566 { 567 this.launchRequest(tile, url); 568 } 569 } 570 } 571 } 572 } 573 } 574 575 /**************************************************************************************************************/ 576 577 /** 578 * Update features 579 */ 580 OpenSearchLayer.prototype.updateFeatures = function( features ) 581 { 582 for ( var i=0; i<features.length; i++ ) 583 { 584 var currentFeature = features[i]; 585 586 switch ( currentFeature.geometry.type ) 587 { 588 case "Point": 589 if ( currentFeature.geometry.coordinates[0] > 180 ) 590 currentFeature.geometry.coordinates[0] -= 360; 591 break; 592 case "Polygon": 593 var ring = currentFeature.geometry.coordinates[0]; 594 for ( var j = 0; j < ring.length; j++ ) 595 { 596 if ( ring[j][0] > 180 ) 597 ring[j][0] -= 360; 598 } 599 break; 600 default: 601 break; 602 } 603 } 604 } 605 606 /*************************************************************************************************************/ 607 608 return OpenSearchLayer; 609 610 }); 611