<!-- Begin

// Copyright (c) 2007-2008 Hydrix except otherwise noted. All rights reserved
// www.hydrix.com

// ---------------------------------------------------------------------------
// UTILITIES
// ---------------------------------------------------------------------------

// Find an element by its id.
function id(x) { if (typeof x == "string") return document.getElementById(x); else return x; }

// Get mouse pointer coordinates relative to the page. Should be cross-browser compatible.
function mouseX(evt)
{
	if (evt.pageX) {
		return evt.pageX;
	} else if (evt.clientX) {
		return evt.clientX + (document.documentElement.scrollLeft ?
			document.documentElement.scrollLeft :
			document.body.scrollLeft);
	} else {
		return null;
	}
}
function mouseY(evt)
{
	if (evt.pageY) {
		return evt.pageY;
	} else if (evt.clientY) {
		return evt.clientY + (document.documentElement.scrollTop ?
			document.documentElement.scrollTop :
			document.body.scrollTop);
	} else {
		return null;
	}
}

function roundNumber(x,digit) {
	if (x > 8191 && x < 10485) {
		x = x-5000;
		x = Math.round(x*Math.pow(10,digit))/Math.pow(10,digit);
		x = x+5000;
	} else {
		x = Math.round(x*Math.pow(10,digit))/Math.pow(10,digit);
	}
	return x;
}

function urlencode(str)
{
	str = encodeURIComponent(str);
	//str = str.replace('+', '%2B');
	//str = str.replace('%20', '+');
	//str = str.replace('*', '%2A');
	//str = str.replace('/', '%2F');
	//str = str.replace('@', '%40');
	return str;
}

// ---------------------------------------------------------------------------
// INITIAL PROCESSING
// ---------------------------------------------------------------------------

var g_loaded = false;
var g_block = null;
var g_block2 = null;
var g_viewer = null;
var g_roundx, g_roundy;

var gc_reln = null;
var gc_undefined = null;
var gc_axes = null;

var g_nmin = null;
var g_nmax = null;
var g_ncurrent = null;
var g_slice_images = null;
var g_current_slice = -1;
var g_number_slices = 9;
var g_slice_scale = new Scale125();
g_slice_scale.setSize(g_number_slices, false);
g_slice_scale.setBorders(0, 8);
var g_slice_values = g_slice_scale.getArray();


jQuery.fn.valFloat = function()
{
	return parseFloat(this.val());
};
jQuery.fn.valInt = function()
{
	return parseInt(this.val());
};

var g_graphwindow = new GraphWindow();
$(function()
{	// This is run when the main document is ready (though images may not all be loaded yet).
	if (gc_is_relation_valid)
	{
		g_loaded = true;
		g_block = id('block');
		g_block2 = id('block2');
		g_viewer = id('viewer');

		gc_reln = $('#reln').val();
		gc_undefined = $('#undefined').is(':checked');
		gc_axes = $('#axes').is(':checked');
		g_graphwindow.setBorders(
			$('#xmin').valFloat(), $('#xmax').valFloat(),
			$('#ymin').valFloat(), $('#ymax').valFloat());
		if (gc_slices)
		{
			g_nmin = $('#nmin').valInt();
			g_nmax = $('#nmax').valInt();
			g_ncurrent = $('#ncurrent').valInt();
			g_slice_scale.setBorders(g_nmin, g_nmax);
			g_slice_values = g_slice_scale.getArray();
		}

		// Display initial graph.
		transitionTo(g_graphwindow, true);
		if (gc_slices && g_ncurrent != -1)
		{
			setSlice(g_ncurrent);
		}

		// Set event handlers.
		$('#clear').click(function(event){
			event.preventDefault(); // Don't submit form.
			resetGraphWindow();
		});
		$('#undefined').click(function(event){
			$("form").submit();
		});
		$('#axes').click(function(event){
			$("form").submit();
		});
		$('#panup').click(function(event){
			event.preventDefault(); // Don't submit form.
			panUp();
		});
		$('#panleft').click(function(event){
			event.preventDefault(); // Don't submit form.
			panLeft();
		});
		$('#panright').click(function(event){
			event.preventDefault(); // Don't submit form.
			panRight();
		});
		$('#pandown').click(function(event){
			event.preventDefault(); // Don't submit form.
			panDown();
		});
		$('#zoomplus').click(function(event){
			event.preventDefault(); // Don't submit form.
			zoomPlus();
		});
		$('#zoomminus').click(function(event){
			event.preventDefault(); // Don't submit form.
			zoomMinus();
		});

		$('#viewer')
			.hover(viewer_mouse_in, viewer_mouse_out)
			.mousedown(viewer_mouse_down)
			.dblclick(recenterAndZoom);

		$('.slice_selector')
			.hover(function(){inSlice(this.id.charAt(0));}, function(){outSlice(this.id.charAt(0));})
			.click(function(){drillSlice(this.id.charAt(0));});
		$('#upslice').click(function(event){
			event.preventDefault(); // Don't submit form.
			drillSlice(-1);
		});
		$('#sliceleft').click(function(event){
			event.preventDefault(); // Don't submit form.
			moveSlice(-4);
		});
		$('#sliceright').click(function(event){
			event.preventDefault(); // Don't submit form.
			moveSlice(4);
		});
	}
});


function unselectSlice()
{
	if (gc_slices)
	{
		if (g_current_slice != -1)
		{
			var slice = id(g_current_slice + 'slice');
			slice.style.backgroundColor = 'black';
			slice.style.color = 'white';
		}
		g_current_slice = -1;
	}
}

function setSlice(s)
{
	if (gc_slices)
	{
		if (s != g_current_slice)
		{
			unselectSlice();

			var slice = id(s + 'slice');
			slice.style.backgroundColor = 'white';
			slice.style.color = 'black';

			g_current_slice = s;

			id('ncurrent').value = s;
			var link = id('current_url');
			var url = link.href;
			url = url.replace(/ncurrent=[-+]?[-+0-9.eE]+/g, 'ncurrent=' + s);
			link.href = url;

			var i = 0;
			for (var row = 0; row < number_tiles_y; row++)
			{
				for (var column = 0; column < number_tiles_x; column++)
				{
					var tile = id('tile_' + (top_indexy - row) + '_' + (left_indexx + column));
					tile.src = g_slice_images[s][row][column].src;
					i++;
				}
			}
		}
	}
	return false;
}
function inSlice(s)
{
	return setSlice(s);
}
function outSlice(s)
{
	return false;
}

function multiplyString(str, num)
{
	var acc = [];
	for (var i = 0; (1 << i) <= num; i++)
	{
		if ((1 << i) & num)
		{
			acc.push(str);
		}
		str += str;
	}
	return acc.join("");
}

var slice_generation_number = 0;
var slice_loading_progress = null;

function startLoadingTiles()
{
	slice_generation_number++;

	slice_loading_progress = new Array(g_number_slices);
	for (var s = 0; s < g_number_slices; s++)
	{
		slice_loading_progress[s] = 0;
		id(s + 'slice').innerHTML = g_slice_values[s] + '<br />' + dots;
	}

	return slice_generation_number;
}

var dots = multiplyString('.', number_tiles_x * number_tiles_y);

function loadTile(this_slice_generation_number, slice, row, column, src)
{
	image = g_slice_images[slice][row][column];
	image.onload = slice_tile_onload;
	image.src = src;
	
	function slice_tile_onload()
	{
		if (this_slice_generation_number == slice_generation_number)
		{	// Still loading current tiles (otherwise it's an old tile, just ignore).
			slice_loading_progress[slice]++;
			var left = number_tiles_x * number_tiles_y - slice_loading_progress[slice];
			if (left > 0)
			{
				id(slice + 'slice').innerHTML = g_slice_values[slice] + '<br />' + dots.slice(0, left);
			}
			else
			{
				id(slice + 'slice').innerHTML = g_slice_values[slice];
			}
		}
	}
}

function preloadSlices(gw)
{
	if (gc_slices)
	{
		if (g_slice_images == null)
		{
			g_slice_images = new Array(g_number_slices);
			for (var s = 0; s < g_number_slices; s++)
			{
				g_slice_images[s] = new Array(number_tiles_y);
				for (row = 0; row < number_tiles_y; row++)
				{
					g_slice_images[s][row] = new Array(number_tiles_x);
					for (column = 0; column < number_tiles_x; column++)
					{
						g_slice_images[s][row][column] = new Image();
					}
				}
			}
		}
		else
		{
			for (var s = 0; s < g_number_slices; s++)
			{
				for (var row = 0; row < number_tiles_y; row++)
				{
					for (var column = 0; column < number_tiles_x; column++)
					{
						image = g_slice_images[s][row][column]
						image.onload = null;
						image.src = '';
					}
				}
			}
		}
	}

	var gen = startLoadingTiles();

	var img_src_prefix1 = 'tile.php?reln=';
	var img_src_prefix2 = '&scalex=' + gw.x.getUnitPerPixel() + '&scaley=' + gw.y.getUnitPerPixel();
	if (gc_undefined) { img_src_prefix2 += '&undefined=1'; }
	if (gc_axes) { img_src_prefix2 += '&axes=1'; }
	for (var s = 0; s < g_number_slices; s++)
	{
		var img_src_prefix = img_src_prefix1 + urlencode(gc_reln.replace(/\bn\b/ig, g_slice_values[s])) + img_src_prefix2;
		for (var row = 0; row < number_tiles_y; row++)
		{
			var img_src_row = img_src_prefix + '&indexy=' + (top_indexy - row);
			for (var column = 0; column < number_tiles_x; column++)
			{
				var src = img_src_row + '&indexx=' + (left_indexx + column);
				loadTile(gen, s, row, column, src);
			}
		}
	}
}

function recalculateSlices()
{
	g_slice_values = g_slice_scale.getArray();
	for (var k = 0; k < g_number_slices; k++)
	{
		id(k + 'slice').innerHTML = g_slice_values[k];
	}

	id('nmin').value = g_slice_values[0];
	id('nmax').value = g_slice_values[g_number_slices - 1];
	var link = id('current_url');
	var url = link.href;
	url = url.replace(/nmin=[-+]?[-+0-9.eE]+/g, 'nmin=' + g_slice_values[0]);
	url = url.replace(/nmax=[-+]?[-+0-9.eE]+/g, 'nmax=' + g_slice_values[g_number_slices - 1]);
	link.href = url;

	preloadSlices(g_graphwindow);
}

function moveSlice(diff)
{
	if (gc_slices)
	{
		unselectSlice();

		var s = g_current_slice;
		g_slice_scale.translatePoints(diff);

		recalculateSlices();

		setSlice(s);
	}
	return false;
}

function drillSlice(s)
{
	if (gc_slices)
	{
		unselectSlice();

		if (s == -1)
		{	// Drill out.
			g_slice_scale.zoom(1);
		}
		else
		{	// Drill in.
			g_slice_scale.zoom(-1);
			g_slice_scale.setCenter(g_slice_values[s]);
		}

		recalculateSlices();

		s = Math.round(g_number_slices / 2 - 1);
		setSlice(s);
	}
	return false;
}

// ---------------------------------------------------------------------------
// MOUSE HANDLING
// ---------------------------------------------------------------------------

var g_mouse_in_viewer = false;
// Current coordinates of the mouse in the viewer, always up to date.
var g_mouse_x_viewer = null, g_mouse_y_viewer = null;
var g_prevent_coord = false;
// 'const' doesn't exist in IE jscript!
var MOUSE_OUT = 0,
	MOUSE_IN = 1,
	MOUSE_DOWN_IN = 2,
	MOUSE_DOWN_OUT = 3,
	MOUSE_DRAGGING_IN = 4,
	MOUSE_DRAGGING_OUT = 5,
	MOUSE_WAIT_IN = 6,
	MOUSE_WAIT_OUT = 7;
var mouse_state = MOUSE_OUT;
var mouse_pointer = ['auto', 'crosshair', 'crosshair', 'crosshair', 'move', 'move', 'wait', 'wait'];

// Event handler for mouse wheel event.
function wheel_move(event)
{
	var delta = 0;
	if (!event) { event = window.event; } // For IE

	if (event.wheelDelta)
	{	// IE/Opera.
		delta = event.wheelDelta/120;
		// In Opera 9, delta differs in sign as compared to IE.
		if (window.opera)
		{
			delta = -delta;
		}
	} else if (event.detail)
	{	// In Mozilla, sign of delta is different than in IE. Also, delta is multiple of 3.
		delta = -event.detail;
	}
	// If delta is nonzero, handle it.
	// Basically, delta is now positive if wheel was scrolled up, and negative, if wheel was scrolled down.
	if (delta)
	{
		if (g_mouse_in_viewer)
		{
			zoomAt(delta > 0);
		}
		// Prevent default actions caused by mouse wheel.
		if (event.preventDefault)
		{
			event.preventDefault();
		}
	}

	event.returnValue = false;
}

function setMouseState(state)
{
	mouse_state = state;
	document.body.style.cursor = mouse_pointer[state];
	if (mouse_state == MOUSE_IN)
	{	// Capture next mouse moves.
		document.onmousemove = viewer_mouseup_move;

		if (window.addEventListener)
		{
			// DOMMouseScroll is for mozilla.
			window.addEventListener('DOMMouseScroll', wheel_move, false);
		}
		// IE/Opera.
		window.onmousewheel = document.onmousewheel = wheel_move;
	}
	else if (mouse_state == MOUSE_OUT)
	{	// Stop capturing mouse moves when pointer is outside viewer.
		document.onmousemove = null;

		if (window.removeEventListener)
		{
			// DOMMouseScroll is for mozilla.
			window.removeEventListener('DOMMouseScroll', wheel_move, false);
		}
		// IE/Opera.
		window.onmousewheel = document.onmousewheel = null;
	}
}
function setMouseWait(wait)
{
	if (wait)
	{
		setMouseState(g_mouse_in_viewer ? MOUSE_WAIT_IN : MOUSE_WAIT_OUT);
	}
	else
	{
		setMouseState(g_mouse_in_viewer ? MOUSE_IN : MOUSE_OUT);
	}
}

function showCoordinates()
{
	if (g_mouse_in_viewer
		&& mouse_state != MOUSE_DRAGGING_IN && mouse_state != MOUSE_DRAGGING_OUT
		&& mouse_state != MOUSE_WAIT_IN && mouse_state != MOUSE_WAIT_OUT)
	{
		var coord = g_graphwindow.pixelsToUnits(g_mouse_x_viewer, g_mouse_y_viewer);

		if ($('#coordinates')
			.html('x = ' + coord.x_u + '<br>y = ' + coord.y_u)
			.is(':hidden'))
		{
			$('#viewertip').hide();
			$('#coordinates').show();
		}
	}
}

function updateMousePosition(e)
{
	if (!e) e = window.event;  // IE Event Model

	if (g_viewer)
	{
		// Coordinates of viewer in page.
		var viewer_x = 0;
		var viewer_y = 0;
		var o = g_viewer;
		while (o) {
			viewer_x += o.offsetLeft;
			viewer_y += o.offsetTop;
			o = o.offsetParent;
		}
		// Coordinates of mouse in viewer.
		g_mouse_x_viewer = mouseX(e) - viewer_x;
		g_mouse_y_viewer = mouseY(e) - viewer_y;

		if (g_mouse_x_viewer < 0 || g_mouse_y_viewer < 0 || g_mouse_x_viewer >= VIEWER_WIDTH || g_mouse_y_viewer >= VIEWER_HEIGHT)
		{	// Mouse is outside the viewer.
			// We need to check because some 'onmouseout' events may be lost when dragging in some browsers.
			viewer_mouse_out(e);
		}
	}
}

function viewer_mouseup_move(e)
{
	updateMousePosition(e);

	if (!g_prevent_coord)
	{
		showCoordinates();
	}

	return false;
}

// Mouse moves in (aka over) the viewer.
function viewer_mouse_in(e)
{
	if (!g_mouse_in_viewer)
	{
		g_mouse_in_viewer = true;

		if (mouse_state == MOUSE_DOWN_OUT)
		{
			setMouseState(MOUSE_DOWN_IN);
		}
		else if (mouse_state == MOUSE_DRAGGING_OUT)
		{
			setMouseState(MOUSE_DRAGGING_IN);
		}
		else if (mouse_state == MOUSE_WAIT_OUT)
		{
			setMouseState(MOUSE_WAIT_IN);
		}
		else
		{	// MOUSE_OUT or fallback.
			setMouseState(MOUSE_IN);
		}

		updateMousePosition(e);
		showCoordinates();
	}

	return false;
}

// Mouse moves out of the viewer.
function viewer_mouse_out(e)
{
	if (g_mouse_in_viewer && (g_mouse_x_viewer < 0 || g_mouse_y_viewer < 0 || g_mouse_x_viewer >= VIEWER_WIDTH || g_mouse_y_viewer >= VIEWER_HEIGHT))
	{
		g_mouse_in_viewer = false;

		if (mouse_state == MOUSE_DOWN_IN)
		{
			setMouseState(MOUSE_DOWN_OUT);
		}
		else if (mouse_state == MOUSE_DRAGGING_IN)
		{
			setMouseState(MOUSE_DRAGGING_OUT);
		}
		else if (mouse_state == MOUSE_WAIT_IN)
		{
			setMouseState(MOUSE_WAIT_OUT);
		}
		else
		{	// MOUSE_IN or fallback.
			setMouseState(MOUSE_OUT);
			$('#coordinates').hide();
			$('#viewertip').show();
		}
	}

	return false;
}

// This code is from the book JavaScript: The Definitive Guide, 5th Edition,
// by David Flanagan. Copyright 2006 O'Reilly Media, Inc. (ISBN #0596101996)
/**
 * Drag.js: drag absolutely positioned HTML elements.
 *
 * This module defines a single drag() function that is designed to be called
 * from an onmousedown event handler.  Subsequent mousemove events will
 * move the specified element. A mouseup event will terminate the drag.
 * If the element is dragged off the screen, the window does not scroll.
 * This implementation works with both the DOM Level 2 event model and the
 * IE event model.
 *
 * Arguments:
 *
 *   block:  the element that received the mousedown event or
 *     some containing element. It must be absolutely positioned.  Its
 *     style.left and style.top values will be changed based on the user's
 *     drag.
 *
 *   event: the Event object for the mousedown event.
 **/
function viewer_mouse_down(e)
{
	if (mouse_state == MOUSE_WAIT_IN || mouse_state == MOUSE_WAIT_OUT)
	{	// Ignore mouse button when waiting for some non-stoppable animation.
		return;
	}

	// Just in case some animation was going on...
	stopAnimation();

	if (mouse_state == MOUSE_IN)
	{
		// Stop capturing mouse moves when pointer is outside viewer.
		document.onmousemove = null;
		setMouseState(MOUSE_DOWN_IN);
	}
	else if (mouse_state == MOUSE_OUT)
	{ // Shouldn't happen! But handling it anyway.
		setMouseState(MOUSE_DOWN_OUT);
	}

	if (g_block)
	{
		// The mouse position (in window coordinates) at which the drag begins 
		var mouse_previous_x = g_mouse_x_viewer;
		var mouse_previous_y = g_mouse_y_viewer;

		// Register the event handlers that will respond to the mousemove events
		// and the mouseup event that follow this mousedown event.
		if (document.addEventListener) {  // DOM Level 2 event model
			// Register capturing event handlers
			document.addEventListener("mousemove", moveHandler, true);
			document.addEventListener("mouseup", upHandler, true);
		}
		else if (document.attachEvent) {  // IE 5+ Event Model
			// In the IE event model, we capture events by calling
			// setCapture() on the element to capture them.
			g_block.setCapture();
			g_block.attachEvent("onmousemove", moveHandler);
			g_block.attachEvent("onmouseup", upHandler);
			// Treat loss of mouse capture as a mouseup event
			g_block.attachEvent("onlosecapture", upHandler);
		}
		else {  // IE 4 Event Model
			// In IE 4 we can't use attachEvent() or setCapture(), so we set
			// event handlers directly on the document object and hope that the
			// mouse events we need will bubble up.
			var oldmovehandler = document.onmousemove; // used by upHandler()
			var olduphandler = document.onmouseup;
			document.onmousemove = moveHandler;
			document.onmouseup = upHandler;
		}

		// We've handled this event. Don't let anybody else see it.
		if (e.stopPropagation) e.stopPropagation();  // DOM Level 2
		else e.cancelBubble = true;                      // IE

		// Now prevent any default action.
		if (e.preventDefault) e.preventDefault();   // DOM Level 2
		else e.returnValue = false;                     // IE
	}

	/**
	 * This is the handler that captures mousemove events when an element
	 * is being dragged. It is responsible for moving the element.
	 **/
	function moveHandler(e)
	{
		if (!e) e = window.event;  // IE Event Model

		updateMousePosition(e);

		var x = g_mouse_x_viewer;
		var y = g_mouse_y_viewer;

		if ((x != mouse_previous_x) || (y != mouse_previous_y))
		{
			if (mouse_state == MOUSE_DOWN_IN)
			{ // First move.
				setMouseState(MOUSE_DRAGGING_IN);
			}
			else if (mouse_state == MOUSE_DOWN_OUT)
			{ // First move.
				setMouseState(MOUSE_DRAGGING_OUT);
			}

			if (g_block)
			{
				moveTo(
					g_block.offsetLeft + x - mouse_previous_x,
					g_block.offsetTop + y - mouse_previous_y);
			}
			mouse_previous_x = x;
			mouse_previous_y = y;
		}

		// And don't let anyone else see this event.
		if (e.stopPropagation) e.stopPropagation();  // DOM Level 2
		else e.cancelBubble = true;                  // IE

		return false;
	}

	/**
	 * This is the handler that captures the final mouseup event that
	 * occurs at the end of a drag.
	 **/
	function upHandler(e)
	{
		if (!e) e = window.event;  // IE Event Model

		if (g_block)
		{
			// Unregister the capturing event handlers.
			if (document.removeEventListener) {  // DOM event model
				document.removeEventListener("mouseup", upHandler, true);
				document.removeEventListener("mousemove", moveHandler, true);
			}
			else if (document.detachEvent) {  // IE 5+ Event Model
				g_block.detachEvent("onlosecapture", upHandler);
				g_block.detachEvent("onmouseup", upHandler);
				g_block.detachEvent("onmousemove", moveHandler);
				g_block.releaseCapture();
			}
			else {  // IE 4 Event Model
				// Restore the original handlers, if any
				document.onmouseup = olduphandler;
				document.onmousemove = oldmovehandler;
			}

			// And don't let the event propagate any further.
			if (e.stopPropagation) e.stopPropagation();  // DOM Level 2
			else e.cancelBubble = true;                  // IE
		}

		if (mouse_state == MOUSE_DOWN_IN)
		{
			setMouseState(MOUSE_IN);
			showCoordinates();
		}
		else if (mouse_state == MOUSE_DOWN_OUT)
		{
			setMouseState(MOUSE_OUT);
			$('#coordinates').hide();
			$('#viewertip').show();
		}
		else if (mouse_state == MOUSE_DRAGGING_IN)
		{
			setMouseState(MOUSE_IN);
			showCoordinates();
		}
		else if (mouse_state == MOUSE_DRAGGING_OUT)
		{
			setMouseState(MOUSE_OUT);
			$('#coordinates').hide();
			$('#viewertip').show();
		}

		return false;
	}

	return false;
}

function disableCoord()
{
	g_prevent_coord = true;
}
function enableCoord()
{
	g_prevent_coord = false;
	if (g_mouse_in_viewer)
	{
		showCoordinates();
	}
}


function startLoading()
{
	$('#loading_message').html(' Loading: 0%');
}
function checkLoading()
{
	var images = 0;
	var hiddens = 0;
	function countTiles(element)
	{
		if (element.tagName == 'IMG')
		{
			if (element.style.visibility == 'hidden')
			{
				hiddens++;
				images++;
			}
			else if (element.style.visibility == 'visible')
			{
				images++;
			}
		}
		else for (var i = element.childNodes.length - 1; i >= 0; i--)
		{
			countTiles(element.childNodes[i]);
		}
	}
	countTiles(g_block);

	if (hiddens > 0)
	{	// Some tiles are still loading...
		p = ((images - hiddens) / images) * 100;
		$('#loading_message').html(' Loading: ' + Math.round(p) + '%');
	}
	else
	{	// All loaded.
		// Remove loading message.
		$('#loading_message').html(' ');
		// Hide old block (so it does not appear when panning).
		g_block2.style.width = '0px';
		g_block2.style.height = '0px';
		g_block2.style.left = '-1px';
		g_block2.style.top = '-1px';
	}
}
function tile_onload()
{
	this.style.visibility = 'visible';
	checkLoading();
}

var g_log10 = Math.log(10);
function setScale(scalex, scaley)
{
	g_roundx = Math.round(1.0 - Math.log(scalex) / g_log10);;
	if (g_roundx < 0) { g_roundx = 0; }
	g_roundy = Math.round(1.0 - Math.log(scaley) / g_log10);
	if (g_roundy < 0) { g_roundy = 0; }
}

function setGraphWindow(gw)
{
	g_graphwindow = gw;
	var xmin = g_graphwindow.x.getMin();
	var xmax = g_graphwindow.x.getMax();
	var ymin = g_graphwindow.y.getMin();
	var ymax = g_graphwindow.y.getMax();

	// Update window.
	id('xmin').value = xmin;
	id('xmax').value = xmax;
	id('ymin').value = ymin;
	id('ymax').value = ymax;

	var link = id('current_url');
	var url = link.href;
	url = url.replace(/xmin=[-+]?[-+0-9.eE]+/g, 'xmin=' + xmin);
	url = url.replace(/xmax=[-+]?[-+0-9.eE]+/g, 'xmax=' + xmax);
	url = url.replace(/ymin=[-+]?[-+0-9.eE]+/g, 'ymin=' + ymin);
	url = url.replace(/ymax=[-+]?[-+0-9.eE]+/g, 'ymax=' + ymax);
	link.href = url;

	setScale(g_graphwindow.x.getUnitPerPixel(), g_graphwindow.y.getUnitPerPixel());
}

function setWindow(xmin, xmax, ymin, ymax)
{
	g_graphwindow.setBorders(xmin, xmax, ymin, ymax);
	setGraphWindow(g_graphwindow);
}

// Move the block to the given coordinates. Update tiles if necessary.
function moveTo(new_left, new_top)
{
	g_graphwindow.translatePixels(g_block.offsetLeft - new_left, new_top - g_block.offsetTop);
	setGraphWindow(g_graphwindow);

	if (gc_slices)
	{	// Slices -> Use 'transitionTo' when needing to get new tiles, so that slices are properly updated.
		function moveMore()
		{
			var gw2 = GraphWindowCopy(g_graphwindow);
			gw2.translatePixels(g_block.offsetLeft - new_left, new_top - g_block.offsetTop);
			return transitionTo(gw2, true);
		}

		if (new_left > 0)
		{	// Submap is too far to the right.
			return moveMore();
		} else if (new_left + number_tiles_x * TILE_WIDTH < VIEWER_WIDTH)
		{	// Submap is too far to the left.
			return moveMore();
		}

		if (new_top > 0)
		{	// Submap is too low.
			return moveMore();
		} else if (new_top + number_tiles_y * TILE_HEIGHT < VIEWER_HEIGHT)
		{	// Submap is too high.
			return moveMore();
		}
	}
	else
	{	// No slices -> Optimized drag, a bit smoother.
		if (new_left > 0)
		{	// Submap is too far to the right.
			// -> Recycle rightmost column to create a new column to the left.
			startLoading();
			do
			{
				var old_col = left_indexx + number_tiles_x - 1;
				left_indexx--;
				var new_col = left_indexx;
				var r = new RegExp("indexx=" + old_col, 'g');
				for (var row = 0; row < number_tiles_y; row++)
				{
					var tile_id_prefix = 'tile_' + (top_indexy - row) + '_';
					// Find rightmost tile.
					var tile_to_recycle = id(tile_id_prefix + old_col);
					// Place it on the left edge of the block.
					tile_to_recycle.style.left = '0px';
					// Change the index of the tile in the tile URL.
					src = tile_to_recycle.src;
					tile_to_recycle.onload = null;
					tile_to_recycle.style.visibility = 'hidden';
					tile_to_recycle.src = 'blank.png';
					tile_to_recycle.onload = tile_onload;
					tile_to_recycle.src = src.replace(r, 'indexx=' + new_col);
					// Change id of the tile to reflect new indexx.
					tile_to_recycle.id = tile_id_prefix + new_col;
					// Shift all other tiles right by one tile width.
					for (var col = 1; col < number_tiles_x; col++)
					{
						var tile_to_shift = id(tile_id_prefix + (left_indexx + col));
						tile_to_shift.style.left = col * TILE_WIDTH + 'px';
					}
				}
				new_left -= TILE_WIDTH;
			}
			while (new_left > 0);
		} else if (new_left + number_tiles_x * TILE_WIDTH < VIEWER_WIDTH)
		{	// Submap is too far to the left.
			// -> Recycle leftmost column to create a new column to the right.
			startLoading();
			do
			{
				var old_col = left_indexx;
				var new_col = left_indexx + number_tiles_x;
				left_indexx++;
				var r = new RegExp("indexx=" + old_col, 'g');
				for (var row = 0; row < number_tiles_y; row++)
				{
					var tile_id_prefix = 'tile_' + (top_indexy - row) + '_';
					// Find leftmost tile.
					var tile_to_recycle = id(tile_id_prefix + old_col);
					// Place it on the right edge of the block.
					tile_to_recycle.style.left = ((number_tiles_x - 1) * TILE_WIDTH) + 'px';
					// Change the index of the tile in the tile URL.
					src = tile_to_recycle.src;
					tile_to_recycle.onload = null;
					tile_to_recycle.style.visibility = 'hidden';
					tile_to_recycle.src = 'blank.png';
					tile_to_recycle.onload = tile_onload;
					tile_to_recycle.src = src.replace(r, 'indexx=' + new_col);
					// Change id of the tile to reflect new indexx.
					tile_to_recycle.id = tile_id_prefix + new_col;
					// Shift all other tiles left by one tile width.
					for (var col = 0; col < number_tiles_x - 1; col++)
					{
						var tile_to_shift = id(tile_id_prefix + (left_indexx + col));
						tile_to_shift.style.left = col * TILE_WIDTH + 'px';
					}
				}
				new_left += TILE_WIDTH;
			}
			while (new_left + number_tiles_x * TILE_WIDTH < VIEWER_WIDTH);
		}

		if (new_top > 0)
		{	// Submap is too low.
			// -> Recycle bottom row to create a new row at the top.
			startLoading();
			do
			{
				var old_row = top_indexy - number_tiles_y + 1;
				top_indexy++;
				var new_row = top_indexy;
				var r = new RegExp("indexy=" + old_row, 'g');
				for (var col = 0; col < number_tiles_x; col++)
				{
					var tile_id_prefix = 'tile_';
					var tile_id_suffix = '_' + (left_indexx + col);
					// Find bottom tile.
					var tile_to_recycle = id(tile_id_prefix + old_row + tile_id_suffix);
					// Place it on the top edge of the block.
					tile_to_recycle.style.top = '0px';
					// Change the index of the tile in the tile URL.
					src = tile_to_recycle.src;
					tile_to_recycle.onload = null;
					tile_to_recycle.style.visibility = 'hidden';
					tile_to_recycle.src = 'blank.png';
					tile_to_recycle.onload = tile_onload;
					tile_to_recycle.src = src.replace(r, 'indexy=' + new_row);
					// Change id of the tile to reflect new indexy.
					tile_to_recycle.id = tile_id_prefix + new_row + tile_id_suffix;
					// Shift all other tiles down by one tile width.
					for (var row = 1; row < number_tiles_y; row++)
					{
						var tile_to_shift = id(tile_id_prefix + (top_indexy - row) + tile_id_suffix);
						tile_to_shift.style.top = row * TILE_HEIGHT + 'px';
					}
				}
				new_top -= TILE_HEIGHT;
			}
			while (new_top > 0);
		} else if (new_top + number_tiles_y * TILE_HEIGHT < VIEWER_HEIGHT)
		{	// Submap is too high.
			// -> Recycle top row to create a new row at the bottom.
			startLoading();
			do
			{
				var old_row = top_indexy;
				var new_row = top_indexy - number_tiles_y;
				top_indexy--;
				var r = new RegExp("indexy=" + old_row, 'g');
				for (var col = 0; col < number_tiles_x; col++)
				{
					var tile_id_prefix = 'tile_';
					var tile_id_suffix = '_' + (left_indexx + col);
					// Find top tile.
					var tile_to_recycle = id(tile_id_prefix + old_row + tile_id_suffix);
					// Place it on the bottom edge of the block.
					tile_to_recycle.style.top = ((number_tiles_y - 1) * TILE_HEIGHT) + 'px';
					// Change the index of the tile in the tile URL.
					src = tile_to_recycle.src;
					tile_to_recycle.onload = null;
					tile_to_recycle.style.visibility = 'hidden';
					tile_to_recycle.src = 'blank.png';
					tile_to_recycle.onload = tile_onload;
					tile_to_recycle.src = src.replace(r, 'indexy=' + new_row);
					// Change id of the tile to reflect new indexy.
					tile_to_recycle.id = tile_id_prefix + new_row + tile_id_suffix;
					// Shift all other tiles up by one tile height.
					for (var row = 0; row < number_tiles_y - 1; row++)
					{
						var tile_to_shift = id(tile_id_prefix + (top_indexy - row) + tile_id_suffix);
						tile_to_shift.style.top = row * TILE_HEIGHT + 'px';
					}
				}
				new_top += TILE_HEIGHT;
			}
			while (new_top + number_tiles_y * TILE_HEIGHT < VIEWER_HEIGHT);
		}
	}

	g_block.style.left = new_left + "px";
	g_block.style.top = new_top + "px";
}

var timer = null;
function stopAnimation()
{
	if (timer)
	{
		clearInterval(timer); // No more frames please.
		timer = null;
		enableCoord();
	}
}

function recenterXY(x, y)
{
	// Move block so that the clicked-on pixel goes to the center of the viewer.
	var block_previous_translation_x = 0;
	var block_previous_translation_y = 0;
	var block_final_translation_x = Math.floor(VIEWER_WIDTH / 2) - x;
	var block_final_translation_y = Math.floor(VIEWER_HEIGHT / 2) - y;

	stopAnimation();
	disableCoord();

	var currentFrame = 0;
	var numberOfFrames = Math.round(ANIMATION_TIME_MS / 25);
	timer = setInterval(displayRecenterFrame, 25);

	function displayRecenterFrame()
	{
		if (timer)
		{
			currentFrame++;
			var ratio = currentFrame / numberOfFrames;
			var dx = Math.round(block_final_translation_x * ratio);
			var dy = Math.round(block_final_translation_y * ratio);
			moveTo(
				g_block.offsetLeft + dx - block_previous_translation_x,
				g_block.offsetTop + dy - block_previous_translation_y);

			if (currentFrame < numberOfFrames)
			{
				block_previous_translation_x = dx;
				block_previous_translation_y = dy;
				showCoordinates();
			}
			else
			{ // Last frame.
				clearInterval(timer); // No more frames please.
				timer = null;
				enableCoord();
			}
		}
	}

	return false;
}
function recenter(e)
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	var gw2 = GraphWindowCopy(g_graphwindow);
	gw2.translatePixels(Math.round(VIEWER_WIDTH / 2 - g_mouse_x_viewer), Math.round(VIEWER_HEIGHT / 2 - g_mouse_y_viewer));

	return transitionTo(gw2, false);
}

function panUp()
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	var gw2 = GraphWindowCopy(g_graphwindow);
	gw2.translatePixels(0, Math.round(VIEWER_HEIGHT / 4));

	return transitionTo(gw2, false);
}
function panDown()
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	var gw2 = GraphWindowCopy(g_graphwindow);
	gw2.translatePixels(0, Math.round(-VIEWER_HEIGHT / 4));

	return transitionTo(gw2, false);
}
function panLeft()
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	var gw2 = GraphWindowCopy(g_graphwindow);
	gw2.translatePixels(Math.round(-VIEWER_WIDTH / 4), 0);

	return transitionTo(gw2, false);
}
function panRight()
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	var gw2 = GraphWindowCopy(g_graphwindow);
	gw2.translatePixels(Math.round(VIEWER_WIDTH / 4), 0);

	return transitionTo(gw2, false);
}


var transition_timer = null;
function transitionTo(gw2, no_animation)
{
	if (mouse_state == MOUSE_WAIT_IN || mouse_state == MOUSE_WAIT_OUT)
	{	// Ignore mouse button when waiting for some non-stoppable animation.
		return;
	}

	stopAnimation();
	disableCoord();
	setMouseWait(true);

	var xmin = g_graphwindow.x.getMin();
	var xmax = g_graphwindow.x.getMax();
	var ymin = g_graphwindow.y.getMin();
	var ymax = g_graphwindow.y.getMax();
	var scalex = g_graphwindow.x.getUnitPerPixel();
	var scaley = g_graphwindow.x.getUnitPerPixel();
	var xmin2 = gw2.x.getMin();
	var xmax2 = gw2.x.getMax();
	var ymin2 = gw2.y.getMin();
	var ymax2 = gw2.y.getMax();

	// Functions that calculate the pixel coordinates of a point, before zoom.
	function xpx_0(x) { return g_graphwindow.x.unitsToPixels(x); }
	function ypx_0(y) { return g_graphwindow.y.unitsToPixels(y); }

	// t=0: Position of xmin2,ymax2 in screen.
	var xmin2px_0 = xpx_0(xmin2);
	var ymax2px_0 = ypx_0(ymax2);
	// t=1: Final stretch. At t=0, stretch==1.
	var sx_1 = (xmax - xmin) / (xmax2 - xmin2);
	var sy_1 = (ymax - ymin) / (ymax2 - ymin2);

	var old_left_indexx = left_indexx;
	var old_top_indexy = top_indexy;
	// Coordinates of middle of top-left pixel in block.
	block_left_x = (old_left_indexx * TILE_WIDTH - Math.floor(TILE_WIDTH / 2)) * scalex;
	block_top_y = ((old_top_indexy + 1) * TILE_HEIGHT - Math.floor(TILE_HEIGHT / 2) - 1) * scaley;
	// Initial position of block, so that given window range is what appears in viewer.
	block_left = xpx_0(block_left_x);
	block_top = ypx_0(block_top_y);

	// Calculate parameters for new graph after zoom.
	// Scale of the graph.
	var new_scalex = gw2.x.getUnitPerPixel();
	var new_scaley = gw2.y.getUnitPerPixel();
	// Index of the top-left tile.
	left_indexx = Math.floor((xmin2 / new_scalex + (Math.floor(TILE_WIDTH / 2) + 0.5)) / TILE_WIDTH);
	top_indexy = Math.floor((ymax2 / new_scaley + (Math.floor(TILE_HEIGHT / 2) + 0.5)) / TILE_HEIGHT);
	// Coordinates of middle of top-left pixel in block.
	var new_block_left_x = (left_indexx * TILE_WIDTH - Math.floor(TILE_WIDTH / 2)) * new_scalex;
	var new_block_top_y = ((top_indexy + 1) * TILE_HEIGHT - Math.floor(TILE_HEIGHT / 2) - 1) * new_scaley;
	// Initial pixel position of block, so that given window range is what appears in viewer.
	var new_block_left = xpx_0(new_block_left_x);
	var new_block_top = ypx_0(new_block_top_y);

	// Get a handle on all tiles, to facilitate work on them.
	var tiles = new Array(number_tiles_y);
	var tiles2 = new Array(number_tiles_y);
	for (var row = 0; row < number_tiles_y; row++)
	{
		var tile_row = new Array(number_tiles_x);
		tiles[row] = tile_row;
		var tile2_row = new Array(number_tiles_x);
		tiles2[row] = tile2_row;
		for (var column = 0; column < number_tiles_x; column++)
		{
			var tile = id('tile_' + (old_top_indexy - row) + '_' + (old_left_indexx + column));
			tile_row[column] = tile;
			// If old tile is not loaded by now, give up!
			tile.onload = null;
			if (tile.style.visibility == 'hidden')
			{	// Still hidden -> don't try to load it anymore.
				tile.src = '';
			}

			var tile2 = id('tile2__' + row + '_' + column)
			tile2_row[column] = tile2;
			// Hide new tiles for now.
			tile2.onload = null;
			tile2.src = 'blank.png';
			tile2.style.visibility = 'hidden';
		}
	}

	// Put new block in front of old block.
	g_block.style.zIndex = 2;
	g_block2.style.zIndex = 3;

	// Show initial "loading: 0%".
	startLoading();

	// Common image parameters -- indexx, indexy will be added for each tile.
	var img_src_prefix = 'tile.php?reln=';
	if (!gc_slices)
	{
		img_src_prefix += urlencode(gc_reln);
	}
	else
	{
		var s = g_current_slice;
		if (s == -1) { s = 0; }
		img_src_prefix += urlencode(gc_reln.replace(/\bn\b/ig, g_slice_values[s]));
	}
	img_src_prefix += '&scalex=' + new_scalex + '&scaley=' + new_scaley;
	if (gc_undefined) { img_src_prefix += '&undefined=1'; }
	if (gc_axes) { img_src_prefix += '&axes=1'; }
	for (var row = 0; row < number_tiles_y; row++)
	{
		tile_row = tiles[row];
		tile2_row = tiles2[row];
		var img_src_row = img_src_prefix + '&indexy=' + (top_indexy - row);

		for (var column = 0; column < number_tiles_x; column++)
		{
			var tile = tile_row[column];
			var tile2 = tile2_row[column];
			// Set final ids now.
			tile.id = 'tile2__' + row + '_' + column;
			var tile_id = 'tile_' + (top_indexy - row) + '_' + (left_indexx + column);
			tile2.id = tile_id;

			// Show it once it is loaded.
			tile2.onload = tile_onload;
			// Start loading the image.
			tile2.src = img_src_row + '&indexx=' + (left_indexx + column);
		}
	}

	if (gc_slices)
	{
		preloadSlices(gw2);
	}

	var currentFrame = 0;
	var numberOfFrames = 1;
	if (!no_animation)
	{
		numberOfFrames = Math.round(ANIMATION_TIME_MS / 25);
		transition_timer = setInterval(displayTransitionFrame, 25);
	}
	else
	{
		displayTransitionFrame();
	}

	function displayTransitionFrame()
	{
		if (transition_timer || no_animation)
		{
			currentFrame++;
			var ratio = currentFrame / numberOfFrames;

			// Position of xmin2,ymax2 in screen.
			var xmin2px = (1 - ratio) * xmin2px_0;
			var ymax2px = (1 - ratio) * ymax2px_0;
			// Current stretch.
			var sx = 1 + ratio * (sx_1 - 1);
			var sy = 1 + ratio * (sy_1 - 1);

			// Position of x|ypx: xmin2|ymax2 + stretched offset of x|ypx_0 relative to xmin2px|ymax2px.
			function xpx(xpx_0) { return xmin2px + (xpx_0 - xmin2px_0) * sx; }
			function ypx(ypx_0) { return ymax2px - (ymax2px_0 - ypx_0) * sy; }

			var tile_width = TILE_WIDTH * sx;
			var tile_height = TILE_HEIGHT * sy;
			var y = 0;
			var tile2_width = TILE_WIDTH / (sx_1 / sx);
			var tile2_height = TILE_HEIGHT / (sy_1 / sy);
			var y2 = 0;
			for (var row = 0; row < number_tiles_y; row++)
			{
				var tile_row = tiles[row];
				var tile2_row = tiles2[row];
				var next_y = Math.round((row + 1) * tile_height);
				var x = 0;
				var next_y2 = Math.round((row + 1) * tile2_height);
				var x2 = 0;
				for (var column = 0; column < number_tiles_x; column++)
				{
					var tile = tile_row[column];
					var next_x = Math.round((column + 1) * tile_width);
					tile.style.left = x + 'px';
					tile.style.top = y + 'px';
					tile.style.width = (next_x - x) + 'px';
					tile.style.height = (next_y - y) + 'px';
					x = next_x;

					var tile2 = tile2_row[column];
					var next_x2 = Math.round((column + 1) * tile2_width);
					tile2.style.left = x2 + 'px';
					tile2.style.top = y2 + 'px';
					tile2.style.width = (next_x2 - x2) + 'px';
					tile2.style.height = (next_y2 - y2) + 'px';
					x2 = next_x2;
				}
				y = next_y;
				y2 = next_y2;
			}
			g_block.style.left = Math.round(xpx(block_left)) + 'px';
			g_block.style.top = Math.round(ypx(block_top)) + 'px';
			g_block.style.width = Math.round(number_tiles_x * tile_width) + 'px';
			g_block.style.height = Math.round(number_tiles_y * tile_height) + 'px';
			g_block2.style.left = Math.round(xpx(new_block_left)) + 'px';
			g_block2.style.top = Math.round(ypx(new_block_top)) + 'px';
			g_block2.style.width = Math.round(number_tiles_x * tile2_width) + 'px';
			g_block2.style.height = Math.round(number_tiles_y * tile2_height) + 'px';

			if (currentFrame == numberOfFrames)
			{	// Last frame.
				if (!no_animation)
				{
					clearInterval(transition_timer); // No more frames please.
					transition_timer = null;
				}

				var block_tmp = g_block;
				g_block = g_block2;
				g_block2 = block_tmp;

				setGraphWindow(gw2);

				setMouseWait(false);
				enableCoord();
			}
		}
	}

	return false;
}

// z<0 -> zoom in (=more details), z>0 -> zoom out.
function zoom(z)
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	var gw2 = GraphWindowCopy(g_graphwindow);
	gw2.zoom(z);

	return transitionTo(gw2, false);
}

function zoomPlus()
{
	return zoom(-1);
}

function zoomMinus()
{
	return zoom(1);
}

function recenterAndZoom(e)
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	var gw2 = GraphWindowCopy(g_graphwindow);
	gw2.centerAndZoom(g_mouse_x_viewer, g_mouse_y_viewer, -1);

	return transitionTo(gw2, false);
}

function zoomAt(plus)
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	if (plus)
	{
		return zoom(-1);
	}
	else
	{
		return zoom(1);
	}
}

function resetGraphWindow()
{
	if (!g_loaded) { return true; } // Not loaded yet -> Do normal form action.

	id('reln').select();

	var gw2 = new GraphWindow();
	gw2.reset();
	return transitionTo(gw2, false);
}
