API Docs for: 1.0.2
Show:

File: GIScene\Layer\Grid.js

/** GIScene.Layer.Grid() is a layer type that loads Tiles from a url based on a Grid. 
 * 
 * @namespace GIScene
 * @class Layer.Grid
 * @constructor
 * @extends GIScene.Layer
 * @param {String} name the layer name for display purposes
 * @param {Object} [config] the layer configuration object
 */ 

GIScene.Layer.Grid = function(name, config){
	
	//make this a Layer
	GIScene.Layer.apply(this,[name, config]);
	
	var defaults = {
		//grid : new GIScene.Grid({origin: new GIScene.Coordinate2(0,0),tileSize: 1024}), //origin, tilesize
		origin: new GIScene.Coordinate2(0,0),
		tileSizes: [1024], 
		
		terrainHeight:71,
		maxNumTiles:25,
		maxDistance:10000,
		computeTileIndicesHandler:'default', //function to determine the tiles to be loaded, can differ from default for analysis reasons
		service:null,
		maxExtent:null, //
		lodDistanceFactor: 1,
		overrideMaterialHandler : null, //will be called for each object after assigning the overrideMaterial with an event.content object as parameter: {object:{Object3D}, material:{THREE.Material}, layer: GIScene.Layer}
		//@TODO sharedOverrideMaterial if only attached to single objects like in wmsoverlay textures and materials could be disposed when deleting tiles, if one mateial is schared by all tiles then material must be kept when deleting tiles
		// hasSharedOverrideMaterial : true, //set to false to make the tilecache delete materials and textures from tiles when deleted
		virtualSelectionAccessor : null 
	};
	
	this.config = GIScene.Utils.mergeObjects(defaults, this.config);
	
	this.url = null;
	this.format = null;
	this.boundingBox = null; //?? will change frequently
	this.verticalAxis = null;
	
	this.origin			= this.config.origin;
	this.tileSizes		= this.config.tileSizes.sort(function(a,b){return b-a;}); //sort descending
	this.grid 			= null;// will be set onSetScene //her could be gridset instead of grid  // this.config.grid;
	this.terrainHeight 	= this.config.terrainHeight;
	var _terrainHeight	= null; //will initialized in init() and adapted to the scene offset onSetScene
	this.maxNumTiles 	= this.config.maxNumTiles; 
	this.maxDistance	= this.config.maxDistance;
	this.maxExtent		= this.config.maxExtent;
	var _maxExtent		= null; //will initialized in init() and adapted to the scene offset onSetScene
	this.lodDistanceFactor = this.config.lodDistanceFactor;
	this.distanceReferencePoint = null; // will be calulated in getNewTiles
	this.computeTileIndicesHandler = null; // set in init()
	this.overrideMaterialHandler = null;
	this.virtualSelection = [];
	this.virtualSelectionAccessor = this.config.virtualSelectionAccessor;
	// this.hasSharedOverrideMaterial = this.config.hasSharedOverrideMaterial;
	
	var doAutoUpdate	= false;
	var autoUpdateIntervalId;
	
	//tileHandling
	this.load = []; //tiles to load
	this.remove = []; //tiles to remove
	
	this.loading = new GIScene.Grid.TileStore(); //holds the xhr request while loading //new GIScene.Grid.IndexStore();
	this.loaded  = new GIScene.Grid.TileStore(); //tiles already loaded
	this.cache	 = new GIScene.Grid.TileStore({
												maxLength:200
												// deleteMaterials: !this.hasSharedOverrideMaterial ,
												// deleteTextures:	 !this.hasSharedOverrideMaterial 
												});
	 	
	this.init = function(){
		this.name = name;
	  	this.url = this.config.url;
	  	this.format = this.config.format;
	  	this.verticalAxis = this.config.verticalAxis || "Y"; 
	  	this.boundingBox = new THREE.Box3();
	  	
	  	this.setTerrainHeight(this.terrainHeight);
	  	this.setMaxExtent(this.maxExtent);
	  	
	  	this.setComputeTileIndicesHandler(this.config.computeTileIndicesHandler);
	  	
	};
	
	var autoUpdate = function() { 
		if(doAutoUpdate){
			doAutoUpdate=false;
			this.update();
			doAutoUpdate=true;
		}
	}.bind(this);
	
	/**
	 * Starts the automatic loading and removing of tiles dependant on the camera perspective
	 * 
	 * @method startUpdate 
	 */
	this.startUpdate = function() { doAutoUpdate = true; autoUpdateIntervalId = window.setInterval(autoUpdate,500);console.log("Layer.Grid():startUpdate()");};
	
	/**
	 * Stops the automatic loading and removing of tiles dependent on the camera perspective
	 * 
	 * @method stopUpdate 
	 */
	this.stopUpdate = function() { window.clearInterval(autoUpdateIntervalId); doAutoUpdate = false;};
	
	this.setComputeTileIndicesHandler = function(handler) {
		this.computeTileIndicesHandler = ( handler == 'default')? this.getNewTilesFromQuadtree : handler;
	};
	
	var errorMaterial = new THREE.MeshBasicMaterial({color:0x55FF55, opacity:0.5, wireframe:false});
	this.getErrorTile = function(gridIndex) {
		
		// var material = new THREE.MeshBasicMaterial({color:0x55FF55, opacity:0.5, wireframe:false});
		var errorTile = new THREE.Mesh(new THREE.CubeGeometry(gridIndex.tileSize, 4, gridIndex.tileSize), errorMaterial);	
		errorTile.name = "errorTile_"+gridIndex.toString();
		//var centroid = GIScene.Grid.prototype.getCentroidFromIndex(gridIndex);//this.grid.getCentroidFromIndex(gridIndex);
		var centroid = this.grid.getCentroidFromIndex(gridIndex);//this.grid.getCentroidFromIndex(gridIndex);
		errorTile.position.set(centroid.x, (this.terrainHeight - this.offset.z), centroid.y);
		return errorTile;
	};
	
	/**
	 * Loads a tile by specifiying a GIScene.Grid.Index
	 * 
	 * @method loadTile
	 * @param {GIScene.Grid.Index} gridIndex
	 */	
	this.loadTile = function(gridIndex, generalize, specialize) {  //GIScene.Grid.Index
		if(!gridIndex)return;
		// console.log("loadTile",gridIndex);
		//get from cache
		var tileFromCache = this.cache.getTile(gridIndex); //object3d or false
		
		if(tileFromCache){
			
			//check material 
			
			var material = null;
			tileFromCache.traverse(function(object){
				if(!material){
					if(object.material){material = object.material;}
				}
			});
			
			var hasUnsharedWmsTextureLoaded = false;
			if(material && material.waitForTexture){
				hasUnsharedWmsTextureLoaded = material.unsharedWmsTextureLoaded;
			}
			// console.log("hasUnsharedWmsTextureLoaded", hasUnsharedWmsTextureLoaded)
			//if part of specialize keep in cache until all are loaded
			
			
			
			
			// var descendants = specialize[gridIndex.toString()];
			// var isPartOfSpecialize = false;
			// if(descendants){
				// isPartOfSpecialize = descendants.some(function(e,i,a){return gridIndex.equals(e);});
				// alert("Hallo");
				// console.log("isPartOfSpecialize", isPartOfSpecialize);
				// var allLoaded = descendants.every(function(e,i,a){
									// return this.cache.getTile(e) !== false;		//später in cache schauen statt loaded
								// });
				// if(allLoaded){
						// this.removeTile(gridIndex);
						// isPartOfSpecialize = false;
					// }
			// }
			//else add immediately
			// if(! isPartOfSpecialize){
			if(hasUnsharedWmsTextureLoaded || !material || !material.waitForTexture){
			// this.loading.remove(gridIndex);//mca2
			this.root.add(tileFromCache);
			
			this.cache.remove(gridIndex);
			this.loaded.add(gridIndex, tileFromCache);
			
			//update selectables
			if(this.selectControl){
				this.selectControl.selectables = this.root.getDescendants();
			}
			
			//ontileadd event
			/**
			 *Fires after a tile is added to the scene. A reference to the tile can be found at event.content.tile 
			 *
			 *@event tileadd
			 *  
			 */
			this.dispatchEvent({type:'tileadd', content:{tile:tileFromCache}});
			
			//add to oldTiles because all have been removed before (loaded and loading). Keep oldTiles updated.
			this.oldTiles.push(gridIndex);
			}
			//remove Tiles from generalize
			//test this.removeTiles(generalize[gridIndex.toString()]);
			
			
			
			//remove Tile from spezialize if all are loaded
			// var descendants = specialize[gridIndex.toString()];
			// if(descendants){
				// var allLoaded = descendants.every(function(e,i,a){
									// return this.loaded.getTile(e) !== false;		//später in cache schauen statt loaded
								// });
				// if(allLoaded){this.removeTile(gridIndex);}
			// }
			
		}else{
			//load tile and push to cache
		
			var onSuccess = function(result) {
				
				//handle errors, when server gives a 200 result but not a model,e.g. hmtl error website
				var errorTile = false;
				if(!(result instanceof THREE.Object3D)){
					result = this.getErrorTile(gridIndex);
				    errorTile = true;
				}
				
				//rotate model if z is up
				if(this.verticalAxis.toUpperCase() == "Z" && !errorTile){
					result.applyMatrix( new THREE.Matrix4().makeRotationX( -Math.PI/2 ) );
				}
				
				
				this.loading.remove(gridIndex); //mca2
			
			//mca	this.loaded.add(gridIndex,result);
				this.cache.add(gridIndex,result);
				
				//setOverrideMaterial
				if(!errorTile)this.setOverrideMaterial(result, this.config.overrideMaterial);
				
				
				//onload event
				/**
				 *Fires after a tile is loaded. A reference to the tile can be found at event.content.tile 
				 *
				 *@event tileload
				 *  
				 */
				this.dispatchEvent({type:'tileload', content:{tile:result}});
				
				//add to layer when texture is loaded
				// var material = null;
				// result.traverse(function(object){
					// if(!material){
						// if(object.material){material = object.material;}
					// }
				// });
// 				
				// if(material){
					// var onSetTexture = function(event){
						// this.loadTile(gridIndex);
						// material.removeEventListener("settexture", onSetTexture);
					// }.bind(this);
// 					
					// material.addEventListener("settexture", onSetTexture);
				// }
				
				//add to layer
				// mca this.root.add(result);
				//remove Tiles from generalize
				// console.log("removeTiles", gridIndex.toString(), generalize);
				// this.removeTiles(generalize[gridIndex.toString()]);
				
			}.bind(this);
			
			var onError = function(e) {
				
				var result = this.getErrorTile(gridIndex);
				
				this.loading.remove(gridIndex); 
			
				this.cache.add(gridIndex,result);
			}.bind(this);
			
			if(this.loading.getTile(gridIndex) === false){
			
				var requestUrl = this.config.service.getGetSceneUrl(gridIndex, this.grid);
				
				var loader = new GIScene.ModelLoader(); //need a loader for every parallel request
				loader.load(requestUrl, this.format, onSuccess, null, onError);
				
				this.loading.add(gridIndex, loader);
				//?????mca3
				// this.oldTiles.push(gridIndex);
			}
			
			
		// var object = this.getErrorTile(gridIndex);
		// this.loaded[gridIndex.toString()] = object; //@TODO remove reference before layer is disposed
		// this.root.add(object);
		}
	};
	
	/**
	 * loads several tiles by specifying an array of GIScene.Grid.Index objects
	 * @method loadTiles
	 * @param {Array of GIScene.Grid.Index} tileArray 
	 */
	this.loadTiles = function(tileArray, generalize, specialize) { 
		if(!tileArray)return;
		for(var i=0,j=tileArray.length; i<j; i++){
		  this.loadTile(tileArray[i], generalize, specialize);
		};	
	};
	
	/**
	 * Removes a tile or aborts its loading by specifiying a GIScene.Grid.Index
	 * 
	 * @method removeTile
	 * @param {GIScene.Grid.Index} gridIndex
	 */	
	this.removeTile = function(gridIndex, generalize){
		if(!gridIndex)return;
		// console.log("removeTile", gridIndex);
		
		// abort while still loading but not further needed
		var xhrIsLoading = this.loading.getTile(gridIndex);
		if(xhrIsLoading) {
			/*console.log(xhrIsLoading);*/
			xhrIsLoading.abort(); 
			this.loading.remove(gridIndex); 
			// if(generalize && gridIndex.toString() in generalize){console.log("removeTile:abortGeneralize");this.oldTiles.push.apply(this.oldTiles,generalize[gridIndex.toString()])};
			return;
		}
				
		var object = this.loaded.getTile(gridIndex);
		
		if(object instanceof THREE.Object3D){
			this.root.remove(object);
			this.loaded.remove(gridIndex);
			this.cache.add(gridIndex, object);
			//ontileremove event
			/**
			 *Fires after a tile is removed from the scene. A reference to the tile can be found at event.content.tile 
			 *
			 *@event tileremove
			 *  
			 */
			this.dispatchEvent({type:'tileremove', content:{tile:object}});
		}
		
	};
	
	/**
	 * removes or aborts the loading of several tiles by specifying an array of GIScene.Grid.Index objects
	 * @method removeTiles
	 * @param {Array of GIScene.Grid.Index} tileArray 
	 */
	this.removeTiles = function(tileArray, generalize) {
		if(!tileArray)return;
		for(var i=0,j=tileArray.length; i<j; i++){
		  this.removeTile(tileArray[i], generalize);
		};
	};
	
	/**
	 * computes the visible tiles of the current camera view 
	 * 
	 * @method getNewTilesFromQuadtree
	 * @return {Array of GIScene.Grid.Index} newTiles
	 */
	this.getNewTilesFromQuadtree = function() {
		// console.log("Layer.Grid():getNewTiles()");
		// console.log(this);
		
		/*
		 *returns Array of THREE.Vector2() while first and last index values (points) are identical  
		 */
		var intersectFrustrumAtTerrainHeight = function(terrainHeight){		//getCuttingPolygon
			// console.log("Layer.Grid():getNewTiles():intersectFrustrumAtTerrainHeight()");
			// console.log(this);
				//topology of frustum  hierarchy: points (n|f)_(t|b)_(l|r)
				var points = {
					//near
					"n_t_l" : new THREE.Vector3(-1, 1,-1),
					"n_t_r" : new THREE.Vector3( 1, 1,-1),
					"n_b_r" : new THREE.Vector3( 1,-1,-1),
					"n_b_l" : new THREE.Vector3(-1,-1,-1),
					//far
					"f_t_r" : new THREE.Vector3( 1, 1, 1),
					"f_t_l" : new THREE.Vector3(-1, 1, 1),
					"f_b_l" : new THREE.Vector3(-1,-1, 1),
					"f_b_r" : new THREE.Vector3( 1,-1, 1)
				};
				
				var lines = {
					//near counterclockwise
					"n_t":{	geometry: new THREE.Line3(points.n_t_r, points.n_t_l) },
					"n_r":{ geometry: new THREE.Line3(points.n_b_r, points.n_t_r) },
					"n_b":{ geometry: new THREE.Line3(points.n_b_l, points.n_b_r) },
					"n_l":{ geometry: new THREE.Line3(points.n_t_l, points.n_b_l) },
					
					//far ccw
					"f_t":{ geometry: new THREE.Line3(points.f_t_l, points.f_t_r) },
					"f_r":{ geometry: new THREE.Line3(points.f_t_r, points.f_b_r) },
					"f_b":{ geometry: new THREE.Line3(points.f_b_r, points.f_b_l) },
					"f_l":{ geometry: new THREE.Line3(points.f_b_l, points.f_t_l) },
					
					//sides from near to far
					"t_l":{ geometry: new THREE.Line3(points.n_t_l, points.f_t_l) },
					"b_l":{ geometry: new THREE.Line3(points.n_b_l, points.f_b_l) },
					"b_r":{ geometry: new THREE.Line3(points.n_b_r, points.f_b_r) },
					"t_r":{ geometry: new THREE.Line3(points.n_t_r, points.f_t_r) }
				};
				 
				var cells = {
					"near"	:[lines.n_l, lines.n_t, lines.n_r, lines.n_b],
					"far" 	:[lines.f_r, lines.f_b, lines.f_l, lines.f_t],
					"left"	:[lines.n_l, lines.t_l, lines.f_l, lines.b_l],
					"top"	:[lines.n_t, lines.t_r, lines.f_t, lines.t_l],
					"right"	:[lines.n_r, lines.b_r, lines.f_r, lines.t_r],
					"bottom":[lines.n_b, lines.b_l, lines.f_b, lines.b_r]
				};
				
				lines.n_t.left = cells.near;
				lines.n_t.right= cells.top;
				
				lines.n_r.left = cells.near;
				lines.n_r.right= cells.right;
				
				lines.n_b.left = cells.near;
				lines.n_b.right= cells.bottom;
				
				lines.n_l.left = cells.near;
				lines.n_l.right= cells.left;
				
				lines.f_t.left = cells.far;
				lines.f_t.right= cells.top;
				
				lines.f_r.left = cells.far;
				lines.f_r.right= cells.right;
				
				lines.f_b.left = cells.far;
				lines.f_b.right= cells.bottom;
				
				lines.f_l.left = cells.far;
				lines.f_l.right= cells.left;
				
				lines.t_l.left = cells.left;
				lines.t_l.right= cells.top;
				
				lines.b_l.left = cells.bottom;
				lines.b_l.right= cells.left;
				
				lines.b_r.left = cells.right;
				lines.b_r.right= cells.bottom;
				
				lines.t_r.left = cells.top;
				lines.t_r.right= cells.right;
				
				var projector = new THREE.Projector();
				
				//project points to world Coordinates
				for (point in points){
					projector.unprojectVector(points[point], this.scene.camera);
					//console.log(point+ ": " + points[point].toArray());
				}
				
				// var terrainHeight = 0; //has to be set dynamically
				var plane = new THREE.Plane().setFromNormalAndCoplanarPoint(new THREE.Vector3(0,1,0), new THREE.Vector3(0,terrainHeight,0));
				
				var  polygonPoints=[],firstCell, firstLine;
				//iterate through lines
				
				//find first cell with cutted lines	
				var setFirstCellAndPoint = function() {
					for (cell in cells) {
						//console.log('Cell: '+ cell);
						//find first point
						for (var i=0;i<4;i++) { //lines in cell
							var point = plane.intersectLine(cells[cell][i].geometry);
							//console.log("intersect check: " +i + " "+ point);
							if (point) {
								//console.log("1.PolygonPoint: " + point.toArray().toString() +" Cell: "+cell + " Line: "+ i);
								polygonPoints.push(point);
								firstCell = cells[cell];
								firstLine = cells[cell][i];
								return;
							}
						}
					}
				}; 
		
				var getRestOfPoints = function(cell, firstLine) {
					for(var i=0;i<4;i++){ //line in cell
						if(cell[i] !== firstLine){
							var point = plane.intersectLine(cell[i].geometry);
							if (point) {
								//console.log("PolygonPoint: " + point.toArray().toString()+ " Cell: "+ cell + " Line: "+i);
								polygonPoints.push(point);
								if(!(polygonPoints[0].equals(polygonPoints[polygonPoints.length-1]))){
									var nextCell = (cell[i].left === cell)? cell[i].right : cell[i].left;
									// (cell[i].left === cell)?console.log('nextCell: rightCell'):console.log('nextCell: leftCell') ;
									getRestOfPoints(nextCell, cell[i]);
								}
							}
						}
					}
				};
				
				// start algorithm
				
				setFirstCellAndPoint();
				if(firstCell)getRestOfPoints(firstCell, firstLine);
				//console.log('Polygon: '+polygonPoints.length);
				
				//flatten polygon to 2D
				for(var i=0,j=polygonPoints.length; i<j; i++){
				  polygonPoints[i] = GIScene.Utils.vector3ToVector2(polygonPoints[i]);
				};
				
			return polygonPoints; //Array of THREE.Vector2
			}.bind(this);
		
		var addBufferToPolygon = function(polygon, bufferDistance) {
			if ( polygon.length < 4 ){return polygon;}; // at least a triangle
			var isCCW = function(poylgon) { // convex polygon only
				var firstDirVector2  = new GIScene.Line2().fromPoints(polygon[0], polygon[1]).directionVector;
				var secondDirVector2 = new GIScene.Line2().fromPoints(polygon[1], polygon[2]).directionVector;
				var firstDirVector3 = new THREE.Vector3(firstDirVector2.x,firstDirVector2.y, 0);
				var secondDirVector3 = new THREE.Vector3(secondDirVector2.x,secondDirVector2.y, 0);
				var normal = THREE.Vector3.prototype.crossVectors(firstDirVector3, secondDirVector3);
				return (normal.z < 0)? true : false ;
			};
			// console.log("addBufferToPolygon", polygon.length, bufferDistance);
			// console.log(new GIScene.Line2().fromPoints(polygon[polygon.length-2], polygon[polygon.length-1]));
			
			if(!isCCW(polygon)){polygon.reverse();}
			 
			var bufferedPolygon = [];
			
			//first intersection
			bufferedPolygon.push(
						new GIScene.Line2().fromPoints(polygon[polygon.length-2], polygon[polygon.length-1])
						.moveRight(bufferDistance)
						.intersect(
							new GIScene.Line2().fromPoints(polygon[0], polygon[1])
							.moveRight(bufferDistance)
						)
					);
			// console.log(bufferedPolygon[0]);
			//rest of intersections
			for (var i=0; i < polygon.length-2; i++){
					
					bufferedPolygon.push(
						new GIScene.Line2().fromPoints(polygon[i], polygon[i+1])
						.moveRight(bufferDistance)
						.intersect(
							new GIScene.Line2().fromPoints(polygon[i+1], polygon[i+2])
							.moveRight(bufferDistance)
						)
					);
					// console.log(i);
			}
			
			bufferedPolygon.push(new THREE.Vector2(bufferedPolygon[0].x, bufferedPolygon[0].y) );
			// console.log(bufferedPolygon.length);
			// console.log(polygon[0]);
			// console.log(bufferedPolygon[0]);
			return bufferedPolygon;
		};
		
		var clipCuttingPolygonByMaxDistance = function(polygon, radius) {
			// console.log("Layer.Grid():getNewTiles():clipCuttingPolygonByMaxDistance()");
			// console.log(this);
			var isCCW = function(vertices) {
				var polygonArea = function() {
					var area = 0;
					for (var i = 0; i < vertices.length; i++) {
						j = (i + 1) % vertices.length;
						area += vertices[i][0] * vertices[j][1];
						area -= vertices[j][0] * vertices[i][1];
					}
					return area / 2;
				};
				
				var clockwise = polygonArea() > 0;
				return !clockwise;
			}; 

			var createCirclePolygon = function(origin, radius){
				// console.log("Layer.Grid():getNewTiles():clipCuttingPolygonByMaxDistance():createCirclePolygon()");
				// console.log(this);
				var sides = 24;
				var angle = Math.PI * ((1/sides) - (1/2));
				var rotatedAngle, x, y;
				var points = [];
				for(var i=0; i<sides; ++i) {
					rotatedAngle = angle + (i * 2 * Math.PI / sides); //orig angle -
					x = origin.x + (radius * Math.cos(rotatedAngle));
					y = origin.y + (radius * Math.sin(rotatedAngle));
					//points.push(new THREE.Vector2(x, y));
					points.push([x,y]);
				}
				return points;
			};
			
			//Sutherland-Hodgman-Algorithm for Polygon Clipping with a convex clipPolygon
			//Adapted from http://rosettacode.org/wiki/Sutherland-Hodgman_polygon_clipping#JavaScript
			var clip = function(subjectPolygon, clipPolygon) {
				// console.log("Layer.Grid():getNewTiles():clipCuttingPolygonByMaxDistance():clip()");
				// console.log(this);
				// subjectPolygon.pop();
				// subjectPolygon.forEach(function(e,i,a){a[i]= e.toArray();});
				//console.log(subjectPolygon.length);
				//clipPolygon.pop();
				// clipPolygon.forEach(function(e,i,a){a[i]= e.toArray();});
				//console.log(clipPolygon);

	            var clip1, clip2, the_s, the_e;
	            var inside = function (the_p) {
	                return (clip2[0]-clip1[0])*(the_p[1]-clip1[1]) > (clip2[1]-clip1[1])*(the_p[0]-clip1[0]);
	            };
	            var intersection = function () {
	                var dc = [ clip1[0] - clip2[0], clip1[1] - clip2[1] ],
	                    dp = [ the_s[0] - the_e[0], the_s[1] - the_e[1] ],
	                    n1 = clip1[0] * clip2[1] - clip1[1] * clip2[0],
	                    n2 = the_s[0] * the_e[1] - the_s[1] * the_e[0],
	                    n3 = 1.0 / (dc[0] * dp[1] - dc[1] * dp[0]);
	                return [(n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3];
	            };
	            var outputList = subjectPolygon;
	            clip1 = clipPolygon[clipPolygon.length-1];
	            // for (j in clipPolygon) {
	            for (var j=0,ln_j=clipPolygon.length;j<ln_j;j++) {
	                /*var*/ clip2 = clipPolygon[j];
	                var inputList = outputList;
	                outputList = [];
	                the_s = inputList[inputList.length - 1]; //last on the input list
	                // for (i in inputList) {
	                for (var i=0,ln=inputList.length; i < ln; i++) {
	                    var the_e = inputList[i];
	                    if (inside(the_e)) {
	                        if (!inside(the_s)) {
	                            outputList.push(intersection());
	                        }
	                        outputList.push(the_e);
	                    }
	                    else if (inside(the_s)) {
	                        outputList.push(intersection());
	                    }
	                    the_s = the_e;
	                }
	                clip1 = clip2;
	            }
	            //console.log("outputList: " +outputList);
	           // outputList.forEach(function(e,i,a){a[i]=new THREE.Vector2().fromArray(e);});
	           	// outputList.push(outputList[0]);
	           	return outputList;
	       }.bind(this);
	        
	        //start algorithm
	        
	        var extentPolygon = (_maxExtent)? _maxExtent.toPolygonV2() : null;
	        // console.log('clipcuttingpolybyextent._maxExtent', _maxExtent.max, this.distanceReferencePoint);
			if(extentPolygon) {
								extentPolygon.forEach(function (e,i,a){ a[i] = e.toVector2().toArray(); });
								extentPolygon.reverse();
							}
	        
	        // console.log("CCW extent: "+ isCCW(extentPolygon));
	        var subjPoly = createCirclePolygon(this.distanceReferencePoint, radius); //Array of Arrays
 	        // console.log("CCW circle: "+ isCCW(subjPoly));
 	        //console.log(polygon);
 	        polygon.pop();
 	        polygon.forEach(function(e,i,a){a[i]= e.toArray();});
 	        if(isCCW(polygon)){polygon.reverse();}
 	        // console.log("CCW frustum cut: "+ isCCW(polygon));
	        var clippedPoly = clip(subjPoly, polygon);//clip(polygon, subjPoly );//
	        //@TODO if maxExtent clip by its bbox rectangle
			// console.log(polygon);
			// console.log(subjPoly);
			// console.log(clippedPoly); //before clipped with maxExtent
			// console.log(extentPolygon);
// 			
			// console.log("CCW clippedPoly: "+ isCCW(clippedPoly));
			if (extentPolygon){
				clippedWithExtent = clip(extentPolygon,clippedPoly);
				//clippedWithExtent = clip(clippedPoly,extentPolygon);
				clippedPoly = clippedWithExtent;
			}
			
			//add first point as last
			if(clippedPoly.length > 0){ 
				clippedPoly.push(clippedPoly[0]);
				// console.log(clippedPoly);
				clippedPoly.forEach(function(e,i,a){a[i]=new THREE.Vector2().fromArray(e);});
				}
			// console.log('clippedPoly',clippedPoly.length);
	        return clippedPoly;
	        
		}.bind(this);
		
		/*
		 * restrict Polygon by angle and distance to camera
		 * 
		 * 5 degress could be a housenumber
		 */
		var restrictPolygonByAngleAndDistance = function(polygon, camera, minAngleDeg, maxDistance) {
			
			var minAngleDeg = minAngleDeg || 5;
			var minAngleRad = THREE.Math.degToRad(minAngleDeg);
			var tanMinAngle = Math.tan(minAngleRad);
			// var camerapos = camera.position.clone();
			// var PI_2 = Math.PI/2;
			//reduce by angle
			//check n-1 points of polygon nth point same as first of they undergo minAngleDeg
			//
			var angleToCam = function(point) {
				var camSubPoint = camera.position.clone().sub(point);
				var camSubPointOnPlane = camSubPoint.clone().setY(0);
				var angleToCamera = camSubPoint.angleTo(camSubPointOnPlane);
				return angleToCamera;
			};
			
			for(var i=0,j=polygon.length; i<j; i++){
				var camSubPoint = camera.position.clone().sub(polygon[i]);
				var camSubPointOnPlane = camSubPoint.clone().setY(0);
				var angleToCamera = camSubPoint.angleTo(camSubPointOnPlane);
				// var angleToCamera = angleToCam(polygon[i]);
				// console.log("angleToCamera: " + THREE.Math.radToDeg(angleToCamera) );
				if (angleToCamera < minAngleRad){
					//move point towards camera to meet angle restrictions
					// console.log("height: " + camSubPoint.clone().sub(camSubPointOnPlane).length()  );
					var distCamSubPointOnPlaneToTarget = camSubPoint.clone().sub(camSubPointOnPlane).length() / tanMinAngle;
					// console.log("distCamSubPointOnPlaneToTarget: " + distCamSubPointOnPlaneToTarget);
					var target = polygon[i].clone().sub(camSubPointOnPlane).normalize().multiplyScalar(distCamSubPointOnPlaneToTarget);
					// console.log("new deg: "+ THREE.Math.radToDeg(angleToCam(target)));
					
					polygon[i] = target;
				}
			};
			return polygon;
		};
		
		/**
		 * @method getOutlineTiles
		 * @private
 		 * @param {Array of THREE.Vector3} polygon
		 */
		var getOutlineTiles = function(polygon, tileSize) {
			// console.log("Layer.Grid():update():getNewTiles():getOutlineTiles()");
			// console.log(this);
			var outlineTiles=[];
			var gridLines2d =[];
			
			// var getV2fromV3 = function(v3) {
				// var v2 = new THREE.Vector2(v3.x, v3.z);
				// return v2;
			// };
			
			//get 2D Lines from Polygon and convert coordinates to grid index coords
			for(var i=0,j=polygon.length-1; i<j; i++){
			  var line = new GIScene.Grid.GridLine(this.grid.getIndexFromPoint2d( polygon[i], tileSize ) , this.grid.getIndexFromPoint2d( polygon[i+1], tileSize ));
			  // console.log(line.start.x +" "+ line.start.y);
			  gridLines2d.push(line);
			};
			
			//getTilesFromLines 
			
			
			
			for(var i=0,j=gridLines2d.length; i<j; i++){
			  outlineTiles = outlineTiles.concat(this.grid.getTilesFromGridLine(gridLines2d[i])); 
			};
			
			//get unique tiles!!, remove duplicates
			return GIScene.Utils.removeDuplicatesFromArray(outlineTiles);
		}.bind(this);
		
		
		var getFillTiles = function(outlineTiles){
			// console.log("Layer.Grid():update():getNewTiles():getFillTiles()");
			// console.log(this);
			
			//scanline
			
			//sort by y
			ySortedOutlineTiles={};
			for(var i=0,j=outlineTiles.length; i<j; i++){
				(outlineTiles[i].y.toString() in ySortedOutlineTiles)? ySortedOutlineTiles[outlineTiles[i].y.toString()].push(outlineTiles[i].x) : ySortedOutlineTiles[outlineTiles[i].y.toString()] = [outlineTiles[i].x]; 
			};
			// console.log(ySortedOutlineTiles);
			//only fill if scanline hits 2 x values
			var fillTiles = [];
			for(scanline in ySortedOutlineTiles){
				var xValues = ySortedOutlineTiles[scanline].sort(function(a,b){return a-b;}); //x always from l to r
				//console.log(xValues);
				if (xValues.length > 1){
					var y = parseInt(scanline);
					// console.log(y);
					var number=0, startX=0;
					// if (xValues.length == 2){
						// number = xValues[1] - xValues[0] - 1;
						// startX = xValues[0];	
					// }
					// else //4 or more xvalues
					// {
					for(var i=1,j=xValues.length; i<j; i++){
					  //find start
					  if(xValues[i] - xValues[i-1] != 1){
					  	number = xValues[i] - xValues[i-1] -1;
					  	startX = i-1;//xValues[i-1];
					  }
					 }
					 // console.log("number: " + number);
					 // console.log("startX: " + startX);
					 // console.log("xValues: "+ xValues);
					//}
					//fillTiles
					for(var i=0,j=number; i<j; i++){
						//console.log("xValues[i+startX]"+parseInt(xValues[startX]+i+1));
					  fillTiles.push(new GIScene.Grid.Index(xValues[startX]+i+1,y,this.grid.tileSize));
					};
				}
			}
			return fillTiles;
		}.bind(this);
		
		/////
		//+ Jonas Raoni Soares Silva
		//@ http://jsfromhell.com/math/is-point-in-poly [rev. #0]
		//@param poly {Array of Objects with x and y property. Last element equals the first element.}
		//@param pt {Object with x and y propery}
		// adapted by M.Auer
		//
		var isPointInPoly = function (poly, pt){
		    for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i)
		        ((poly[i].y <= pt.y && pt.y < poly[j].y) || (poly[j].y <= pt.y && pt.y < poly[i].y))
		        && (pt.x < (poly[j].x - poly[i].x) * (pt.y - poly[i].y) / (poly[j].y - poly[i].y) + poly[i].x)
		        && (c = !c);
		    return c;
		};
		
		//tile intersects polygon
		var tileIntersectsPolygon = function(tile, polygon) {
			// console.log("tileIntersectsPolygon");

				// from http://www.java-gaming.org/index.php?topic=22590.0
			   function linesIntersect( x1,  y1,  x2,  y2,  x3,  y3,  x4,  y4){
			      // Return false if either of the lines have zero length
			      if (x1 == x2 && y1 == y2 ||
			            x3 == x4 && y3 == y4){
			         return false;
			      }
			      // Fastest method, based on Franklin Antonio's "Faster Line Segment Intersection" topic "in Graphics Gems III" book (http://www.graphicsgems.org/)
			      var ax = x2-x1;
			      var ay = y2-y1;
			      var bx = x3-x4;
			      var by = y3-y4;
			      var cx = x1-x3;
			      var cy = y1-y3;
			
			      var alphaNumerator = by*cx - bx*cy;
			      var commonDenominator = ay*bx - ax*by;
			      if (commonDenominator > 0){
			         if (alphaNumerator < 0 || alphaNumerator > commonDenominator){
			            return false;
			         }
			      }else if (commonDenominator < 0){
			         if (alphaNumerator > 0 || alphaNumerator < commonDenominator){
			            return false;
			         }
			      }
			      var betaNumerator = ax*cy - ay*cx;
			      if (commonDenominator > 0){
			         if (betaNumerator < 0 || betaNumerator > commonDenominator){
			            return false;
			         }
			      }else if (commonDenominator < 0){
			         if (betaNumerator > 0 || betaNumerator < commonDenominator){
			            return false;
			         }
			      }
			      if (commonDenominator == 0){
			         // This code wasn't in Franklin Antonio's method. It was added by Keith Woodward.
			         // The lines are parallel.
			         // Check if they're collinear.
			         var y3LessY1 = y3-y1;
			         var collinearityTestForP3 = x1*(y2-y3) + x2*(y3LessY1) + x3*(y1-y2);   // see http://mathworld.wolfram.com/Collinear.html
			         // If p3 is collinear with p1 and p2 then p4 will also be collinear, since p1-p2 is parallel with p3-p4
			         if (collinearityTestForP3 == 0){
			            // The lines are collinear. Now check if they overlap.
			            if (x1 >= x3 && x1 <= x4 || x1 <= x3 && x1 >= x4 ||
			                  x2 >= x3 && x2 <= x4 || x2 <= x3 && x2 >= x4 ||
			                  x3 >= x1 && x3 <= x2 || x3 <= x1 && x3 >= x2){
			               if (y1 >= y3 && y1 <= y4 || y1 <= y3 && y1 >= y4 ||
			                     y2 >= y3 && y2 <= y4 || y2 <= y3 && y2 >= y4 ||
			                     y3 >= y1 && y3 <= y2 || y3 <= y1 && y3 >= y2){
			                  return true;
			               }
			            }
			         }
			         return false;
			      }
			      return true;
			   }
			
			var tileCorners = this.grid.getCornerCoordsFromIndex(tile);
			for(var i=0,j=polygon.length-1; i<j; i++){
				//iterate segments
				var tileIntersects =		linesIntersect(tileCorners[0].x,tileCorners[0].y, tileCorners[1].x,tileCorners[1].y, polygon[i].x, polygon[i].y, polygon[i+1].x, polygon[i+1].y )
										||	linesIntersect(tileCorners[1].x,tileCorners[1].y, tileCorners[2].x,tileCorners[2].y, polygon[i].x, polygon[i].y, polygon[i+1].x, polygon[i+1].y )
										||	linesIntersect(tileCorners[2].x,tileCorners[2].y, tileCorners[3].x,tileCorners[3].y, polygon[i].x, polygon[i].y, polygon[i+1].x, polygon[i+1].y )
										||	linesIntersect(tileCorners[3].x,tileCorners[3].y, tileCorners[0].x,tileCorners[0].y, polygon[i].x, polygon[i].y, polygon[i+1].x, polygon[i+1].y )
										;  
				if(tileIntersects)return true;  
			}
			return false;
			;
		}.bind(this);
			
		
		
		//reduce to maxNumTiles
		var workingVector = new THREE.Vector3();
		var distToCam = function (elementA, elementB) {
			var cameraPosition = this.scene.camera.position.clone();
			var centroidA = this.grid.getCentroidFromIndex(elementA);
			workingVector.set(centroidA.x, this.terrainHeight, centroidA.y);
			var distA = cameraPosition.distanceTo(workingVector);
			var centroidB = this.grid.getCentroidFromIndex(elementB);
			workingVector.set(centroidB.x, this.terrainHeight, centroidB.y);
			var distB = cameraPosition.distanceTo(workingVector);
			return distA - distB;
		}.bind(this);
		
		//area of 2D Polygon
		var getPolygonArea = function(polygon) {
	        var area = 0.0;
	        if ( polygon.length > 2 ) {
	            var sum = 0.0;
	            for (var i=0, len=polygon.length; i < len-1; i++) {
	                var b = polygon[i];
	                var c = polygon[i+1];
	                sum += (b.x + c.x) * (c.y - b.y);
	            }
	            area = - sum / 2.0;
	        }
	        return Math.abs(area);
	   };

		// calculates the point projected from the bottom center of the viewport on the plane at terrainHeight
		var getCenterBottomPoint = function() {
			var nearB = new THREE.Vector3(0,-1,1);
			var farB  = new THREE.Vector3(0,-1,-1);
			var nearT = new THREE.Vector3(0,1,1);
			var farT  = new THREE.Vector3(0,1,-1);
			
			var projector = new THREE.Projector();
			var nearBWorld = projector.unprojectVector(nearB, this.scene.camera);
			var farBWorld = projector.unprojectVector(farB, this.scene.camera);
			var nearTWorld = projector.unprojectVector(nearT, this.scene.camera);
			var farTWorld = projector.unprojectVector(farT, this.scene.camera);
			var checkLineB = new THREE.Line3(nearB, farB);
			var checkLineT = new THREE.Line3(nearT, farT);
			var checkPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(new THREE.Vector3(0,1,0), new THREE.Vector3(0,_terrainHeight,0));
			var centerBottomPoint = checkPlane.intersectLine(checkLineB);
			var centerTopPoint = checkPlane.intersectLine(checkLineT);
			var distanceReferencePoint = (centerBottomPoint)? centerBottomPoint : centerTopPoint;
			var centerBottomPoint2d = (distanceReferencePoint)?new THREE.Vector2(distanceReferencePoint.x, distanceReferencePoint.z) : null;
			return centerBottomPoint2d;
		}.bind(this);
		
		//============================================================
		//start algorithm
		//============================================================
		
		//get Height from existing tiles average or sample??
		
		var polygonPoints = intersectFrustrumAtTerrainHeight(_terrainHeight);//getHeight automatically
		// var polygonPointsFrustrumCut = polygonPoints.length;
		//add buffer around polygon
		//console.log(this.name+" intersectFrustrum", polygonPoints.length, getPolygonArea(polygonPoints), _terrainHeight);
		polygonPoints = addBufferToPolygon(polygonPoints, this.tileSizes[this.tileSizes.length-1]);
		
		
		// var poly3d=[];
		// for(var i=0,j=polygonPoints.length; i<j; i++){
			// poly3d.push(GIScene.Utils.vector2ToVector3(polygonPoints[i],_terrainHeight)); 
		// };
		// var line_geom = new THREE.Geometry();
		// line_geom.vertices = poly3d;
		// _polygon = new THREE.Line(line_geom);
		// this.scene.root.add(_polygon);
		
		//get centerbottom point of viewport on terrainHeight plane as reference for maxDistance calculations		
		var oldDistanceReferencePoint = this.distanceReferencePoint;
		this.distanceReferencePoint = getCenterBottomPoint() || oldDistanceReferencePoint;
		if(!this.distanceReferencePoint){return [];}
		

		//get clipped polygon
		polygonPoints = clipCuttingPolygonByMaxDistance(polygonPoints, this.maxDistance);
		//console.log(this.name+" clipCuttingPolygonByMaxDistance", polygonPoints.length);
		// poly3d = [];
		// for(var i=0,j=polygonPoints.length; i<j; i++){
		  // poly3d.push(GIScene.Utils.vector2ToVector3(polygonPoints[i],_terrainHeight)); 
		// };
		// var line_geom = new THREE.Geometry();
		// line_geom.vertices = poly3d;
		// _polygon = new THREE.Line(line_geom);
		// this.scene.root.add(_polygon);
		
		
		if(false){
		//Bresenham way
		console.time('bresenham');
		var outlineTiles = getOutlineTiles(polygonPoints); //Bresenham Lines
		//console.log("outlineTiles: "+outlineTiles.length);
		//console.log("tileSize: "+this.grid.tileSize);
		//console.log("terrainHeight: "+this.terrainHeight);
		
		var fillTiles = getFillTiles(outlineTiles); //scanline algorithm
		
		var allTiles = outlineTiles.concat(fillTiles);
		// console.log("numAllTiles: "+allTiles.length);
		
		
		
		//reduce
		// allTiles.sort(distToCam);
		// allTiles.length = this.maxNumTiles;
		
		// console.timeEnd('bresenham');
		//Bresenham way End
		}
		
		// allTiles=[];
		//Quadtree way
		if(true){
		// console.time('quadtree');
		// var origTileSize = this.grid.tileSize;  //@TODO use smallest TileSize this.tileSizes Math.min.apply(null,this.tileSizes);
		var smallestTileSize = Math.min.apply(null,this.tileSizes);
		var biggestTileSize  = Math.max.apply(null,this.tileSizes);
		var area = getPolygonArea(polygonPoints);
		//console.log(this.name+" area", area);
		var estNumberOfAllTiles = area/Math.pow(biggestTileSize,2);
		// var tileSizeFactor = Math.pow(2, Math.round(Math.log(Math.sqrt(area)/this.grid.tileSize)/Math.LN2));//nearest power of 2 on log scale (wikipedia)
		var tileSizeFactor = Math.pow(2, Math.round(Math.log(Math.sqrt(area)/smallestTileSize)/Math.LN2));//nearest power of 2 on log scale (wikipedia)
		
		// var tempTileSize= origTileSize*tileSizeFactor;
		var tempTileSize= smallestTileSize*tileSizeFactor;
		
		// console.log("Quadtree");
		/*console.log("origTileSize: "+origTileSize);
		console.log("area: "+area);
		console.log("estNumberOfAllTiles: "+estNumberOfAllTiles);
		console.log("tileSizeFactor: "+tileSizeFactor);
		console.log("tempTileSize: "+tempTileSize);
		*/
		if (estNumberOfAllTiles > 10000){
			var confirmed = confirm("estNumberOfAllTiles: "+Math.round(estNumberOfAllTiles)+". Stop calculating?");
			if(confirmed)return [];
		}
		
		var rootNodes;
		if(tileSizeFactor > 1){
			//this.grid.tileSize=tempTileSize;
			rootNodes = getOutlineTiles(polygonPoints, tempTileSize);
		}
		else {
			rootNodes = getOutlineTiles(polygonPoints, smallestTileSize);
		}
		// this.grid.tileSize=origTileSize;
		
		var traverseCriteria = function(node) {
			//tile is overlapping clipped viewport polygon
			var centroid2 = this.grid.getCentroidFromIndex(node);//v2
			var tileCorners = this.grid.getCornerCoordsFromIndex(node);
			
			var isTileOverlappingPolygon =		
												isPointInPoly(polygonPoints, tileCorners[0])
											||	isPointInPoly(polygonPoints, tileCorners[1])
											||	isPointInPoly(polygonPoints, tileCorners[2])
											||	isPointInPoly(polygonPoints, tileCorners[3])
											||	isPointInPoly(polygonPoints, centroid2)
											||	tileIntersectsPolygon(node, polygonPoints)
											;
			
			return isTileOverlappingPolygon;
			
			
		}.bind(this);
		
		var isLeafNode = function(node) {
			var isLeaf = false;
			
				
				
				if(node.tileSize <= this.tileSizes[0] && node.tileSize > this.tileSizes[this.tileSizes.length-1]){ //all tileSizes exept the smallest one
					var nodeCentroid = this.grid.getCentroidFromIndex(node);
					
					//is in distance of distanceReferencePoint
					// if(this.distanceReferencePoint.distanceTo(nodeCentroid) > node.tileSize * this.lodDistanceFactor){
						// isLeaf = true;
					// }
					//is in distance of camera
					//var camPos = new THREE.Vector2(this.scene.camera.position.x, this.scene.camera.position.z);
					var nodeCentroid3d = new THREE.Vector3(nodeCentroid.x, _terrainHeight, nodeCentroid.y);
					if(this.scene.camera.position.distanceTo(nodeCentroid3d) > node.tileSize * this.lodDistanceFactor){
						isLeaf = true;
					}
					
				}else if(node.tileSize == this.tileSizes[this.tileSizes.length-1]){
					isLeaf = true;
				}
				
				
			
			
			return isLeaf;
		}.bind(this);
		
		var onTileIsLeaf = function(node) {
				//@TODO better check for 3d distance OR checkfor distance to point on ground projected from centerbottom of viewport
				//var camera2d = new THREE.Vector2(this.scene.camera.position.x,this.scene.camera.position.z);
				
				
				// var nodeCentroid = this.grid.getCentroidFromIndex(node);
				// if(this.distanceReferencePoint.distanceTo(nodeCentroid)<= this.maxDistance)
					allTiles.push(node);
		}.bind(this);
		
		var allTiles = [];
		for(var i=0,j=rootNodes.length; i<j; i++){
		  this.grid.traverseIf(rootNodes[i],traverseCriteria,isLeafNode,onTileIsLeaf);
		};
		// console.timeEnd("quadtree");
		}
		//Quadtree way end
		
		// log2 = document.getElementById('log2');
		// log2.innerHTML = "PolyPts: "+ polygonPoints.length
		// +" PolyFrutrumCutPts: "+ polygonPointsFrustrumCut;
						// +"RootNodes: "+ rootNodes.length
						// +" TempTS: "+ tempTileSize
						// +" DRPt: "+ this.distanceReferencePoint.toArray();
						
		
		return allTiles;
	};
	
	/**
	 * updates the current tiles, loads new tiles and removes old ones
	 * @method update
	 */
	var oldTilesString=[];
	this.oldTiles=[];
	this.update = function() {
		// console.log("Layer.Grid():update()");
		// console.log(this);
		//console.time("update");
		var  newTiles, remove, keep, load;
		
		newTiles = this.computeTileIndicesHandler();
		
		// console.time('string');
		// var newTilesString = new Array(newTiles.length);
// 		
		// newTiles.forEach(function(item){
								// newTilesString.push(JSON.stringify(item));
		// });
// 		
		// var remove = oldTilesString.filter(function (item, index, array) {
		                       // return newTilesString.indexOf(item) == -1;
		                      // });
		// var keep = oldTilesString.filter(function (item, index, array) {
		                       // return newTilesString.indexOf(item) != -1;
		                       // });                       
// 		                       
		// var load = newTilesString.filter(function (item, index,array){
		                        // return  oldTilesString.indexOf(item) == -1;
		                   // });
		// console.timeEnd('string');
		
		// console.time('obj');
		
		
		
		
		this.remove = this.oldTiles.filter(function(v,ix,a){
			return !(newTiles.some(function(v_){
				return (v_.equals(v));
			}));
		});
		
		//just for error checking, not needed in production
		// var keep_ = this.oldTiles.filter(function(v,ix,a){
			// return (newTiles.some(function(v_){
				// return (v_.equals(v));
			// }));
		// });
		
		this.load = newTiles.filter(function (v,ix,a){
			return !(this.oldTiles.some(function(v_){
				return (v_.equals(v));
			}));
		}.bind(this));
		
		
		
		
		//remove load items from newTiles(oldTiles in next iteration), add loaded later but not in loading
		this.oldTiles = newTiles.filter(function(v,ix,a){
			return !(this.load.some(function(v_){
				return (v_.equals(v));
			}));
		}.bind(this));
		// this.oldTiles = newTiles;
		
		
		
		// console.log("generalize", generalize);
		
		// gehe alle remove durch und checke ob desendantOf load
		
		//which are parents to be removed by a number of children. Load x and remove parent
		// load has parent in remove?
		var specialize = {}; // {"parent":[descendants]}
		//gehe alle load durch und checke ob sie abstammende sind von einem remove
		
		//SPECIALIZE
		this.load.forEach(function(el, il, al){
			
			this.remove.forEach(function (er, ir, ar){
				if(this.grid.isDescendantOf(el, er)){
					// if(!specialize[er.toString()]) {specialize[er.toString()] = [];};
					// specialize[er.toString()].push(el);
					
					// if(this.specialize.getTile(er) === false){
						// this.specialize.add(er,[el]);
					// }
					// else{
						// this.specialize.object.push(el);
					// }
					// console.log("specialize", this.specialize);
					//not yet removed 
					this.oldTiles.push(ar[ir]); //keep in oldTiles for next iteration check
					ar[ir] = null; //do not remove
				}
			}.bind(this));
		}.bind(this) );
// 		
		// //check for specialize remove. Remove parent if all children are in loaded
		// for(parent in this.specialize.store){
			// var descendants = this.specialize.store[parent].object;
			// var allLoaded = descendants.every(function(e,i,a){
				// return this.loaded.getTile(e);
			// }.bind(this));
			// if(allLoaded){
// 				
				// this.specialize.remove(new GIScene.Grid.Index().fromString(parent));
				// this.remove.push(new GIScene.Grid.Index().fromString(parent));
			// }else{
				// // auto update specialize???
				// this.specialize.remove(new GIScene.Grid.Index().fromString(parent));
				// newTiles.push(new GIScene.Grid.Index().fromString(parent));
			// }
		// }
		//
		
		
		//find out which of the new ones replace children. Load one remove all descendants
		// load has children in remove?
		// generalize
		var generalize = {};
		
		this.load.forEach(function(el,il,al){
			this.remove.forEach(function(er, ir, ar){
				if(this.grid.isDescendantOf(er, el)){
					// if(!generalize[el.toString()]) {generalize[el.toString()] = [];};
					//add remove index to generalize for later removement
					// console.log("generalizePush", er);
					// generalize[el.toString()].push(er);
					// console.log("generalize", generalize);
					//remove generalize descentands from immediately remove array
					// this.remove = this.remove.filter(function(e,i,a){
						// return e.toString() != er.toString();
					// });
					//not yet removed 
					this.oldTiles.push(ar[ir]); //keep in oldTiles for next iteration check
					ar[ir] = null; //do not remove
					
				}
			}.bind(this)); 
		}.bind(this));
		//______
		// else{
			//find out which ones to remove immediately
			// remove rest immediately
		// }
		
		
		


		// console.timeEnd('obj');
		
		// console.log(remove.length);
		// console.log(keep.length);
		// console.log(load.length);
		// console.log(this.remove.length);
		// console.log(keep_.length);
		// console.log(this.load.length);
		                   
		//removeTiles(remove); //cancelTiles in Loading queue
		//keepInCacheTiles
		//deleteTiles
			
		//sort load by distance to camera
		// this.removeTiles(this.abort, generalize);
		
		this.loadTiles(this.load, generalize, specialize);
		this.removeTiles(this.remove, generalize);
		
		//check loading tiles for abortion
		this.abort = 
		this.loading.indexStore.store.filter(function(v,ix,a){
			return !(this.load.some(function(v_){
				return (v_.equals(new GIScene.Grid.Index().fromString(v)));
			}));
		}.bind(this));
		//console.log("abort", this.abort.length);
		this.removeTiles(this.abort, generalize);
		
		// this.oldTiles = newTiles;
		// oldTilesString = newTilesString;
		// console.timeEnd("update");
	};
	
	/**
	 * set an approximate terrain height at which the view frustrum will be cut horizontally to determine the visible tiles to be loaded
	 * @method setTerrainHeight
	 * @param {Number} height the terrain height in coordinate reference system (CRS) units (e.g. meters)
	 */
	this.setTerrainHeight = function(height) {
		this.terrainHeight = height;
		_terrainHeight = (this.scene)? this.terrainHeight - this.scene.config.offset.z : this.terrainHeight;
		// console.log(_terrainHeight);
	};
	
	/**
	 * set the max extent of the layer to restrict loading of tiles to a rectangle/bounding box
	 * @method setMaxExtent
	 * @param {GIScene.Extent2} maxExtent2 a 2D bounding box in coordinate reference system (CRS) units 
	 */
	this.setMaxExtent = function(maxExtent2) {
		if(maxExtent2){
			if(this.scene){
				var offset2 = new GIScene.Coordinate2(this.scene.config.offset.x, this.scene.config.offset.y); 
				var min = maxExtent2.min.clone().sub(offset2);
				var max = maxExtent2.max.clone().sub(offset2);
				_maxExtent = new GIScene.Extent2(new GIScene.Coordinate2(min.x, min.y), new GIScene.Coordinate2(max.x, max.y));
				
			}
			else{
				_maxExtent = this.maxExtent;
			}
			
			// console.log(this.name+ ' _maxExtent', _maxExtent);
		}
	};
	
	/**
	 * switches the visibility of the layer on or off
	 * @method setVisibility
	 * @param {Boolean} visibility 
	 */
	this.setVisibility = function(visibility) {
		(visibility)?this.startUpdate():this.stopUpdate();
		GIScene.Layer.Grid.prototype.setVisibility.call(this, visibility);
	};
	
	//start auto initialization 
	this.init();
	
	// when added to a scene 
	var onSetScene = function(event) {
		console.log('Grid.onSetScene ' + this.name);
		var scene = event.content;
		
		this.grid = (scene) ? new GIScene.Grid({origin:this.origin, tileSizes:this.tileSizes, sceneOffset:scene.config.offset})
								 : null
								 ;
		//_terrainHeight = this.terrainHeight - scene.config.offset.z;
		this.setTerrainHeight(this.terrainHeight);
		
		//this.maxExtent = (this.maxExtent)? this.maxExtent[0].toVector3(). : null;
		this.setMaxExtent(this.maxExtent);
		
		(scene) ? this.startUpdate() : this.stopUpdate();
	}.bind(this);
	this.addEventListener('setScene', onSetScene);
	
	
	if(this.config.overrideMaterialHandler){
		//this.addEventListener('afterSetOverrideMaterial', this.config.overrideMaterialHandler);
		this.setOverrideMaterialHandler(this.config.overrideMaterialHandler);
	}
	
	if(this.attributeReader){
		
		var setAttributes = function(event) {
			
			var root = (!!event)? event.content.tile : this.root;
			
			root.traverse(function(object){
				if( ! ("gisceneAttributes" in object.userData) ){object.userData.gisceneAttributes = {}; }
				for (attr in this.attributeReader){
					object.userData.gisceneAttributes[attr] = this.attributeReader[attr](object);
				}
			}.bind(this) );
		}.bind(this);
		
		//check for already loaded objects
		setAttributes();
		//check all new loaded objects on load event
		this.addEventListener('tileload', setAttributes);
	}
	
	//if(this.virtualSelectionAccessor){
	
		
		// var selectVirtualSelection = function(event) {
// 			
			// var root = (!!event)? event.content.tile : this.root;
			// var matches = [];
			// for (var i=0,l=this.virtualSelection.length; i < l ; i++){
				// var match = GIScene.Utils.getObjectsBy(root, function(object){
					// return this.virtualSelectionAccessor(object) == this.virtualSelection[i];
				// }.bind(this));
				// //Array.prototype.push.apply( matches, match);
				// if(match.length > 0){
					// this.selectControl.select(match[i]);
					// this.virtualSelection.splice(i,1);
				// }		
			// }	 
		// }.bind(this);
// 		
		// var restoreVirtualSelection = function(event) {
			// var tile = event.content.tile;
// 			
			// tile.traverse(function(object){
				// if(object.userData.isSelected){
					// this.virtualSelection.push(this.virtualSelectionAccessor(object));
					// this.selectControl.unselect(object);
				// }
			// }.bind(this));
// 			
		// }.bind(this);
		
	var selectSelectionQueryStack = function(event) {
		if(this.selectionQueryStack){
			this.selectByAttributes(this.selectionQueryStack,event.content.tile);
		}
		
	}.bind(this);
	
	var unselectAllOnTileRemove = function(event) {
		var tile = event.content.tile;
		
		tile.traverse(function(object){
				if(object.userData.isSelected){
					this.selectControl.unselect(object);
				}
			}.bind(this)
		);
		
	}.bind(this);
	
	//check already loaded
	//selectVirtualSelection();
	if(this.selectionQueryStack)this.selectByAttributes(this.selectionQueryStack);
	
	//check future added tiles
	this.addEventListener('tileadd', selectSelectionQueryStack);
	
	//unselect all on tile remove
	this.addEventListener('tileremove',unselectAllOnTileRemove);
		
	
};

//Inherit from GIScene.Layer
GIScene.Layer.Grid.prototype = Object.create( GIScene.Layer.prototype );


//Prototype Methods

GIScene.Layer.Grid.prototype.setOverrideMaterial = function(node, overrideMaterial) {
	
	//set material to loaded tiles
	GIScene.Layer.prototype.setOverrideMaterial.apply(this, [node, overrideMaterial]);
	
	//set material to cached tiles
	var tileStore = this.cache.store;
	for(tile in tileStore){
		var object = tileStore[tile].object;
		GIScene.Layer.prototype.setOverrideMaterial.apply(this, [object, overrideMaterial]);
	}
	
};

GIScene.Layer.Grid.prototype.setOverrideMaterialHandler = function(overrideMaterialHandler) {
	if(overrideMaterialHandler){
		//remove the old
		if(this.overrideMaterialHandler){
			this.removeEventListener('afterSetOverrideMaterial', this.overrideMaterialHandler);
		}
		//set the new
		this.overrideMaterialHandler = overrideMaterialHandler;
		this.addEventListener('afterSetOverrideMaterial', this.overrideMaterialHandler);
	}
	else{
		if(this.overrideMaterialHandler){
			this.removeEventListener('afterSetOverrideMaterial', this.overrideMaterialHandler);
		}
	}
	
};

GIScene.Layer.Grid.prototype.setActiveStyle = function(style){
	
	if( !style ||  (typeof style == 'string' && style.toLowerCase() == "default") ){
			style = this.styles[0];
		}
	
	//is layer grid?
	// var isGridLayer = layer instanceof GIScene.Layer.Grid;				
	// console.log("isGridLayer", isGridLayer);
	
	var recursive = style.recursive; //TODO implement recursive as parameter of setOverrideMaterial
	var material = style.material;
	var selectionType = (style.rootObjects)? "byObjects" : (style.rootObjectKeyAttribute)? "byAttributes" : "selectAll";
	
	//isMaterrial RasterOverlay or WMSOvwerlay
	var isWMSOverlayMaterial = material instanceof GIScene.WMSOverlayMaterial;
	console.log("isWMSOverlayMaterial",isWMSOverlayMaterial);
	
	if(/*isGridLayer && */ isWMSOverlayMaterial){
		this.setOverrideMaterialHandler(GIScene.OverrideMaterialHandler.WMS);
		
	}else{
		this.setOverrideMaterialHandler(null);
		
	}
	
	GIScene.Layer.prototype.setActiveStyle.apply(this, [style]); //@TODO instead of using super class method, implement event driven updates onTileAdd similar to selection queryStack 
	
};