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