/*
* © 2007-2008 Benoît Pin – Centre de recherche en informatique – École des mines de Paris
* http://plinn.org
* Licence Creative Commons http://creativecommons.org/licenses/by-nc/2.0/
* $Id: mosaique.js 1265 2009-08-05 08:22:59Z pin $
* $URL: http://svn.luxia.fr/svn/labo/projects/zope/Portfolio/trunk/skins/mosaique.js $
*/

var Mosaique;

(function(){

var hiddenTilesNumber = 1;
var batchSize = 5;
var reNb = /\-?\d+/	;

Mosaique = function(screenArea, imgUrlBase, margins) {
	this.screenArea = screenArea;
	if (!margins)
		margins = {'top':0, 'right':0, 'bottom':0, 'left':0};
	this.margins = margins;
	this.prepareScreen();
	this.setContainerPosition(new Point(0,0));
	
	this.imgUrlBase = imgUrlBase;
	this.xmlPath = imgUrlBase + "/tiling_infos.xml";
	
	this.tiles = null;
	this.xTileRange = [0,0];
	this.yTileRange = [0,0];
	
	this._loadingQueue = new Array();
	this._currentSequence = null;
	this._loadingIterator = 0;
	this.loadingState = 0;
	
	this.dragInProgress = false;
	this.initialClickPoint = null;
	this.initialPosition = null;
	var thisMos = this;
	this._ddHandlers = {'down' : function(evt){thisMos._mouseDownHandler(evt);},
						'move' : function(evt){thisMos._mouseMoveHandler(evt);},
						'up' :   function(evt){thisMos._mouseUpHandler(evt);}};
	this.ddHandlers = null;
	
	this.rcsItoC = new Point(0, 0); // vector to translate coordinate systems
	
	this.remainD = new Point(0, 0);
	this.getXmlInfo();
}

Mosaique.prototype.getXmlInfo = function() {
	var req = new XMLHttpRequest();
	var thisMosaique = this;
	req.onreadystatechange = function() {
		if(req.readyState == 4)
			thisMosaique._loadXmlInfo(req);
	};
	req.open("GET",thisMosaique.xmlPath,true);
	req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
	req.send(null);
};

Mosaique.prototype._loadXmlInfo = function(req) {
	var doc = req.responseXML.documentElement;
	this.thumbnailWidth = parseInt(doc.getElementsByTagName('thumbnailwidth')[0].firstChild.data);
	this.thumbnailHeight = parseInt(doc.getElementsByTagName('thumbnailheight')[0].firstChild.data);
	this.tileSize = parseInt(doc.getElementsByTagName('tilesize')[0].firstChild.data);

	var zoomList = doc.getElementsByTagName("zoom");
	this.zoomTable = new Array(zoomList.length);
	var zoom;
	for (var i=0 ; i<zoomList.length ; i++) {
		zoom = zoomList[i];
		var zoomInfo = new Object();
		zoomInfo['level'] = parseInt(zoom.getAttribute('zoomlevel'));
		zoomInfo['width'] = parseInt(zoom.getElementsByTagName('width')[0].firstChild.data);
		zoomInfo['height'] = parseInt(zoom.getElementsByTagName('height')[0].firstChild.data);
		zoomInfo['tilesX'] = parseInt(zoom.getElementsByTagName('tilesx')[0].firstChild.data);
		zoomInfo['tilesY'] = parseInt(zoom.getElementsByTagName('tilesy')[0].firstChild.data);
		this.zoomTable[i] = zoomInfo;
	}
	
	this.setCurrentDimensionValues(this.getBestFitZoom());
	this.navigateur = new Navigateur(this);
	
	this.prepareContainer();
	this.setLoadingOrder();
	var ulc = new Point(this.imageWidth/2 - this.screenWidth/2, 0);
	if (this.screenHeight >= this.imageHeight)
		ulc.y = this.imageHeight/2 - this.screenHeight/2;
	this.loadScreen(ulc);
	this.addEventListeners();
};

Mosaique.prototype.addEventListeners = function() {
	var thisMos = this;
	addListener(document, 'mousedown', function(evt){thisMos.mouseDownHandler(evt);}, 'mosaique.dd');
	addListener(document, 'mouseup', function(evt){thisMos.mouseUpHandler(evt);}, 'mosaique.dd');
};

Mosaique.prototype.getBestFitZoom = function() {
	var i;
	for (i = 1 ; i < this.zoomTable.length ; i++)
		if (this.screenWidth - this.zoomTable[i]['width'] < 0 ||
			this.screenHeight - this.zoomTable[i]['height'] < 0)
				break;
	return i-1;
};

Mosaique.prototype.setCurrentDimensionValues = function(zoomIndex) {
	var zoomInfo = this.zoomTable[zoomIndex];
	this.zoomIndex = zoomIndex;
	this.zoomLevel = zoomInfo['level'];
	this.imageWidth = zoomInfo['width'];
	this.imageHeight = zoomInfo['height'];
	this.xtiles = zoomInfo['tilesX'];
	this.ytiles = zoomInfo['tilesY'];
	this.halfTileSize = this.tileSize / 2;
	this.gridWidth = this.xtiles*this.tileSize;
	this.gridHeight = this.ytiles*this.tileSize;
	
	var WnbTiles = Math.ceil(this.screenWidth/this.tileSize) + 2 * hiddenTilesNumber;
	var HnbTiles = Math.ceil(this.screenHeight/this.tileSize) + 2 * hiddenTilesNumber;
	this.WnbTiles = (WnbTiles > this.xtiles) ? this.xtiles : WnbTiles;
	this.HnbTiles = (HnbTiles > this.ytiles) ? this.ytiles : HnbTiles;
	
};

Mosaique.prototype.prepareScreen = function() {
	this.screenWidth = getObjectWidth(this.screenArea);
	this.screenHeight = getObjectHeight(this.screenArea);
	var mask = this.rootElement = document.createElement('div');
	with (mask.style) {
		position = 'absolute';
		width = this.screenWidth - this.margins['right'] + 'px';
		height = this.screenHeight - this.margins['bottom'] + 'px';
		background = base_properties["backgroundColor"];
		overflow = 'hidden';
	}
	this.screenArea.insertBefore(mask, this.screenArea.firstChild);
	
	var container = document.createElement('div');
	with (container.style) {
		position = 'absolute';
		top = '0px'; 
		left = '0px';
		cursor = 'move';
	}
	
	mask.appendChild(container);
	this.container = container;
};

Mosaique.prototype.prepareContainer = function() {
	var __getImg;
	if (browser.isGecko) {
		__getImg = function(evt, o) {
			return o;
		}
	}
	else {
		__getImg = function(evt) {
			return getTargetedObject(evt);
		}
	}
	var thisMos = this;
	var loadNext;
	if (browser.isIE) {
		loadNext = function(evt) {
			var tile = __getImg(evt, this);
			tile.style.visibility = 'visible';
			setTimeout(function(){thisMos._loadNextTile();}, 1);
		}
	}
	else {
		loadNext = function(evt) {
			var tile = __getImg(evt, this);
			tile.style.visibility = 'visible';
			thisMos._loadNextTile();
		}
	}
	
    this.setContainerPosition(new Point(0,0));
	this.xTileRange = [0,0];
	this.yTileRange = [0,0];

	var size = parseInt(this.tileSize);
	this.tiles = new Array(this.WnbTiles);
	for (var x = 0 ; x < this.WnbTiles ; x++) {
		this.tiles[x] = new Array(this.HnbTiles);
		for (var y = 0 ; y < this.HnbTiles ; y++) {
			var img = document.createElement("img");
			with(img) {
				width = size;
				height = size;
			}
			addListener(img, 'load', loadNext, 'mosaique.tiles');
			img.style.position='absolute';
			img.style.left = x * size + 'px';
			img.style.top = y * size + 'px';
			this.container.appendChild(img);
			this.tiles[x][y] = img;
		}
	}
	with(this.container.style) {
		width = size * this.WnbTiles + 'px';
		height = size * this.HnbTiles + 'px';
	}
};


Mosaique.prototype.setContainerPosition = function(point) {
	with(this.container.style) {
		left = point.x + 'px';
		top = point.y + 'px';
	}
};

Mosaique.prototype.getContainerPosition = function() {
	var x = parseInt(this.container.style.left);
	var y = parseInt(this.container.style.top);
	var p = new Point(x, y);
	return p;
};

Mosaique.prototype.setImagePosition = function(point) {
	this.setContainerPosition(this.rcsItoC.diff(point));
};

Mosaique.prototype.getImagePosition = function() {
    var cp = this.getContainerPosition();
    return this.rcsItoC.diff(cp);
};

Mosaique.prototype.getImageCenterPosition = function() {
    var ip = this.getImagePosition();
    return ip.add(new Point(this.screenWidth/2, this.screenHeight/2));
}



Mosaique.prototype.loadScreen = function(position) {
	
	var tSize = this.tileSize;
	var ulTileCoord = new Point(Math.floor(position.x/tSize), Math.floor(position.y/tSize));
	var modulo = new Point(position.x % tSize, position.y % tSize);
	
	var adjX, adjY;
	if (modulo.x > this.halfTileSize) {
		adjX = function(n){return n - hiddenTilesNumber + 1;};
		this.remainD.x = - (modulo.x - tSize);
	}
	else {
		adjX = function(n){return n - hiddenTilesNumber;};
		this.remainD.x = - modulo.x;
	}
	if (modulo.y > this.halfTileSize) {
		this.remainD.y = - (modulo.y - tSize);
		adjY = function(n){return n - hiddenTilesNumber + 1;};
	}
	else {
		adjY = function(n){return n - hiddenTilesNumber;};
		this.remainD.y = - modulo.y;
	}
		
	var xTileRange, yTileRange;
	xTileRange = [ulTileCoord.x, ulTileCoord.x + this.WnbTiles].map(adjX);
	yTileRange = [ulTileCoord.y, ulTileCoord.y + this.HnbTiles].map(adjY);

	//console.assert(xTileRange[1] - xTileRange[0] == this.WnbTiles, xTileRange, this.WnbTiles);
	//console.assert(yTileRange[1] - yTileRange[0] == this.HnbTiles, yTileRange, this.HnbTiles);
	
	if (xTileRange[0] < 0) {
		this.remainD.x = Math.abs(xTileRange[0] + ((position.x < 0) ? 1 : 0)) * tSize - modulo.x;
		xTileRange = [0, this.WnbTiles];
	}
	else if (xTileRange[1] > this.xtiles) {
		this.remainD.x = - (xTileRange[1] - this.xtiles - ((modulo.x > this.halfTileSize) ? 1 : 0)) * tSize - modulo.x;
		xTileRange = [this.xtiles - this.WnbTiles, this.xtiles];
	}

	if (yTileRange[0] < 0) { 
		this.remainD.y = Math.abs(yTileRange[0] + ((position.y < 0) ? 1 : 0)) * tSize - modulo.y;
		yTileRange = [0, this.HnbTiles];
	}
	else if (yTileRange[1] > this.ytiles) {
		this.remainD.y = - (yTileRange[1] - this.ytiles - ((modulo.y > this.halfTileSize) ? 1 : 0)) * tSize - modulo.y;
		yTileRange = [this.ytiles - this.HnbTiles, this.ytiles];
	}
		
	//console.assert(xTileRange[0] >= 0 && xTileRange[1] <= this.xtiles);
	//console.assert(yTileRange[0] >= 0 && yTileRange[1] <= this.ytiles);
	
	var dTx = this.xTileRange[0] - xTileRange[0];
	var dTy = this.yTileRange[0] - yTileRange[0];
	this.rcsItoC = this.rcsItoC.diff(new Point(dTx * tSize, dTy * tSize));

	this.setImagePosition(position);
	
	this.xTileRange = xTileRange;
	this.yTileRange = yTileRange;
	
	var xOffset = this.xTileRange[0];
	var yOffset = this.yTileRange[0];

	var baseUrl = this.imgUrlBase + '/getTile?zoom=' + this.zoomLevel / 100.0;
	
	var tilesSrc = new Array(this.WnbTiles);
	for (var x = 0 ; x < this.WnbTiles ; x++) {
		tilesSrc[x] = new Array(this.HnbTiles);
		for (var y = 0 ; y < this.HnbTiles ; y++) {
			tilesSrc[x][y] = baseUrl + '&x=' + (x + xOffset) + '&y=' + (y + yOffset);
		}
	}
	this.queueLoadingSequence({'type':'full',
							   'src' : tilesSrc,
							   'order' : this.loadingOrder,
							   'length' : this.loadingOrder.length});
};

Mosaique.prototype.loadColumns = function(n){
	/*
	n > 0 <=> x displacement > 0 => shift columns from right to left
	Returns the number of columns that have been loaded.
	*/
	
	
	var newRange;
	
	while (n != 0) {
		newRange = [this.xTileRange[0] - n, this.xTileRange[1] - n]
		if (newRange[0]<0 || newRange[1] > this.xtiles) {
			(n>0) ? n-- : n++;
			continue;
		}
		else {
			this.xTileRange = newRange;
			break;
		}
		return 0;
	}
	

    var shift, from, to, increment;
    if (n>0) {
    	shift = - (this.WnbTiles) * this.tileSize;
    	from = 0;
    	to = n;
    	increment = 1;
    }
    else {
    	shift = (this.WnbTiles) * this.tileSize;
    	from = this.WnbTiles - 1;
    	to = this.WnbTiles + n -1;
    	increment = -1;
    }

	var thisMos = this;
	var beforeSequence = function(){thisMos._shiftColumns(n, shift, from, to, increment)};

	var order = new Array();
	var tilesSrc = new Array();
	var baseUrl = this.imgUrlBase + '/getTile?zoom=' + this.zoomLevel / 100.0;
	var xOffset = this.xTileRange[0];
	var yOffset = this.yTileRange[0];

	for (var x = from ; x != to ; x += increment) {
		tilesSrc[x] = new Array();
	    for (var y = 0 ; y < this.HnbTiles ; y++) {
			order.push([x, y]);
			tilesSrc[x][y] = baseUrl + '&x=' + (x + xOffset) + '&y=' + (y + yOffset);
		}
	}
	order = order.reverse();
	this.queueLoadingSequence({'type':'column',
							   'src' : tilesSrc,
							   'order' : order,
							   'length' : order.length,
							   'beforeSequence' : beforeSequence});
	return n;
};

Mosaique.prototype._shiftColumns = function(n, shift, from, to, increment){
	/* rows rotations */
	this.tiles = rotateArray(this.tiles, -n);
	var tile, left;
	/* positional shifting */
	for (var x = from ; x != to ; x += increment) {
		left = parseInt(this.tiles[x][0].style.left);
		for (var y = 0 ; y < this.HnbTiles ; y++) {
		    var tile = this.tiles[x][y];
			tile.style.left = left + shift + 'px';
			tile.style.visibility = 'hidden';
		}
	}
	
};



Mosaique.prototype.loadRows = function(n) {
	/*
	n > 0 <=> y displacement > 0 => shift rows from bottom to top
	Returns the number of rows that's have been loaded.
	*/

	var newRange;
	
	while (n != 0) {
		newRange = [this.yTileRange[0] - n, this.yTileRange[1] - n]
		if (newRange[0]<0 || newRange[1] > this.ytiles) {
			(n>0) ? n-- : n++;
			continue;
		}
		else {
			this.yTileRange = newRange;
			break;
		}
		return 0;
	}

	var shift, from, to, increment;
	if (n>0) {
		shift = - (this.HnbTiles) * this.tileSize;
		from = 0;
		to = n;
		increment = 1;
	}
	else {
		shift = (this.HnbTiles) * this.tileSize;
		from = this.HnbTiles - 1;
		to = this.HnbTiles + n -1;
		increment = -1;
	}
	
	var thisMos = this;
	var beforeSequence = function(){thisMos._shiftRows(n, shift, from, to, increment)};

	var order = new Array();
	var tilesSrc = new Array();
	var baseUrl = this.imgUrlBase + '/getTile?zoom=' + this.zoomLevel / 100.0;
	var xOffset = this.xTileRange[0];
	var yOffset = this.yTileRange[0];
	
	for (var y = from ; y != to ; y += increment) {
		for (var x = 0 ; x < this.WnbTiles ; x++) {
			order.push([x, y]);
			if (!tilesSrc[x])
				tilesSrc[x] = new Array();
			tilesSrc[x][y] = baseUrl + '&x=' + (x + xOffset) + '&y=' + (y + yOffset);
		}
	}
	order = order.reverse();
	this.queueLoadingSequence({'type':'row',
							   'src' : tilesSrc,
							   'order' : order,
							   'length' : order.length,
							   'beforeSequence' : beforeSequence});
	return n;
};

Mosaique.prototype._shiftRows = function(n, shift, from, to, increment) {
	/* columns rotations */
	for (var x = 0 ; x < this.WnbTiles ; x++)
		this.tiles[x] = rotateArray(this.tiles[x], -n);

	var tile, top;
	
	/* positional shifting */
	for (var y = from ; y != to ; y += increment) {
		top = parseInt(this.tiles[0][y].style.top);
		for (var x = 0 ; x < this.WnbTiles ; x++) {
		    var tile = this.tiles[x][y];
			tile.style.top = top + shift + 'px';
			tile.style.visibility = 'hidden';
		}
	}
};


Mosaique.prototype.queueLoadingSequence = function(sequenceInfo) {
    if(!sequenceInfo.length) return;
	this._loadingQueue.push(sequenceInfo);
	if (!this.loadingState && this._loadingQueue.length)
		this._loadNextSequence();
};

Mosaique.prototype._loadNextSequence = function() {
	var seq = this._loadingQueue.shift();
	if (seq == null) {
		this._loadingQueue = new Array();
		this.loadingState = 0;
		return;
	}
	switch(seq['type']) {
		case 'full' :
			this.loadingState = 1;
			break;
		case 'row' :
		case 'column' :
			this.loadingState = 2;
			seq['beforeSequence']();
			break;
	}
	this._loadingIterator = 0;
	//this._loadNextTile();
	this._startSequence(seq);
};

Mosaique.prototype._startSequence = function(seq) {
	this._currentSequence = seq;
	
	var size = Math.min(batchSize, this._currentSequence.length);
	this._loadingIterator += size;

	var coord, src, tile;
	for (var i=0 ; i<size ; i++) {
		coord = this._currentSequence.order[i];
		src = this._currentSequence.src[coord[0]][coord[1]];
		tile = this.tiles[coord[0]][coord[1]];
		tile.src = src;
	}
};

Mosaique.prototype._loadNextTile = function() {
	if (this.loadingState == 0)
		return;
	else if (this._loadingIterator >= this._currentSequence['length']) {
		this._loadNextSequence();
		return;
	}
		
	var coord = this._currentSequence.order[this._loadingIterator];
	this._loadingIterator++;
	
	var src = this._currentSequence.src[coord[0]][coord[1]];
	var tile = this.tiles[coord[0]][coord[1]];
	tile.src = src;
};


/* drag and drop generic handlers */
Mosaique.prototype.mouseDownHandler = function(evt) {
	var target = getTargetedObject(evt);
	if (target.tagName == 'INPUT' || target.tagName == 'TEXTAREA')
		return;
	disableDefault(evt);
	evt = getEventObject(evt);
	var navDisp = this.navigateur.display;
	
	if (target.parentNode.parentNode == navDisp) {
		if (target == this.navigateur.frame.firstChild)
			this.ddHandlers = this.navigateur._ddHandlers;
		else {
			this.ddHandlers = null;
			return;
		}
	}
	else
		this.ddHandlers = this._ddHandlers;
	
	addListener(document, 'mousemove', this.ddHandlers['move'], 'mosaique.dd');
	
	this.ddHandlers['down'](evt);
};

Mosaique.prototype.mouseUpHandler = function(evt) {
	if (this.ddHandlers != null) {
		removeListener(document, 'mousemove', this.ddHandlers['move']);
		this.ddHandlers['up'](evt);
		this.ddHandlers = null;
	}
}


/* Mosaique drag and drop handlers */
Mosaique.prototype._mouseDownHandler = function(evt) {
	this.initialClickPoint = new Point(evt.clientX, evt.clientY);
	this.initialPosition = this.getContainerPosition();
	this.rShift = 0;
	this.cShift = 0;
	this.dragInProgress = true;
}

Mosaique.prototype._mouseMoveHandler = function(evt) {
	disableDefault(evt);
	if(!this.dragInProgress)
		return;
	
	evt = getEventObject(evt);
	var currentPoint = new Point(evt.clientX, evt.clientY);
	var displacement = currentPoint.diff(this.initialClickPoint);
	this.setContainerPosition(this.initialPosition.add(displacement));
	
	var r = (displacement.y + this.halfTileSize + this.remainD.y) / this.tileSize;
	r = Math.floor(r);
	
	if (this.rShift - r != 0)
		this.rShift += this.loadRows(r - this.rShift);

	var c = (displacement.x + this.halfTileSize + this.remainD.x) / this.tileSize;
	c = Math.floor(c);
	
	if (this.cShift - c != 0)
		this.cShift += this.loadColumns(c - this.cShift);
};

Mosaique.prototype._mouseUpHandler = function(evt) {
	this.dragInProgress = false;
	evt = getEventObject(evt);
	this._mouseMoveHandler(evt);
	var finalPoint = new Point(evt.clientX, evt.clientY);
	var displacement = finalPoint.diff(this.initialClickPoint);
	this.remainD = this.remainD.add(new Point(displacement.x - this.cShift * this.tileSize, displacement.y - this.rShift * this.tileSize));
	this.navigateur.alignFrame();
};



Mosaique.prototype.setLoadingOrder = function() {
    var startX = 0;
    var stopX = this.WnbTiles;
    var startY = 0;
    var stopY = this.HnbTiles;
    var x = 0, y = 0;
    var order = new Array();

    var direction=0;
    

    while((startX != stopX) && (startY != stopY)) {
        switch(direction) {
            case 0 : // left -> right
                startY++;
                
                for (x = startX ; x < stopX ; x++)
                    order.push([x, y]);
                x--;
                break;

            case 1 : // up -> bottom
                stopX--;
                for (y = startY ; y < stopY ; y++)
                    order.push([x, y]);
                y--;
                break;

            case 2 : // right -> left
                stopY--;
                
                for (x = stopX-1 ; x >= startX ; x--)
                    order.push([x, y])
                x++;
                break;
                
            case 3 : // bottom -> up
                startX++;
                
                for (y = stopY-1 ; y >= startY ; y--)
                    order.push([x,y]);
                y++;
                break;
        }

        direction++;
        if (direction % 4 == 0)
            direction = 0;
    }
    
    
    this.loadingOrder = order.reverse();
}

Mosaique.prototype.cleanContainer = function() {
	removeGroupListeners('mosaique.tiles');
	while (this.container.childNodes[0])
		this.container.removeChild(this.container.childNodes[0]);
}

Mosaique.prototype.loadZoomLevel = function(zoomIndex) {
	this.loadingState = 0;
	var oldWnbTiles = this.WnbTiles;
	var oldHnbTiles = this.HnbTiles;
	var oldCenter = this.getImageCenterPosition();
	var zoomInfo = this.zoomTable[zoomIndex];
	var newLevel = zoomInfo['level'];

	// center coordinates translated into target zoom level
	var center = oldCenter.mul(newLevel / this.zoomLevel);
	var ulc = center.diff(new Point(this.screenWidth/2, this.screenHeight/2)); // upper left corner
	
	this.setCurrentDimensionValues(zoomIndex);
	
	if (oldWnbTiles != this.WnbTiles || oldHnbTiles != this.HnbTiles) {
		this.cleanContainer();
		this.prepareContainer();
		this.rcsItoC = new Point(0,0);
		this.setLoadingOrder();
	}
	this.loadScreen(ulc);
};

Mosaique.prototype.unload = function() {
	this.navigateur.unload();
	removeGroupListeners('mosaique.dd');
	removeGroupListeners('mosaique.tiles');
	this.screenArea.removeChild(this.rootElement);
};

})();

/* UTILS */
function Point(x, y) {
	this.x = Math.round(x);
	this.y = Math.round(y);
}
Point.prototype.diff = function(point) { return new Point(this.x - point.x, this.y - point.y); };
Point.prototype.add = function(point) { return new Point(this.x + point.x, this.y + point.y); };
Point.prototype.mul = function(k) { return new Point(this.x * k, this.y *k)};
Point.prototype.toString = function() { return "(" + String(this.x) + ", " + String(this.y) + ")"; };

function rotateArray(t, n) {
	return t.slice(n,t.length).concat(t.slice(0,n));
}

if (!Array.prototype.map) {
	Array.prototype.map = function(f) {
		var r = new Array(this.length);
		for (var i = 0 ; i < this.length ; i++ ){
			r[i] = f(this[i]);
		}
		return r;
	};
}

