/**
* A Grid is defined by an origin and a tile size and provides functions to get indices, boundingBoxes etc.
*
* @namespace GIScene
* @class Grid
* @constructor
* @param {Object} config
*
*/
GIScene.Grid = function (config) {
//Layer properties
var defaults = {
origin: new GIScene.Coordinate2(0,0) , //new THREE.Vector2(0,0),
// tileSize:16,
tileSizes : [16],
sceneOffset: new GIScene.Coordinate3()
};
this.config = GIScene.Utils.mergeObjects(defaults, config || {});
this.origin = this.config.origin; //.setY(-this.config.origin.y); //substract offset?
// this.tileSize = this.config.tileSize;
this.tileSizes = this.config.tileSizes;
this.sceneOffset = this.config.sceneOffset;
this._sceneOffset = this.sceneOffset.toVector3();
this._origin = this.origin.toVector2().clone().sub(GIScene.Utils.vector3ToVector2(this._sceneOffset));
};
/**
* An object representing a tile in the grid.
* The x property indexes the tiles from west to east.
* The y property indexes the tiles from south to north.
*
* @namespace GIScene
* @class Grid.Index
* @constructor
* @param {Integer} x
* @param {Integer} y
* @param {Number} tileSize
*/
GIScene.Grid.Index = function(x,y,tileSize) {
this.x = x;
this.y = y;
this.tileSize = tileSize;
};
GIScene.Grid.Index.prototype = {
//constructor : GIScene.Grid.Index,
equals : function(gridIndex) {
return this.x == gridIndex.x && this.y == gridIndex.y && this.tileSize == gridIndex.tileSize;
},
toString : function() {return this.x+"_"+this.y+"_"+this.tileSize;},
fromString : function(gridIndexString) {
var tokens = gridIndexString.split("_");
this.x = tokens[0];
this.y = tokens[1];
this.tileSize = tokens[2];
return this;
},
getChildNW : function() {
var x = this.x * 2;
var y = this.y * 2 + 1;
return new GIScene.Grid.Index(x,y,this.tileSize * 0.5 );
},
getChildSW : function() {
var x = this.x * 2;
var y = this.y * 2;
return new GIScene.Grid.Index(x,y,this.tileSize * 0.5 );
},
getChildNE : function() {
var x = this.x * 2 + 1;
var y = this.y * 2 + 1;
return new GIScene.Grid.Index(x,y,this.tileSize * 0.5 );
},
getChildSE : function() {
var x = this.x * 2 + 1;
var y = this.y * 2;
return new GIScene.Grid.Index(x,y,this.tileSize * 0.5 );
}
};
GIScene.Grid.GridLine = function (start, end) { //GIScene.Grid.Index
//check for same tileSize
if(start.tileSize != end.tileSize){
alert("GIScene.Grid.GridLine(): start and end tiles have different tileSizes. They must have equal ones!");
}
this.start = start;
this.end = end;
};
GIScene.Grid.IndexStore = function() {
this.store = []; //stores stringified GIScene.Grid.Index values
// Index or Array of Index
this.add = function(indexArray){
indexArray = ( indexArray instanceof Array )? indexArray : [indexArray];
//indexArray.forEach(function (e,i,a){this.store[e.toString()]={index:e};}.bind(this));
indexArray.forEach(function(e,i,a){a[i]=a[i].toString();});
this.store = this.store.concat(indexArray);
};
// Index or Array of Index
this.remove = function(indexArray){
indexArray = ( indexArray instanceof Array )? indexArray : [indexArray];
//indexArray.forEach(function(e,i,a){ delete this.store[e.toString()]; }.bind(this));
indexArray.forEach(function(e,i,a){a[i] = a[i].toString();});
var lengthBefore = this.store.length;
this.store = this.store.filter(function(e,i,a){return( indexArray.indexOf(e) == -1 );});
if(lengthBefore == this.store.length){return false;}else {return true;}
};
};
GIScene.Grid.TileStore = function(config) {
var defaults = {
maxLength:1 //null
// deleteMaterials: false,
// deleteTextures: false
};
this.config = GIScene.Utils.mergeObjects(defaults, config || {});
this.maxLength = this.config.maxLength;
// this.deleteMaterials = this.config.deleteMaterials;
// this.deleteTextures = this.config.deleteTextures;
this.store = {}; //stores stringified GIScene.Grid.Index values with a reference to a THREE.Object3D
this.indexStore = new GIScene.Grid.IndexStore();
this.length = 0;
var ageCounter = 0;
// Index or Array of Index
this.add = function(gridIndex, tileObject){
if(this.maxLength && this.length > this.maxLength){this.removeOldestEntry();}
// console.log("GIScene.Grid.TileStore.length: "+this.length);
// tileArray = ( tileArray instanceof Array )? tileArray : [tileArray];
// tileArray.forEach(function (e,i,a){this.store[e.toString()]={index:e};}.bind(this));
// tileArray.forEach(function(e,i,a){a[i]=a[i].toString();});
// this.store = this.store.concat(tileArray);
if(gridIndex instanceof GIScene.Grid.Index){
this.store[gridIndex.toString()] = {object : tileObject, age:ageCounter++};
this.indexStore.add(gridIndex);
this.length++;
}else{
console.log("GIScene.Grid.TileStore: First argument is not of type GIScene.Grid.Index");
// this.length++;
}
};
// Index or Array of Index
this.remove = function(gridIndex){
if(this.store[gridIndex.toString()]){
delete this.store[gridIndex.toString()].object;
delete this.store[gridIndex.toString()].age;
delete this.store[gridIndex.toString()];
this.indexStore.remove(gridIndex);
this.length--;
return true;
}
else return false;
};
this.getTile = function (gridIndex) {
var gridIndexString = gridIndex.toString();
return (gridIndexString in this.store)? this.store[gridIndexString].object : false;
};
this.removeOldestEntry = function() {
var oldest={age:Number.MAX_VALUE};
var oldestKey;
for(tile in this.store){
if(this.store[tile].age < oldest.age){oldest = this.store[tile]; oldestKey = tile;};
}
console.log("oldestKey: "+oldestKey);
//dispose object and it geometry but leave material and textures in case that they are used by multiple objects
var object = this.store[oldestKey].object;
// if(object.geometry){
// object.geometry.dispose();
// delete object.geometry;
// }
// var object = null;
(object.material && object.material.isShared === false )? //isShared is defined e.g. in OverrideMaterialHandler/WMS.js
GIScene.Utils.disposeObject(object,true, true, true, true) :
GIScene.Utils.disposeObject(object,true, false, false, true)
;
delete this.store[oldestKey].object;
delete this.store[oldestKey].age;
delete this.store[oldestKey];
this.length--;
};
};
GIScene.Grid.prototype = {
constructor : GIScene.Grid,
// getIndexFromPoint2d : function ( vector2 ) {
getIndexFromPoint2d : function ( vector2, tileSize ) {
return new GIScene.Grid.Index(
// Math.floor( vector2.x / this.tileSize ) - Math.floor( this._origin.x / this.tileSize ),
Math.floor( vector2.x / tileSize ) - Math.floor( this._origin.x / tileSize ),
//y needs to be negative because z goes negative to north. (0,0) is now in the lower left corner of the grid
// -(Math.ceil( vector2.y / this.tileSize ) - Math.floor( this._origin.y / this.tileSize )),
-(Math.ceil( vector2.y / tileSize ) - Math.floor( this._origin.y / tileSize )),
// (Math.ceil( -vector2.y / this.tileSize ) - Math.floor( -this.origin.y / this.tileSize )),
// this.tileSize
tileSize
);
},
//returns decimal gridcoords with tile center as full numbers
getGridCoordFromPoint2d :function (vector2, tileSize) { //vector2 in graphics order (x: W->E, y:N->S[neg. value grows to the north]) and sceneoffset subtracted
return new THREE.Vector2(
( vector2.x / tileSize ) - ( this._origin.x / tileSize ) - ( 0.5 ),
-( ( vector2.y / tileSize ) - ( this._origin.y / tileSize ) ) - ( 0.5 )
);
},
getCentroidFromIndex: function(index) {
var x = index.x*index.tileSize + this._origin.x/*index.tileSize*/ + index.tileSize/2;
var y = -index.y*index.tileSize + this._origin.y/*index.tileSize*/ - index.tileSize/2;
// var y = -index.y*index.tileSize + this.origin.y/*index.tileSize*/ - index.tileSize/2;
return new THREE.Vector2(x,y);
},
getBoundingBoxFromIndex: function(index) {
var centroid2 = this.getCentroidFromIndex(index);//v2
var halfSize = index.tileSize/2;
var bbox = {
left: centroid2.x - halfSize,
right: centroid2.x + halfSize,
top: centroid2.y - halfSize,
bottom: centroid2.y + halfSize
};
return bbox;
},
getCornerCoordsFromIndex: function(index) {
var bbox = this.getBoundingBoxFromIndex(index);
var cornerCoords = [
{x: bbox.left, y: bbox.bottom}, //sw
{x: bbox.right, y: bbox.bottom},//se
{x: bbox.right, y: bbox.top}, //ne
{x: bbox.left, y: bbox.top} //nw
];
return cornerCoords;
},
isDescendantOf: function(descendant, ancestor) { //both GIScene.Grid.Index
if(!descendant || !ancestor)return false;
var tileSizeLevelDistance = this.tileSizes.indexOf(descendant.tileSize) - this.tileSizes.indexOf(ancestor.tileSize);
var levelFactor = tileSizeLevelDistance *2;
var xAncestorLevel = Math.floor(descendant.x / levelFactor);
var yAncestorLevel = Math.floor(descendant.y / levelFactor);
return (xAncestorLevel == ancestor.x && yAncestorLevel == ancestor.y); //isDescendant ?
},
traverseIf_old: function(node, traverseCriteria, callback) {
if(traverseCriteria(node)){
if(node.tileSize <= this.tileSizes[0]){ //is leaf
callback(node);
}
else {
this.traverseIf(node.getChildNW(),traverseCriteria, callback);
this.traverseIf(node.getChildNE(),traverseCriteria, callback);
this.traverseIf(node.getChildSW(),traverseCriteria, callback);
this.traverseIf(node.getChildSE(),traverseCriteria, callback);
}
}
},
traverseIf: function(node, traverseCriteria, isLeafNode, callback) {
if (traverseCriteria(node)){ //is overlapping clipped view port polygon
if (isLeafNode(node)){
callback(node);
} else {
this.traverseIf(node.getChildNW(),traverseCriteria, isLeafNode, callback);
this.traverseIf(node.getChildNE(),traverseCriteria, isLeafNode, callback);
this.traverseIf(node.getChildSW(),traverseCriteria, isLeafNode, callback);
this.traverseIf(node.getChildSE(),traverseCriteria, isLeafNode, callback);
}
}
},
//Bresenham Line
getTilesFromGridLine : function (gridLine) {
var tileSize = gridLine.start.tileSize;
var tiles = [];
// Translate coordinates
var x1 = gridLine.start.x; //startCoordinates.left;
var y1 = gridLine.start.y; //startCoordinates.top;
var x2 = gridLine.end.x; //endCoordinates.left;
var y2 = gridLine.end.y; //endCoordinates.top;
// Define differences and error check
var dx = Math.abs(x2 - x1);
var dy = Math.abs(y2 - y1);
var sx = (x1 < x2) ? 1 : -1;
var sy = (y1 < y2) ? 1 : -1;
//8-connectedLine Bresenham
if(false){
var err = dx - dy;
// Set first coordinates
tiles.push(new GIScene.Grid.Index(x1, y1, tileSize));
// Main loop
while (!((x1 == x2) && (y1 == y2))) {
var e2 = err << 1;
if (e2 > -dy) {
err -= dy;
x1 += sx;
}
if (e2 < dx) {
err += dx;
y1 += sy;
}
// Set coordinates
tiles.push(new GIScene.Grid.Index(x1, y1, tileSize));
}
}
//4-connected line (see http://stackoverflow.com/questions/5186939/algorithm-for-drawing-a-4-connected-line)
var e = 0;
for (var i=0; i < dx+dy; i++) {
tiles.push(new GIScene.Grid.Index(x1, y1, tileSize));
var e1 = e + dy;
var e2 = e - dx;
if (Math.abs(e1) < Math.abs(e2)) {
x1 += sx;
e = e1;
} else {
y1 += sy;
e = e2;
}
}
// draw reverse
var e = 0;
for (var i=0; i < dx+dy; i++) {
tiles.push(new GIScene.Grid.Index(x1, y1, tileSize));
var e1 = e + dy;
var e2 = e - dx;
if (Math.abs(e1) < Math.abs(e2)) {
x1 += -sx;
e = e1;
} else {
y1 += -sy;
e = e2;
}
}
//if start and endpoint are equal return one tile
if(dx+dy == 0){
tiles.push(new GIScene.Grid.Index(x1, y1, tileSize));
}
// Return the result
return tiles;
},
/**
* Returns an array of Grid.Index objects which are intersected by a line
*
* @method getTilesFromLineIntersection
* @param {GIScene.Coordinate2} startCoord2
* @param {GIScene.Coordinate2} endCoord2
* @param {Number} tileSize
* @return {Array of GIScene.Grid.Index} tiles
*/
getTilesFromLineIntersection : function(startCoord2, endCoord2, tileSize) { //GIScene.Coordinate2
// ctx.rasterLine4 = function(x1o,y1o,x2o,y2o, color, scale) {
var tiles = [];
var startV2_ = startCoord2.toVector2().clone().sub(GIScene.Utils.vector3ToVector2(this._sceneOffset));
var endV2_ = endCoord2.toVector2().clone().sub(GIScene.Utils.vector3ToVector2(this._sceneOffset));
//convert world into grid coord system
var startGridCoord = this.getGridCoordFromPoint2d( startV2_, tileSize );
var endGridCoord = this.getGridCoordFromPoint2d( endV2_, tileSize );
var x1o = startGridCoord.x;
var y1o = startGridCoord.y;
var x2o = endGridCoord.x;
var y2o = endGridCoord.y;
//original slope (m)
var dxo = (x2o - x1o);
var dyo = (y2o - y1o);
var m = dyo/dxo;
//@TODO horizontal and vertical lines (m==0 bzw. m == Infinity)
var horizontal = ( dyo == 0 );
var vertical = ( dxo == 0 );
//step direction
var sx = (x1o < x2o) ? 1 : -1; //step x (positive: line goes from left to right)
var sy = (y1o < y2o) ? 1 : -1; //step y (positive: line goes from top to bottom)
//const
var m_sx = m*sx;
var m_sy = (1/m)*sy;
//number of steps to draw
var lx1 = Math.abs(Math.round(x2o)-Math.round(x1o)); //in x direction
var ly1 = Math.abs(Math.round(y2o)-Math.round(y1o)); //in y direction
//start values
var x1o_round = Math.round(x1o);
var y1o_round = Math.round(y1o);
var _delta_x = x1o - x1o_round;
var _delta_y = y1o - y1o_round;
var x_step = x1o - _delta_y * (1/m);
var y_step = y1o - _delta_x * m;
//end values
var x2o_round = Math.round(x2o);
var y2o_round = Math.round(y2o);
//tile coordinates
var _x,_y;
//if slope <= 1 do x steps
if( Math.abs(m) <= 1 && !horizontal){
//1st to penultimate point
for ( var i = 0; i < lx1; i++ ){
_x = x1o_round + i*sx;
_y = (y_step + i*m_sx);
var x_round = Math.round(_x);
var y_round = (i!=0)? Math.round(_y) : y1o_round; //draw first point where it is, all others with y according to middle of tile
// ctx.drawPoint(x_round,y_round,color,scale);
tiles.push(new GIScene.Grid.Index(x_round,y_round,tileSize));
//need for filling point?
var change = (Math.round(y_step + (i+1)*m_sx) - y_round);// != 0);
if(i == lx1-1){
//last change
change = y2o_round - y_round; // != 0;
}
if(change != 0){
//fill left or right
var y_cutNext = (y_step + (m_sx*(i+0.5)));
var left_side = Math.round(y_cutNext) == y_round;
//if passing exactly in the middle draw both for touching is true
//be tolerant to floating point errors
var both = ((Math.floor((Math.abs(y_cutNext)%1) * 10000))/10000 == 0.5);
if(left_side || both){
// ctx.drawPoint(x_round+sx,y_round,"green",scale);
tiles.push(new GIScene.Grid.Index(x_round+sx,y_round,tileSize));
}
if(!left_side || both){
// ctx.drawPoint(x_round,y_round+sy,"blue",scale);
tiles.push(new GIScene.Grid.Index(x_round,y_round+sy,tileSize));
}
if( Math.abs(change) > 1){
// ctx.drawPoint(x_round+sx,y_round+sy,"aqua",scale);
tiles.push(new GIScene.Grid.Index(x_round+sx,y_round+sy,tileSize));
}
}
} //end for
// lastpoint
// ctx.drawPoint(Math.round(x2o),Math.round(y2o),"yellow",scale);
tiles.push(new GIScene.Grid.Index(Math.round(x2o),Math.round(y2o),tileSize));
}
//if slope > 1 do y steps
if( Math.abs(m) > 1 && !vertical){
//1st to penultimate point
for ( var i = 0; i < ly1; i++ ){
_y = y1o_round + i*sy;
_x = (x_step + i*m_sy);
var y_round = Math.round(_y);
var x_round = (i!=0)? Math.round(_x) : x1o_round; //draw first point where it is, all others with y according to middle of tile
// ctx.drawPoint(x_round,y_round,color,scale);
tiles.push(new GIScene.Grid.Index(x_round,y_round,tileSize));
//need for filling point
var change = (Math.round(x_step + (i+1)*m_sy) - x_round );//!= 0); 0:no change; 1 or 2?
if(i == ly1-1){
//last change
change = x2o_round - x_round;// != 0;
}
if(change !=0 ){
//fill left or right
var x_cutNext = (x_step + (m_sy*(i+0.5)));
var left_side = Math.round(x_cutNext) == x_round;
//if passing exactly in the middle draw both for touching is true
//be tolerant to floating point errors
var both = ((Math.floor((Math.abs(x_cutNext)%1) * 10000))/10000 == 0.5);
if(!left_side || both){
// ctx.drawPoint(x_round+sx,y_round,"green",scale);
tiles.push(new GIScene.Grid.Index(x_round+sx,y_round,tileSize));
}
if(left_side || both){
// ctx.drawPoint(x_round,y_round+sy,"blue",scale);
tiles.push(new GIScene.Grid.Index(x_round,y_round+sy,tileSize));
}
if( Math.abs(change) > 1){
// ctx.drawPoint(x_round+sx,y_round+sy,"aqua",scale);
tiles.push(new GIScene.Grid.Index(x_round+sx,y_round+sy,tileSize));
}
}
} //end for
//lastpoint
// ctx.drawPoint(x2o_round,y2o_round,"yellow",scale);
tiles.push(new GIScene.Grid.Index(x2o_round,y2o_round,tileSize));
}
//if slope Infinity draw vetical line
if( vertical ){
for(var i=0;i<=ly1;i++){
// ctx.drawPoint(x1o_round,y1o_round + i,color,scale);
tiles.push(new GIScene.Grid.Index(x1o_round,y1o_round + i,tileSize));
//be tolerant to floating point errors
if( Math.floor( ( Math.abs(x1o)%1 )*10000 )/10000 == 0.5 ){
// ctx.drawPoint(x1o_round-1,y1o_round + i,"green",scale);
tiles.push(new GIScene.Grid.Index(x1o_round-1,y1o_round + i,tileSize));
}
}
}
if( horizontal ){
for(var i=0;i<=lx1;i++){
// ctx.drawPoint(x1o_round+i,y1o_round,color,scale);
tiles.push(new GIScene.Grid.Index(x1o_round+i,y1o_round,tileSize));
//be tolerant to floating point errors
if( Math.floor( ( Math.abs(y1o)%1 )*10000 )/10000 == 0.5 ){
// ctx.drawPoint(x1o_round +i,y1o_round - 1,"green",scale);
tiles.push(new GIScene.Grid.Index(x1o_round +i,y1o_round - 1,tileSize));
}
}
}
return tiles;
//};
},
addEventListener: THREE.EventDispatcher.prototype.addEventListener,
hasEventListener: THREE.EventDispatcher.prototype.hasEventListener,
removeEventListener: THREE.EventDispatcher.prototype.removeEventListener,
dispatchEvent: THREE.EventDispatcher.prototype.dispatchEvent
};