/*
 * (C)  Bernhard Zwischenbrugger
 * http://datenkueche.com
 * bz(at)datenkueche(dot)com
 *
 * (C) 2009-2010 Andrzej Zaborowski <balrogg@gmail.com>
 * Wikipedia layer and other modifications.
 * Forked off of http://www.khtml.org/v0.24/osm.js
 * (the emotional comments aren't mine)
 */

/*
 * mapTypeControl
 */
function mapTypeControl() {
	this.map = null;
	this.control_div = document.createElement("div");
	this.control_div.style.top = "5px";
	this.control_div.style.left = "100px";
	this.control_div.style.position = "absolute";
}

mapTypeControl.prototype.initialize = function(map) {
	this.map = map;
	map.control.appendChild(this.control_div);
}

kMap.prototype.map_type = function(type) {
	this.clear_map();

	switch (type) {
	case "mapnik":
		this.make_url = this.mapnik_url;
		this.max_zoom = 18;
		break;
	case "osma":
		this.make_url = this.osma_url;
		this.max_zoom = 17;
		break;
	case "bikemap":
		this.make_url = this.bikemap_url;
		this.max_zoom = 18;
		break;
	default:
		alert("unknown maptype");
	}

	this.draw();
}

/*
 * mapControl
 */
function mapControl() {
	this.map = null;
	this.control_div = document.createElement("div");
	this.control_div.style.top = "5px";
	this.control_div.style.left = "5px";
	this.control_div.style.position = "absolute";
	this.move_speed = 1;
	this.sliding = false;
}

mapControl.prototype.initialize = function(map) {
	this.map = map;
	this.icon_server = "http://www.khtml.org/"

	/* Keyboard */
	Event.attach(document, "keydown", this.key_down, this, false);
	Event.attach(document, "keyup", this.key_up, this, false);

	/* Zoommoveelement */
	map.control.appendChild(this.control_div);

	Event.attach(map.control, "mouseup", this.stopMove, this, false);
	var img = this.append_img(this.icon_server +
			"images/west.png", 0, 20);
	Event.attach(img, "mousedown", this.start_move_west, this, false);
	var img = this.append_img(this.icon_server +
			"images/east.png", 40, 20);
	Event.attach(img, "mousedown", this.start_move_east, this, false);
	var img = this.append_img(this.icon_server +
			"images/north.png", 20, 0);
	Event.attach(img, "mousedown", this.start_move_north, this, false);
	var img = this.append_img(this.icon_server +
			"images/south.png", 20, 40);
	Event.attach(img, "mousedown", this.start_move_south, this, false);

	var img = this.append_img(this.icon_server +
			"images/zoom-plus.png", 20, 63);
	Event.attach(img, "mousedown", this.start_zoom_in, this, false);
	var img = this.append_img(this.icon_server +
			"images/zoom-minus.png", 20, 278);
	Event.attach(img, "mousedown", this.start_zoom_out, this, false);

	var div = document.createElement("div");
	this.slider = div;
	div.style.position = "absolute";
	div.style.top = "0px";
	div.style.left = "0px";
	this.control_div.appendChild(div);
	var img = this.append_img(this.icon_server +
			"images/zoombar.png", 20, 80);
	div.appendChild(img);
	var img = this.append_img(this.icon_server +
	    		"images/slider.png", 19, 100);
	div.appendChild(img);
	Event.attach(div, "mousedown", this.startSlider, this, false);
	Event.attach(div.parentNode.parentNode.parentNode, "mousemove",
		     this.moveSlider, this, false);
	Event.attach(div.parentNode.parentNode.parentNode, "mouseup",
		     this.endSlider, this, false);
}

mapControl.prototype.key_down = function(evt) {
	/* if (evt.preventDefault)
	 *	evt.preventDefault();
	 * else
	 *	evt.returnValue = false;
	 */
	switch (evt.keyCode) {
	case 39:
		this.start_move_east();
		break;
	case 37:
		this.start_move_west();
		break;
	case 38:
		this.start_move_north();
		break;
	case 40:
		this.start_move_south();
		break;
	case 107:
	case 187:	/* chrome */
	case 61:
		this.start_zoom_in();
		break;
	case 189:	/* chrome */
	case 109:
		this.start_zoom_out();
		break;
	case 13:
		this.map.snap();
		break;
	default:
		// alert(evt.keyCode);
	}
}

mapControl.prototype.key_up = function(evt) {
	this.stopMove();
}

mapControl.prototype.startSlider = function(evt) {
	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	this.sliding = true;
	var z = (-evt.pageY + this.map.origworld.offsetTop + 80 + 200) / 11.12;
	if (z < this.map.ui_min_zoom)
		z = this.map.ui_min_zoom;
	if (z > this.map.ui_max_zoom)
		z = this.map.ui_max_zoom;
	this.map.zoom = z;
	this.map.draw();
}

mapControl.prototype.moveSlider = function(evt) {
	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	if (this.sliding) {
		var z = (-evt.pageY + this.map.origworld.offsetTop +
				80 + 200) / 11.12;
		if (z < this.map.ui_min_zoom)
			z = this.map.ui_min_zoom;
		if (z > this.map.ui_max_zoom)
			z = this.map.ui_max_zoom;
		this.map.zoom = z;
		this.map.draw();
	}
}

mapControl.prototype.callback = function() {
	var mover = this.slider.childNodes.item(1);
	var top = -this.map.zoom * 11.11 + this.map.origworld.offsetTop + 260;
	mover.style.top = top + "px";
}

mapControl.prototype.endSlider = function(evt) {
	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	this.sliding = false;
}

mapControl.prototype.start_move_east = function() {
	this.move(1, 0);
	this.move_speed = this.move_speed + 0.2;
	this_object = this;
	this_object.move_timer =
		setTimeout("this_object.start_move_east()", 20);
}

mapControl.prototype.start_move_west = function() {
	this.move(-1, 0);
	this.move_speed = this.move_speed + 0.2;
	this_object = this;
	this_object.move_timer =
		setTimeout("this_object.start_move_west()", 20);
}

mapControl.prototype.start_move_south = function() {
	this.move(0, 1);
	this.move_speed = this.move_speed + 0.2;
	this_object = this;
	this_object.move_timer =
		setTimeout("this_object.start_move_south()", 20);
}

mapControl.prototype.start_move_north = function() {
	this.move(0, -1);
	this.move_speed = this.move_speed + 0.2;
	this_object = this;
	this_object.move_timer =
		setTimeout("this_object.start_move_north()", 20);
}

mapControl.prototype.start_zoom_in = function() {
	// this.move(0, -1);
	this.zoom(+1);
	this.move_speed = this.move_speed + 0.2;
	this_object = this;
	this_object.move_timer = setTimeout("this_object.start_zoom_in()", 20);
}

mapControl.prototype.start_zoom_out = function() {
	// this.move(0,-1);
	this.zoom(-1);
	this.move_speed = this.move_speed + 0.2;
	this_object = this;
	this_object.move_timer = setTimeout("this_object.start_zoom_out()", 20);
}

mapControl.prototype.stopMove = function() {
	this.move_speed = 1;
	clearTimeout(this.move_timer);
}

mapControl.prototype.move = function(x, y) {
	this.map.x = this.map.x + x * this.move_speed;
	this.map.y = this.map.y + y * this.move_speed;
	this.map.latlon();
	this.map.draw();
}

mapControl.prototype.zoom = function(z) {
	z = this.map.zoom + 0.01 * z * this.move_speed / 4 + z * 0.01;
	if (z < this.map.ui_min_zoom)
		z = this.map.ui_min_zoom;
	if (z > this.map.ui_max_zoom)
		z = this.map.ui_max_zoom;
	this.map.zoom = z;
	this.map.draw();
}

mapControl.prototype.append_img = function(src, left, top) {
	var img = document.createElement("img");
	img.setAttribute("src", src);
	img.style.top = top + "px";
	img.style.left = left + "px";
	img.style.position = "absolute";
	this.control_div.appendChild(img);
	return img;
}

/*
 *
 * THIS IS THE MAIN OBJECT
 *
 */
function kMap(world) {
	this.origworld = world;

	this.overlay_array = new Array();

	this.eventtarget = document.createElement("div");
	this.eventtarget.style.position = "absolute";
	this.eventtarget.style.top = "0px";
	this.eventtarget.style.left = "0px";
	this.eventtarget.style.width = this.origworld.offsetWidth + "px";
	this.eventtarget.style.height = this.origworld.offsetHeight + "px";
	this.origworld.appendChild(this.eventtarget);

	this.world = document.createElement("div");
	this.world.style.position = "absolute";
	this.world.style.top = 0 + "px";
	this.world.style.left = 0 + "px";
	this.eventtarget.appendChild(this.world);

	this.control = document.createElement("div");
	this.control.style.position = "absolute";
	this.control.style.top = "0px";
	this.control.style.left = "0px";
	world.appendChild(this.control);

	this.copyright_div = this.origworld.appendChild(this.copyright());

	this.world_overlay = document.createElement("div");
	this.world_overlay.style.zIndex = 2000;
	this.world_overlay.style.position = "absolute";
	this.world_overlay.setAttribute("name", "overlay");
	this.world.appendChild(this.world_overlay);

	this.get_size(world);

	/* constants */
	this.ui_max_zoom = 20;
	this.ui_min_zoom = 1.4;
	this.max_zoom = 18;
	this.min_zoom = 0;
	this.zoom = 2;
	this.wx = 256;
	this.wy = 256;
	/* variables */
	this.make_url = this.mapnik_url;
	this.snapping = false;
	this.snapping_dynamic = true;
	this.clicked = false;
	this.visible_layer = null;
	this.down_x = 0;
	this.down_y = 0;
	this.client_x = this.width / 2 + this.left;	/* middle of map */
	this.client_y = this.height / 2 + this.top;
	this.dx = 0;
	this.dy = 0;
	this.x = 0;
	this.y = 0;
	this.zoomspeed = 0.01;
	this.load_stat_array = new Array(this.ui_max_zoom -
			Math.floor(this.ui_min_zoom));
	for (var i = Math.floor(this.ui_min_zoom); i <= this.ui_max_zoom; i++)
		this.load_stat_array[i] = 0;
	this.frames = 0;
	this.xydraw_blocked = false;
	this.moveBlocked = false;
	this.double_click = false;
	this.snap_timer = null;
	this.select_area = null;
	// this.world = world;
	world.style.overflow = "hidden";
	// window.attach(this, "resize", this.get_size, false);
	// this.world.attach(this, "DOMMouseScroll", this.wheel, false);
	Event.attach(window, "resize", this.get_size, this, false);
	Event.attach(this.eventtarget, "DOMMouseScroll", this.wheel, this,
		false);
	Event.attach(this.eventtarget, "mousedown", this.down, this,
		false);
	Event.attach(this.eventtarget, "dblclick", this.doubledown, this,
		false);
	Event.attach(document, "mouseup", this.up, this, false);
	Event.attach(document, "mousemove", this.move, this, false);

	// while(this.world.firstChild)
	//	this.world.removeChild(this.world.firstChild)
	// this.xydraw(1000, 800, 3);

	this.callbackHandlers = new Array();
}

/*
 * Overlay handling
 */
kMap.prototype.add_img_overlay = function(lon, lat, el) {
	el.style.position = "absolute";
	this.world_overlay.appendChild(el);

	var x = (lon / 360) * this.wx;
	var y = (this.lat2y(-lat) / 360) * this.wy;

	var overlay = { 0: x, 1: y, 2: el, parent: this,
			redraw: imglayer_draw, get_size: function(o) {} };
	this.overlay_array.push(overlay);
	this.draw("yes");
}

kMap.prototype.add_wikipedia_overlay = function(levels) {
	var wikilayer = {
		"parent": this,
		features: {},
		links: {},
		tiles: {},
		points: {},
		"levels": levels,
		bounds: { min: { x: 0, y: 0, z: 0 },
			max: { x: 0, y: 0, z: 0 } },
		graph_ns: "http://www.w3.org/2000/svg",
		redraw: wikilayer_draw,
		get_size: wikilayer_get_size,
	};
	wikilayer.graph = document.createElementNS(wikilayer.graph_ns, "svg");
	wikilayer.graph.style.position = "absolute";
	/* Should not matter */
	wikilayer.graph.setAttributeNS(null, "preserveAspectRatio", "none");
	wikilayer.get_size(wikilayer);

	this.world_overlay.appendChild(wikilayer.graph);

	this.overlay_array.push(wikilayer);
	this.draw("yes");

	var map = this;
	if (window.opera || window.navigator.userAgent.indexOf("WebKit") > -1)
		this.world.addEventListener('mousemove', function(evt) {
				/* Weak :( */
				wikilayer_mouse(wikilayer, evt,
						evt.clientX -
						map.origworld.offsetLeft,
						evt.clientY -
						map.origworld.offsetTop); },
				false);
	else
		wikilayer.graph.onmousemove = function(evt) {
				wikilayer_mouse(wikilayer, evt,
						evt.layerX, evt.layerY); };

	var tdefs = document.createElementNS(wikilayer.graph_ns, "defs");
	wikilayer.graph.appendChild(tdefs);

	wikilayer.graph_path = document.createElementNS(wikilayer.graph_ns,
			"path");
	wikilayer.graph_path.setAttributeNS(null, "id", "circ");
	tdefs.appendChild(wikilayer.graph_path);

	wikilayer.graph_text = document.createElementNS(wikilayer.graph_ns,
			"text");
	wikilayer.graph_text.setAttributeNS(null, "font-family", "Verdana");
	wikilayer.graph_text.setAttributeNS(null, "fill", "black");
	wikilayer.graph_text.setAttributeNS(null, "text-anchor", "middle");

	wikilayer.graph_tpath = document.createElementNS(wikilayer.graph_ns,
			"textPath");
	wikilayer.graph_tpath.setAttributeNS("http://www.w3.org/1999/xlink",
			"href", "#circ");
	wikilayer.graph_tpath.setAttributeNS(null, "startOffset", "50%");
	wikilayer.graph_text.appendChild(wikilayer.graph_tpath);

	wikilayer.zoom_start = function() {
		if (wikilayer.selected_feature) {
			wikilayer.selected_feature.set_selected(0);
			delete wikilayer["selected_link"];
			delete wikilayer["selected_feature"];
		}
	}
}

kMap.prototype.draw_overlays = function(middle_x, middle_y, zoom, int_zoom) {
	var faktor = Math.pow(2, zoom);
	for (var i in this.overlay_array) {
		this.overlay_array[i].redraw(this.overlay_array[i],
				middle_x, middle_y, zoom, faktor);
	}
}

kMap.prototype.remove_overlays = function() {
	while (this.world_overlay.firstChild)
		this.world_overlay.removeChild(
				this.world_overlay.firstChild);
}

/*
 * Generic image / text overlay
 */
function imglayer_draw(ovl, middle_x, middle_y, zoom, faktor) {
	var x = ovl[0];
	var y = ovl[1];
	var el = ovl[2];
	var map = ovl.parent;
	el.style.left = (x + 128) * faktor - map.x + "px";
	el.style.top = (y + 128) * faktor - map.y + "px";
}

/*
 * Wikipedia tiled overlay
 */
function wikilayer_tags_update(layer) {
	var tags = {};
	var text = "";
	var found = 0;

	for (var tile in layer.tiles)
		for (var tag in layer.tiles[tile].tags) {
			if (!(tag in tags))
				tags[tag] = layer.tiles[tile].tags[tag];
			else
				tags[tag] += layer.tiles[tile].tags[tag];
			found = 1;
		}
	if (found) {
		var list = [];
		for (var tag in tags) {
			var size;
			if (tags[tag] > 19)
				size = 20;
			else if (tags[tag] > 9)
				size = 10;
			else if (tags[tag] > 1)
				size = 2;
			else
				size = 1;
			list.push("<span class=\"tag" + size + "\">" + tag +
					"</span>");
		}
		list.sort();
		text = "<a href=\"http://flickr.com/\" class=" +
			"\"external_link\">Flickr</a> tags: " + list.join(" ");
	}

	document.getElementById("wp_tags").innerHTML = text;
}

function wikilayer_add_tile(tiledesc, obj) {
	var layer = obj[4];
	var tile = { features: [], tags: {} };
	tile.z = obj[2];
	tile.x = obj[0] << (16 - tile.z);
	tile.y = obj[1] << (16 - tile.z);
	layer.tiles[obj[3]] = tile;

	var tilediv = document.createElementNS(layer.graph_ns, "g");
	tile.graph = tilediv;
	layer.graph.appendChild(tile.graph);

	for (var feature in tiledesc)
		wikilayer_add_feature(tiledesc[feature], layer, tile);

	wikilayer_tags_update(layer);
}

function wikilayer_add_feature(feature, layer, tile) {
	var id = feature["id"];
	var link = null;
	if (feature["url"])
		link = feature["url"];
	if (feature["website"])
		link = feature["website"];
	if (feature["flickr"])
		link = feature["flickr"].id;
	if (feature["wikipedia"])
		link = feature["wikipedia"];
	if (link == null)
		return;

	var graph = wikilayer_geojson_to_svg(feature,
			feature.geometry, layer, tile.z);
	if (!graph.length)
		return;
	delete feature["geometry"];

	/* TODO: avoid displaying features with same id multiple times in
	 * different tiles.  */
	//layer["features"][id] = feature;
	tile.features.push(feature);

	if (!(link in layer.links))
		layer.links[link] = [];
	layer.links[link].push(feature);

	feature.link = link;
	feature.centre = graph[0];
	feature.graph = graph[1];
	feature.set_size = graph[2];
	feature.selected = 0;
	feature.set_selected = function(selected) {
		wikilayer_svg_select(feature, selected, layer); };

	feature.set_size(1);

	tile.graph.appendChild(feature.graph);

	layer.points[feature.centre] = feature;

	if (feature.flickr && feature.flickr.tags)
		for (var tag in feature.flickr.tags)
			if (!tile.tags[feature.flickr.tags[tag]])
				tile.tags[feature.flickr.tags[tag]] = 1;
			else
				tile.tags[feature.flickr.tags[tag]] += 1;
}

String.prototype.to_xml_safe = function() {
	return this.replace(/&/g, "&amp;").replace(/</g, "&lt;").
		replace(/>/g, "&gt;");
}

function wikilayer_geojson_to_svg(feature, geometry, layer, zoom) {
	var wp = feature["wikipedia"];
	var fl = feature["flickr"];
	var baselevel = layer.bounds.max.z > 18 ? 18 : layer.bounds.max.z;
	var factor = (1 << (20 - baselevel)) / 16.0;
	var fill = wp ? "#38f" : fl ? "#e3a" : "#777";
	var fillr = wp ? 0x33 : fl ? 0xee : 0x77;
	var fillg = wp ? 0x88 : fl ? 0x33 : 0x77;
	var fillb = wp ? 0xff : fl ? 0xaa : 0x77;
	var centre;
	var node;
	var size;

	feature.isnode = 0;
	feature.current_size = 1.0;

	if (geometry.type == "Point") {
		node = document.createElementNS(layer.graph_ns, "circle");

		node.setAttributeNS(null, "stroke-width", 0 * factor);

		var x = (geometry.coordinates[0] + 180.0) * (65536.0 / 360.0);
		var y = (180.0 - layer.parent.lat2y(geometry.coordinates[1])) *
				(65536.0 / 360.0);

		node.setAttributeNS(null, "cx", x);
		node.setAttributeNS(null, "cy", y);

		centre = [ x, y ];

		feature.isnode = 1;

		size = function(d) {
			feature.current_size = d;

			var zf = Math.max(0.0, 1.5 - d) * 2.0;
			var r = Math.ceil(fillr * zf);
			var g = Math.ceil(fillg * zf);
			var b = Math.ceil(fillb * zf);
			var c = 0x100000 | (r << 16) | (g << 8) | b;

			d *= factor;
			node.setAttributeNS(null, "r", d * 0.15);
			node.setAttributeNS(null, "fill", "#" + c.toString(16));

			if (feature.selected) {
				var r = d * 0.15;
				var path = "M" + x + " " + (y + r) +
					"A" + r + " " + r + " 0 1 1 " +
					x + " " + (y - r) +
					"A" + r + " " + r + " 0 1 1 " +
					x + " " + (y + r);
				layer.graph_path.setAttributeNS(null,
						"d", path);
				layer.graph_text.setAttributeNS(null,
						"font-size", "" + (0.4 * r));
			}
		};
	} else if (geometry.type == "LineString") {
		var bounds = [ 65536, 65536, 0, 0 ];
		var points = [];
		node = document.createElementNS(layer.graph_ns, "polyline");

		for (var c in geometry.coordinates) {
			var x = (geometry.coordinates[c][0] + 180.0) *
				(65536.0 / 360.0);
			var y = (180.0 - layer.parent.lat2y(
					geometry.coordinates[c][1])) *
				(65536.0 / 360.0);

			points.push(x + "," + y);

			if (x > bounds[2])
				bounds[2] = x;
			if (x < bounds[0])
				bounds[0] = x;
			if (y > bounds[3])
				bounds[3] = y;
			if (y < bounds[1])
				bounds[1] = y;
		}

		node.setAttributeNS(null, "fill", "none");
		node.setAttributeNS(null, "stroke", "black");
		node.setAttributeNS(null, "points", points.join(" "));

		/* TODO: should find a better center point */
		centre = [ (bounds[0] + bounds[2]) * 0.5,
				(bounds[1] + bounds[3]) * 0.5 ];

		size = function(d) {
			node.setAttributeNS(null, "stroke-width",
					0.1 * (d - 1.0) * factor);

			if (feature.selected) {
				var path = "M" + (centre[0] - 20 * factor) +
					" " + centre[1] +
					"L" + (centre[0] + 20 * factor) +
					" " + centre[1];
				layer.graph_path.setAttributeNS(null,
						"d", path);
				layer.graph_text.setAttributeNS(null,
						"font-size",
						"" + (0.07 * d * factor));
			}
		};
	} else if (geometry.type == "Polygon") {
		var bounds = [ 65536, 65536, 0, 0 ];
		node = document.createElementNS(layer.graph_ns, "g");

		for (var c in geometry.coordinates) {
			var points = [];
			var poly = document.createElementNS(layer.graph_ns,
					"polygon");

			for (var d in geometry.coordinates[c]) {
				var x = (geometry.coordinates[c][d][0] + 180.0)
					* (65536.0 / 360.0);
				var y = (180.0 - layer.parent.lat2y(
						geometry.coordinates[c][d][1]))
					* (65536.0 / 360.0);

				points.push(x + "," + y);

				if (x > bounds[2])
					bounds[2] = x;
				if (x < bounds[0])
					bounds[0] = x;
				if (y > bounds[3])
					bounds[3] = y;
				if (y < bounds[1])
					bounds[1] = y;
			}

			node.appendChild(poly);
			poly.setAttributeNS(null, "points", points.join(" "));
		}

		node.setAttributeNS(null, "fill", fill);
		node.setAttributeNS(null, "stroke", "black");

		centre = [ (bounds[0] + bounds[2]) * 0.5,
				(bounds[1] + bounds[3]) * 0.5 ];

		size = function(d) {
			node.setAttributeNS(null, "stroke-width",
					0.2 * (d - 1.0) * factor);

			if (feature.selected) {
				var path = "M" + (centre[0] - 20 * factor) +
					" " + centre[1] +
					"L" + (centre[0] + 20 * factor) +
					" " + centre[1];
				layer.graph_path.setAttributeNS(null,
						"d", path);
				layer.graph_text.setAttributeNS(null,
						"font-size",
						"" + (0.07 * d * factor));
			}
		};
	} else
		return [];

	return [ centre, node, size ];
}

function wikilayer_svg_select(feature, selected, layer) {
	var baselevel = layer.bounds.max.z > 18 ? 18 : layer.bounds.max.z;
	var factor = (1 << (20 - baselevel)) / 16.0;

	/* TODO: split out the svg-specific and non-svg parts */
	if (selected) {
		if (!feature.popup_timer)
			feature.popup_timer = setTimeout(function() {
					wikilayer_popup(feature, layer);
				}, 1000);
	} else if (feature.popup_timer) {
		clearTimeout(feature.popup_timer);
		feature.popup_timer = 0;
	} else
		wikilayer_popup_off(feature, layer);

	feature.selected = selected;
	if (selected) {
		var name = "";
		if ("name" in feature && feature.name.length > 0)
			name = feature.name; /* .to_xml_safe(); */

		feature.set_size(feature.current_size);
		feature.text_node = document.createTextNode(name);
		layer.graph_tpath.appendChild(feature.text_node);
		layer.graph.appendChild(layer.graph_text);
	} else {
		layer.graph.removeChild(layer.graph_text);
		layer.graph_tpath.removeChild(feature.text_node);
		delete feature["text_node"];
	}
	/* TODO:
	 *   On single click anywhere on the selected feature except on the
	 *   html, go to the wikipedia page directly, especially before the
	 *   popup has popped up
	 * TODO: check why bbox middle is always on the right and below
	 * the actual feature!
	 */
}

var zoom_to_feature;
var load_iframe;
var wikilayer_re = /^http:\/\/([a-z-]+)\.wikipedia\.org\/wiki\//;
function wikilayer_popup(feature, layer) {
	feature.popup_timer = 0;
	var popup = document.getElementById("wp_popup");

	var id = "id" in feature ? feature.id : 0;
	var objtype = id >= 0 ? feature.isnode ?
		"node" : "way" : "relation";
	if (id < 0)
		id = -id;

	var exturl = feature.link;
	var proto = "";
	var wikipedia_title = feature.wikipedia;
	/* If the link looks like a URL, assume it's an URL, not a wikipedia
	 * page title.  */
	if (
		!wikipedia_title ||
		/* protocol://host/path */
		exturl.indexOf("://") > -1 ||
		/* world wide web */
		exturl.indexOf("www.") > -1 ||
		/* host name with dots and a slash */
		exturl.match(/^.+\..+\/.*$/)) {
		wikipedia_title = "";

		if (exturl.indexOf("://") < 0)
			proto = "http://";
	}
	if ((proto + exturl).match(wikilayer_re))
		wikipedia_title = (proto + exturl).replace(wikilayer_re, "$1:");

	if (wikipedia_title.substr(wikipedia_title.indexOf(":") + 1) in
			{"yes": 1, "true": 1}) /* Limited usage in UK */
		wikipedia_title = wikipedia_title.substr(0,
				wikipedia_title.indexOf(":") + 1) +
			feature.name;

	document.getElementById("wp_popup_name").innerHTML =
		"name" in feature ? feature.name.to_xml_safe() : "";

	if (id) {
		var osmurl = "http://www.openstreetmap.org/browse/" +
			objtype + "/" + id;
		document.getElementById("wp_popup_obj_link").innerHTML =
			"OpenStreetMap " + objtype + " " + id;
		document.getElementById("wp_popup_obj_link").href = osmurl;
	} else
		document.getElementById("wp_popup_obj_link").innerHTML = "";

	if (wikipedia_title) {
		document.getElementById("wp_popup_ext_link").innerHTML =
			"Retrieving wikipedia page metadata...";
		wikilayer_retrieve_page_info(wikipedia_title, function(ret) {
					wikilayer_popup_fill_in(feature,
							wikipedia_title, ret);
				});
	} else if (feature.flickr) {
		var text = "";

		if (feature.foursquare)
			text += "<a href=\"http://foursquare.com/venue/" +
				feature.foursquare + "\" " +
				"class=\"external_link\">Venue " +
				feature.foursquare + "</a> on " +
				"foursquare.com<br />";

		if (feature.dopplr) {
			str = feature.dopplr.replace("=", "/");
			pos = str.indexOf("/");
			type = str.substr(0, pos);
			name = str.substr(pos + 1);
			if (type == "eat")
				place = "Food place";
			else if (type == "explore")
				place = "Explore place";
			else
				place = "Place";
			text += "<a href=\"http://dplr.it/" + str + "\" " +
				"class=\"external_link\">" + place + " \"" +
				name + "\"</a> on Dopplr<br />";
		}

		text += "This picture of it";
		if (feature.flickr.title)
			text += ", titled \"" + feature.flickr.title + "\"";
		if (feature.flickr.datetaken)
			text += ", taken on " + feature.flickr.datetaken;
		if (feature.flickr.ownername)
			text += ",<br />copyright user " +
				feature.flickr.ownername;
		text += ", was posted on flickr.com:<br />";

		text += "<a href=\"http://www.flickr.com/photos/" +
			feature.flickr.owner + "/" + feature.flickr.id +
			"/\" lass=\"external_link\">" +
			"<img class=\"wp_image\" src=\"" +
			feature.flickr.url + "\" /></a>";

		document.getElementById("wp_popup_ext_link").innerHTML = text;
	} else {
		document.getElementById("wp_popup_ext_link").innerHTML =
			"<a href=\"" + encodeURI(proto + exturl) + "\" " +
			"class=\"external_link\">Go to " +
			exturl.to_xml_safe() + "</a><br />" +
			"Or <a href=\"javascript:load_iframe()\" " +
			"class=\"external_link\">load it here</a>";
		load_iframe = function() {
			document.getElementById("wp_popup_ext_link").innerHTML =
					"<a href=\"" +
					encodeURI(proto + exturl) +
					"\" class=\"external_link\">Go to " +
					exturl.to_xml_safe() + "</a><br />" +
					"<iframe src=\"" +
					encodeURI(proto + exturl) +
					"\" />";
		}
	}

	zoom_to_feature = function() {
		var lat = layer.parent.y2lat(180.0 -
				feature.centre[1] / 65536 * 360.0);
		var lon = feature.centre[0] / 65536 * 360.0 - 180.0;
		layer.parent.set_centre(lat, lon, layer.bounds.max.z + 1.0);
	};
	document.getElementById("wp_popup_zoom_link").href =
		"javascript:zoom_to_feature()";

	if (!feature.flickr)
		document.getElementById("wp_popup_ext_link").innerHTML +=
			"<br />No picture available";

	var map = layer.parent;
	var factor = Math.pow(2, 16 - map.zoom);
	popup.style.left = Math.round(feature.centre[0] * map.wx / factor +
			map.width / 2 - map.x) + "px";
	popup.style.top = Math.round(feature.centre[1] * map.wy / factor +
			map.height / 2 - map.y) + "px";

	popup.style.visibility = "visible";
}

function wikilayer_popup_off(feature, layer) {
	var popup = document.getElementById("wp_popup");
	popup.style.visibility = "hidden";
}

function to_list(this_obj, joint, num) {
	if (num && num < this_obj.length)
		return this_obj.slice(0, num).join(", ") + "...";

	if (this_obj.length > 1)
		return this_obj.slice(0, -1).join(", ") +
			(joint ? " " + joint + " " : " and ") +
			this_obj[this_obj.length - 1];
	return this_obj.join("");
}

function wikilayer_popup_fill_in(feature, fromtitle, response) {
	var statusobj = document.getElementById("wp_popup_ext_link");
	var langobj = document.getElementById("wp_language");

	/* Figure out the original link's language */
	var fromlanguage = "en";
	if (fromtitle.indexOf(":") > -1) {
		fromtitle = fromtitle.split(":", 2);
		fromlanguage = fromtitle[0];
		fromtitle = fromtitle[1];
	}

	/* Figure out the preferred language for pages */
	var tolanguage = "en";
	if (navigator.language)
		tolanguage = navigator.language.toLowerCase();
	if (langobj) {
		var value = langobj.options[langobj.selectedIndex].value;
		if (value && value != "default")
			tolanguage = value;
	}
	tolanguage = tolanguage.split(",");

	/* Parse Wikipedia API response */
	query = response["query"];
	if (!query) {
		statusobj.innerHTML = "WP API response lacks 'query' element.";
		return;
	}

	pages = query["pages"];
	if (!pages) {
		statusobj.innerHTML = "WP API response lacks 'pages' element.";
		return;
	}

	var num = 0;
	var langs = {};
	var avail = [];
	var images = [];
	for (var id in pages)
		if (parseInt(id) > 0) {
			num ++;
			if ("langlinks" in pages[id])
				for (var i in pages[id].langlinks) {
					lang = pages[id].langlinks[i];
					langs[lang["lang"]] = lang["*"];
					avail.push(lang["lang"]);
				}

			if ("images" in pages[id])
				for (var image in pages[id].images)
					images.push(pages[id].
							images[image].title);
		}

	if (!num) {
		statusobj.innerHTML = "Wikipedia found no pages by the " +
			"title '" + fromtitle.to_xml_safe() + "' (" +
			fromlanguage.to_xml_safe() + ")";
		return;
	}

	avail.push(fromlanguage);
	langs[fromlanguage] = fromtitle; /* TODO: use the normalised title */

	var picture_html = wikilayer_picture_html(images);
	var picture = images.length > 0;

	var lang = "xxx";
	for (var l in tolanguage) {
		var lang = tolanguage[l].substr(0, 2);
		if (lang in langs)
			break;
	}
	if (!(lang in langs)) {
		statusobj.innerHTML = "This page hasn't yet been translated " +
			"into any of the following languages: " +
			to_list(tolanguage, "or") +
			"<br />Current translations: " + to_list(avail.sort()) +
			/* TODO: make links */
			"<br />" + picture_html;

		return;
	}

	var url = "http://" + lang + ".wikipedia.org/wiki/" + langs[lang];

	statusobj.innerHTML = "<a href=\"" + encodeURI(url) + "\" " +
		"class=\"external_link\">Go to Wikipedia page " +
		langs[lang].to_xml_safe() + "</a><br />";

	if (picture)
		statusobj.innerHTML += "<a href=\"" + encodeURI(url) + "\" " +
			"class=\"external_link\">" + picture_html +
			"</a><br />";
	else
		statusobj.innerHTML += picture_html + "<br />";

	if (avail.length > 1) {
		statusobj.innerHTML += "<span class=\"other\">This page is " +
			"also available in the following languages: " +
			to_list(avail.sort(), "and", 20) + "</span>";
			/* TODO: make links */
	} else
		statusobj.innerHTML += "<span class=\"other\">This page is " +
			"not available in any other languages.</span>";
}

function wikilayer_picture_html(images) {
	if (!images.length)
		return "No picture available";
	/* TODO: Display multiple images?
	 * For the moment choose the first image, or if available a png or jpg
	 * image.  */
	var title = images[0];
	for (var i in images)
		if (images[i].indexOf(".png") > -1) {
			title = images[i];
			break;
		}
	for (var i in images)
		if (images[i].indexOf(".jpg") > -1) {
			title = images[i];
			break;
		}

	var title = title.substr(title.indexOf(":") + 1).replace(/ /g, "_");
	var hex = hex_md5(title);
	var url = "http://upload.wikimedia.org/wikipedia/commons/" +
			hex.substr(0, 1) + "/" + hex.substr(0, 2) +
			"/" + encodeURIComponent(title);

	if (title.indexOf(".svg") > -1)
		return "<embed class=\"wp_image\" src=\"" + url +
			"\" type=\"image/svg+xml\" />";

	return "<img class=\"wp_image\" src=\"" + url + "\" />";
}

function wikilayer_draw(layer, middle_x, middle_y, zoom, faktor) {
	faktor /= 65536.0;
	zoom -= 0.5;

	var map = layer.parent;
	var start_x = (middle_x - map.width / 2) / map.wx / faktor;
	var start_y = (middle_y - map.height / 2) / map.wy / faktor;

	var end_x = (middle_x + map.width / 2) / map.wx / faktor;
	var end_y = (middle_y + map.height / 2) / map.wy / faktor;

	layer.graph.setAttributeNS(null, "viewBox",
			start_x + " " + start_y + " " +
			(end_x - start_x) + " " + (end_y - start_y));

	/* TODO: wrap around instead of exiting cowardly */
	if (start_x >= 65536)
		return;
	start_x = start_x > 0 ? start_x : 0;
	if (start_y >= 65536)
		return;
	start_y = start_y > 0 ? start_y : 0;
	if (end_x < 0)
		return;
	end_x = end_x > 65536 ? 65536 : end_x;
	if (end_y < 0)
		return;
	end_y = end_y > 65536 ? 65536 : end_y;

	/* If there's no chance of us needing to download new tiles,
	 * don't go through the other checks.  */
	if (start_x >= layer.bounds.min.x &&
			start_y >= layer.bounds.min.y &&
			zoom >= layer.bounds.min.z &&
			end_x <= layer.bounds.max.x &&
			end_y <= layer.bounds.max.y &&
			zoom < layer.bounds.max.z)
		return;

	var minzoom = -1;
	var maxzoom = map.ui_max_zoom;
	var i = 0;
	while (i < layer.levels.length && zoom > layer.levels[i])
		minzoom = layer.levels[i ++];
	if (i < layer.levels.length)
		maxzoom = layer.levels[i];

	if (minzoom != layer.bounds.min.z) {
		/* TODO: create a new div etc.., like in real_xydraw */
		wikilayer_tile_cancel_kill_remove(layer, layer.tiles);

		layer.bounds.min.z = minzoom;
		layer.bounds.max.z = maxzoom;

		if (minzoom < 0) {
			/* Harcoded :-( */
			layer.bounds.min.x = 0;
			layer.bounds.min.y = 0;
			layer.bounds.max.x = 65536;
			layer.bounds.max.y = 65536;
			wikilayer_add_tile([ {
				"wikipedia": "OpenStreetMap",
				"name":      "The Planet",
				"id":        0,
				"geometry":  {
					"type":        "Point",
					"coordinates": [ 0, 0 ],
				}} ], [ 0, 0, 0, "x", layer ]);
		}
	}

	if (minzoom < 0)
		return;

	var tilezoom = minzoom - 1;

	var shift = 16 - tilezoom;
	var mask = 0x1ffff << shift;
	var step = 1 << shift;
	start_x = Math.floor(start_x) & mask;
	start_y = Math.floor(start_y) & mask;
	end_x = (Math.ceil(end_x) + step - 1) & mask;
	end_y = (Math.ceil(end_y) + step - 1) & mask;

	layer.bounds.min.x = start_x;
	layer.bounds.min.y = start_y;
	layer.bounds.max.x = end_x;
	layer.bounds.max.y = end_y;

	var old_tiles = layer.tiles;
	/* Remove the list from layer now to avoid race conditions,
	 * wikilayer_add_tile may be called synchronously or asynchronously...
	 */
	layer.tiles = {};

	end_x >>= shift;
	end_y >>= shift;
	for (var x = start_x >> shift; x < end_x; x ++)
		for (var y = start_y >> shift; y < end_y; y ++) {
			path = tilezoom + "/" + x + "/" + y + ".txt";
			if (path in old_tiles) {
				layer.tiles[path] = old_tiles[path];
				delete old_tiles[path];
				continue;
			}

			var tmp = wikilayer_make_request(path,
					wikilayer_add_tile,
					[ x, y, tilezoom, path, layer ]);
			if (!(path in layer.tiles) && tmp)
				/* Try to be synchronous-proof */
				layer.tiles[path] = tmp;
		}

	wikilayer_tile_cancel_kill_remove(layer, old_tiles);
	wikilayer_tags_update(layer);
}

function wikilayer_tile_cancel_kill_remove(layer, tiles) {
	for (var path in tiles) {
		tile = tiles[path];
		if ("callback" in tile) {
			/* XXX: this is extremely racy.  Also, does aborting
			 * the connections save any bandwidth, or is it more
			 * efficient to just set the dummy callback and let
			 * the requests finish? */
			tile.callback = function(a, b) {};
			tile.abort();
		} else if ("features" in tile) {
			layer.graph.removeChild(tile.graph);

			for (var f in tile.features) {
				if (tile.features[f] ==
						layer["selected_feature"]) {
					layer.selected_feature.set_selected(0);
					delete layer["selected_link"];
					delete layer["selected_feature"];
				}

				link = tile.features[f].link;
				for (var i in layer.links[link])
					if (layer.links[link][i] ==
							tile.features[f])
						delete layer.links[link][i];
				/* FAIL: calculating length to find out if
				 * list is non-empty */
				/* Again, racy if make_request is async... */
				if (!layer.links[link].length)
					delete layer.links[link];

				delete layer.points[tile.features[f].centre];
			}
		}

		delete tiles[path];
	}
}

var c = function(value) { return value; }
var cq = [];
function wikilayer_make_request(jsonurl, callback, obj) {
	var http_request;
	if (window.XMLHttpRequest) { /* Mozilla, Safari,... */
		http_request = new XMLHttpRequest();
		if (http_request.overrideMimeType)
			http_request.overrideMimeType('text/plain');
	} else if (window.ActiveXObject) { /* IE */
		try {
			http_request = new ActiveXObject("Msxml2.XMLHTTP");
		} catch (e) {
			try {
				http_request =
					new ActiveXObject("Microsoft.XMLHTTP");
			} catch (e) {}
		}
	}
	if (!http_request) {
		alert('I couldn\'t make a XMLHTTP object :-(');
		return false;
	}
	http_request.onreadystatechange = function() {
		if (http_request.readyState != 4)
			return;
		if (http_request.status == 200)
			callback(eval(http_request.responseText), obj);
		else if (http_request.status == 404)
			callback([], obj);
		else {
			/* TODO: handle the error (retry? display error
			 * in a non-obtrusive text field?) */
			/* TODO: or fall back to JSON-P immediately? */
			alert('Error ' + http_request.status +
				' reading the json tile file');
			if (http_request.status == 0) {
				wikilayer_make_request =
					wikilayer_make_request_jsonp;
				wikilayer_make_request(
						jsonurl, callback, obj);
			}
		}
	}
	http_request.open('GET', jsonurl, true);
	http_request.send(null);

	return http_request;
}

/* JSON-P: poor man's way to load the JSON files, advantages:
 *  - works cross-site, if you don't want to host the tile files
 *    in the same place as the page or use somebody else's tiles.
 *  - works offline, without requiring you to set up the http server
 *    if you only want to hack on the map page on a travel.
 * Disadvantages:
 *  - needs modified tiles in order to include the function name.
 *  - probably kills the browser caching, which would normally work
 *    if XMLHttpRequest was used.
 *  - also kills concurrency as the calls are all synchronous (?)
 */
function wikilayer_make_request_jsonp(jsonurl, callback, obj) {
	var node = document.createElement("script");
	node.setAttribute("type", "text/javascript");
	node.setAttribute("src", jsonurl);

	c = function(value) {
		/* TODO: should execute in a bottom-half to be semantically
		 * identical with wikilayer_make_request(), otherwise
		 * we may (and will) cause mysterious breakage.
		 * Are there bottom-halves in js in the language? */
		cb = cq.shift();
		cb[0](value, cb[1]);
	}

	cq.push([callback, obj]);
	document.body.appendChild(node);
	document.body.removeChild(node); /* Clean-up (?) */
	return 0;
}

var jsonpcb;
function wikilayer_retrieve_page_info(title, callback) {
	var from = "en";

	if (title.indexOf(":") > -1) {
		title = title.split(":", 2);
		from = title[0];
		title = title[1];
	}

	var query = "http://" + from + ".wikipedia.org/w/api.php?" +
			"action=query&prop=langlinks|images&" +
			"lllimit=max&imlimit=5&format=json&" +
			"titles=" + encodeURIComponent(title);

	jsonpcb = callback;
	query += "&callback=jsonpcb";

	var node = document.createElement("script");
	node.setAttribute("src", query);
	node.setAttribute("type", "text/javascript");
	document.body.appendChild(node);
	if (!window.opera && window.navigator.userAgent.indexOf("WebKit") == -1)
		document.body.removeChild(node); /* Clean-up (?) */

	return from;
}

function wikilayer_get_size(layer) {
	layer.graph.style.top = -layer.parent.height / 2 + "px";
	layer.graph.style.left = -layer.parent.width / 2 + "px";
	layer.graph.style.width = layer.parent.width + "px";
	layer.graph.style.height = layer.parent.height + "px";
}

function wikilayer_mouse(layer, evt, evt_x, evt_y) {
	if (layer.parent.sliding || layer.parent.moving ||
			layer.parent.zoom_timer)
		return;

	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	var map = layer.parent;
	var factor = Math.pow(2, 16 - map.zoom);
	var x = (evt_x - map.width / 2 + map.x) / map.wx * factor;
	var y = (evt_y - map.height / 2 + map.y) / map.wy * factor;

	var mindist = 1;
	var closest;
	for (var p in layer.points) {
		var feat = layer.points[p];
		var dx = (feat.centre[0] - x) / factor;
		var dy = (feat.centre[1] - y) / factor;
		var d2 = dx * dx + dy * dy;
		if (d2 < 0.5) {
			feat.set_size(2 / (d2 + 1.5));
			if (d2 < mindist) {
				mindist = d2;
				closest = feat;
			}
		}
	}

	if (mindist > 0.9 || layer.selected_link != closest.link) {
		if ("selected_link" in layer) {
			var links = layer.links[layer.selected_link];
			layer.selected_feature.set_selected(0);
			delete layer["selected_link"];
			delete layer["selected_feature"];
			for (var l in links)
				links[l].set_size(1.0);
		}
		if (mindist > 0.9)
			return;
	}

	if (layer.selected_link != closest.link) {
		layer.selected_link = closest.link;
		layer.selected_feature = closest;
		layer.selected_feature.set_selected(1);
	}
	var links = layer.links[layer.selected_link];
	for (var l in links)
		links[l].set_size(1 / (mindist + 0.25));
}

/*
 * GET Browser Window size - important if you resize the window
 */
kMap.prototype.get_size = function() {
	this.width = this.origworld.offsetWidth;
	this.height = this.origworld.offsetHeight;
	this.top = this.origworld.offsetTop;
	this.left = this.origworld.offsetLeft;
	this.pagetop = 0;
	this.pageleft = 0;
	for (var obj = this.origworld; obj != document.body;
			obj = obj.offsetParent) {
		this.pagetop += obj.offsetTop;
		this.pageleft += obj.offsetLeft;
	}
	this.client_x = this.width / 2;
	this.client_y = this.height / 2;
	this.world_overlay.style.top = this.height / 2 + "px";
	this.world_overlay.style.left = this.width / 2 + "px";
	this.copyright_div.style.top = this.height - 40 + "px";

	for (i in this.overlay_array)
		this.overlay_array[i].get_size(this.overlay_array[i]);
}

/*
 * Mouse Down Handler
 */
kMap.prototype.down = function(evt) {
	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	clearTimeout(this.snap_timer);
	this.down_x = parseInt(evt.pageX) - this.pageleft;
	this.down_y = parseInt(evt.pageY) - this.pagetop;

	if (this.double_click)
		this.zoom_in(evt);
	else {
		if (evt.shiftKey) {
			/*
			 * Selects area if shift key is pressed
			 */
			this.select_area = document.createElement("div");
			this.eventtarget.appendChild(this.select_area);
			this.select_area.style.position = "absolute";
			// this.select_area.style.top =
			//	parseFloat(evt.pageY) - this.top + "px";
			// this.select_area.style.left =
			//	parseFloat(evt.pageX) - this.left + "px";
			if (!document.all) {
				this.select_area.style.backgroundColor =
				    "white";
			}
			this.select_area.style.border = "1px solid grey";
			this.select_area.style.opacity = 0.4;
			// this.select_area.style.filter =
			//	"alpha(opacity = 20)";
		} else {
			this.double_click = true;
			this_object = this;
			setTimeout("this_object.double_click = false", 300);
			this.clicked = true;
			this.moving = true;
		}
	}
}

/*
 * Double click
 */
kMap.prototype.doubledown = function(evt) {
	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	this.down_x = parseInt(evt.pageX) - this.pageleft;
	this.down_y = parseInt(evt.pageY) - this.pagetop;
	this.zoom_in(evt);
}

kMap.prototype.zoom_in = function(evt) {
	var steps = 20;
	var force = "no";
	// var ddx = this.down_x - evt.pageX;
	// var ddy = this.down_y - evt.pageY;
	ddx = this.client_x - this.left - this.width / 2;
	ddy = this.client_y - this.top - this.height / 2;
	for (var i = 0; i <= steps; i++) {
		var dx = ddx * (1 - i / steps);
		var dy = ddy * (1 - i / steps);
		var z = this.zoom +
			(Math.floor(this.zoom + 1) - this.zoom) * (i / steps);
		this_object = this;
		if (i == steps) {
			// alert(z);
			z = Math.round(z)
			    force = "yes";
		}
		if (z > this.ui_max_zoom)
			z = this.ui_max_zoom;
		var cmd = "this_object.dx=" + dx + ";this_object.dy=" + dy +
			";this_object.zoom=" + z + ";this_object.draw('" +
			force + "')";
		setTimeout(cmd, 20 * i);
	}

}

/*
 * Snap Functions:
 * The tiles are 256x256 pixels. If the zoomlevel is not an integer,
 * the tiles have a size that is different to 256x256. Because the Browser
 * has to zoom the images the quality is not so good.
 *
 * Snap zooms to an integer zoomlevel
 */
kMap.prototype.snap = function() {
	var z = Math.round(this.zoom);
	this.zoom = z;
	this.draw("yes");
}

kMap.prototype.autosnap = function() {
	if (this.snapping) {
		this_object = this;
		window.clearTimeout(this.snap_timer);
		if (typeof(this_object.snap) == "function") {
			/* No idea but keyboard action... */
			this_object.snap_timer = setTimeout(function() {
				this_object.snap();
			}, 500);
		}
	}
}

kMap.prototype.snap_zoom = function(sn) {
	this.snapping = sn;
}

kMap.prototype.snap_zoom_dynamic = function(sn) {
	this.snapping_dynamic = sn;
}

/*
 * If all tiles for a layer finished loading this function is called
 */
kMap.prototype.img_loaded = function(evt, int_zoom) {
	try {
		evt.srcElement.style.visibility = "";
	}
	catch(e) {
	}
	this.load_stat_array[int_zoom]--;
	if (this.load_stat_array[int_zoom] == 0) {
		if (int_zoom > this.visible_layer)
			this.visible_layer = int_zoom;
		this.draw();
	}
}

/*
 * Mouse Move
 */
kMap.prototype.move = function(evt) {
	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	this.double_click = false;
	this.client_x = parseInt(evt.pageX) - this.pageleft;
	this.client_y = parseInt(evt.pageY) - this.pagetop;

	if (this.moving) {
		if (this.moveBlocked)
			return;

		var dx = this.down_x - this.client_x;
		var dy = this.down_y - this.client_y;
		var x = this.x + dx;
		var y = this.y + dy;
		// this.visualDiv.style.top = y;
		// this.visualDiv.style.left = x;

		this.moveBlocked = true;
		this.world.style.top = -dy + "px";
		this.world.style.left = -dx + "px";
		this_object = this;
		setTimeout(function() {
			this_object.moveBlocked = false;
		}, 1);
		if (Math.abs(dx) > 100 || Math.abs(dy) > 100) {
			this.world.style.top = 0 + "px";
			this.world.style.left = 0 + "px";
			this.x = x;
			this.y = y;
			this.down_x = parseInt(evt.pageX) - this.pageleft;
			this.down_y = parseInt(evt.pageY) - this.pagetop;
			this.latlon();
			this.xydraw(x, y, this.zoom);
		}
	}
	if (this.select_area != null) {
		// this.select_area.style.width =
		//		parseFloat(evt.pageX) - this.left -
		//		parseFloat(this.select_area.style.left) + "px";
		// this.select_area.style.height =
		//		parseFloat(evt.pageY) - this.top -
		//		parseFloat(this.select_area.style.top) + "px";
		var width = parseFloat(evt.pageX) - this.down_x - this.pageleft;
		var height = parseFloat(evt.pageY) - this.down_y - this.pagetop;
		if (width > 0) {
			var left = this.down_x;
		} else {
			var left = evt.pageX - this.pagetop;
			width = width * -1;
		}
		if (height > 0) {
			var top = this.down_y - this.top;
		} else {
			var top = evt.pageY - this.pagetop - this.top;
			height = height * -1;
		}
		this.select_area.style.top = top + "px";
		this.select_area.style.left = left + "px";
		this.select_area.style.width = width + "px";
		this.select_area.style.height = height + "px";
	}
}

/*
 * Mouse up
 */
kMap.prototype.up = function(evt) {
	if (this.moving) {
		this.x = this.x + this.down_x - parseInt(evt.pageX) +
			this.pageleft;
		this.y = this.y + this.down_y - parseInt(evt.pageY) +
			this.pagetop;
		this.moving = false;

		/* GPS (WGS84) Koordinaten berechnen */

		this.world.style.top = 0 + "px";
		this.world.style.left = 0 + "px";
		// this.lon = (this.x / Math.pow(2,this.zoom) /
		//		this.wx - 0.5) * 360;
		// this.lat = this.y2lat((this.y / Math.pow(2, this.zoom) /
		//		this.wy - 0.5) * -360);
		this.latlon();
		this.draw();
	}

	if (this.select_area != null) {
		this.areaSelect();
		this.select_area.parentNode.removeChild(this.select_area);
		this.select_area = null;
	}
}

/*
 * If Shift-key is held you can select an area with the mouse....
 */
kMap.prototype.areaSelect = function() {
	var top = parseFloat(this.select_area.style.top);
	var left = parseFloat(this.select_area.style.left);
	var width = parseFloat(this.select_area.style.width);
	var height = parseFloat(this.select_area.style.height);

	if (isNaN(width))
		return;
	if (isNaN(height))
		return;

	var x = left + width / 2;
	var y = top + height / 2;
	this.latlon();
	var deltaz1 = parseFloat(this.width) / width;
	var deltaz2 = parseFloat(this.height) / height;
	if (deltaz1 < deltaz2) {
		deltaz = deltaz1;
	} else {
		deltaz = deltaz2;
	}
	deltazoom = Math.log(deltaz) / Math.log(2);
	this.client_x = x + this.left;
	this.client_y = y + this.top;
	// console.log(this.client_x + ":" + this.client_y);
	this.latlon();

	this.zoom = Math.floor(this.zoom + deltazoom);
	this.dx = 0;
	this.dy = 0;
	this.draw();
}

/*
 * This is an important funktion for WGS84 calculations.
 * It's used in many places
 * Please hands off it you don't exactly know what you do.
 */
kMap.prototype.latlon = function()
{
	this.dx = this.client_x - this.left - this.width / 2;
	this.dy = this.client_y - this.top - this.height / 2;
	this.lon = ((this.x + this.dx) /
			Math.pow(2, this.zoom) / this.wx - 0.5) * (360);
	this.lat = this.y2lat(((this.y + this.dy) /
			Math.pow(2, this.zoom) / this.wy - 0.5) * (-360));
	// document.getElementById("lon").firstChild.nodeValue=this.lon;
}

/*
 * Set the map Centre and zoom level
 */
kMap.prototype.set_centre = function(lat, lon, z) {
	this.zoom = z;
	this.lat = lat;
	this.lon = lon;

	if (this.visible_layer == null) {
		this.visible_layer = Math.floor(this.zoom)
	}
	this.dx = 0;
	this.dy = 0;
	this.draw("yes");
}

kMap.prototype.set_centre_from_location = function(lat, lon, z) {
	var param = decodeURIComponent(document.location.search);
	var params = param.substr(param.indexOf("?") + 1).split("&");
	var values = { "lat": lat, "lon": lon, "z": z };
	var aliases = { "lng": "lon", "zoom": "z" };

	for (var p in params) {
		v = params[p].split("=");
		if (v.length < 2)
			continue;

		if (v[0] in aliases)
			v[0] = aliases[v[0]];
		values[v[0]] = parseFloat(v[1]);
	}

	this.set_centre(values.lat, values.lon, values.z);
}

/*
 * Mouse wheel support for zoom
 */
kMap.prototype.wheel = function(evt)
{
	if (evt.preventDefault)
		evt.preventDefault();
	else
		evt.returnValue = false;

	this.latlon();

	var delta = 0;
	if (!evt)		/* For IE. */
		evt = window.event;
	if (evt.wheelDelta) {	/* IE/Opera. */
		delta = evt.wheelDelta / 120;
		if (window.opera) {
			delta = delta * 2;
		}
	} else if (evt.detail) { /** Mozilla case. */
		delta = -evt.detail / 3;
	}

	// var normal = e.detail ? e.detail * -1 : e.wheelDelta / 120;
	if (evt.altKey) {
		extraspeed = 2;
	} else {
		extraspeed = 10;
	}

	delta *= Math.abs(delta);
	this.zoom = this.zoom + this.zoomspeed * delta * extraspeed;

	this.zoomspeed = this.zoomspeed + 0.01;
	this.lastzoomspeed = evt.altKey ? 0 : this.zoomspeed;
	this.zoomdelta = delta;
	if (this.zoom > this.ui_max_zoom)
		this.zoom = this.ui_max_zoom;
	if (this.zoom < this.ui_min_zoom)
		this.zoom = this.ui_min_zoom;

	this_object = this;
	setTimeout(function() {
		this_object.zoomspeed = this_object.zoomspeed - 0.01;
	}, 100);

	for (var i in this.overlay_array)
		if (this.overlay_array[i].zoom_start)
			this.overlay_array[i].zoom_start();

	// this.xydraw(this.x, this.y, this.zoom);
	this.draw();

	if (this.snapping_dynamic) {
		if (this.zoom_timer)
			clearTimeout(this.zoom_timer);
		this_object = this;
		this_object.zoom_timer =
			setTimeout("this_object.zoominert()", 100);
	}
}

kMap.prototype.zoominert = function() {
	this.zoom_timer = 0;

	var intzoom = Math.floor(this.zoom);
	if (this.zoomdelta > 0.1)
		intzoom++;

	if (intzoom > this.ui_max_zoom + 0.001)
		return;
	if (intzoom < this.ui_min_zoom - 0.001)
		return;

	var diff = intzoom - this.zoom;
	if (Math.abs(diff) > this.lastzoomspeed * 50)
		return;

	this.latlon();

	if (Math.abs(diff) < 0.01) {
		this.zoom = intzoom; /* + 0.00001 */
		this.lastzoomspeed = 0;

		this.draw("yes");
	} else {
		this.zoom += diff * 0.3;
		this.lastzoomspeed *= 1 - 0.3;

		this_object = this;
		this_object.zoom_timer =
			setTimeout("this_object.zoominert()", 50);

		this.draw();
	}
}

/*
 * Oops, this function is duplicated
 */
kMap.prototype.img_loaded = function(evt, int_zoom)
{
	// if(evt.target)
	//	evt.target.style.visibility = "";
	// else
	//	try {
	//		evt.srcElement.style.visibility = "";
	//	} catch(e) {}
	try {
		evt.srcElement.style.visibility = "";
	}
	catch(e) {
	}
	// console.log("z: " + int_zoom);
	this.load_stat_array[int_zoom]--;
	if (this.load_stat_array[int_zoom] == 0) {
		this.visible_layer = int_zoom;
		this.draw();
	}
}

/*
 * This function draws the map and uses "xydraw" and "real_xydraw"
 * Hands off please!!
 */
kMap.prototype.draw = function(force) {
	// console.log(this.dx, this.dy, this.client_x, this.client_y);
	this.x = (this.lon / 360 + 0.5) *
		Math.pow(2, this.zoom) * this.wx - this.dx;
	this.y = (this.lat2y(-this.lat) / 360 + 0.5) *
		Math.pow(2, this.zoom) * this.wy - this.dy;

	// document.getElementById("lat").firstChild.nodeValue =
	//	this.lon + ":" + this.zoom + ":" + this.wy + ":" + this.dx;
	// document.getElementById("lon").firstChild.nodeValue = this.y;

	// console.log("--" + this.x + ":" + this.y);
	this.xydraw(this.x, this.y, this.zoom, force);
}

/*
 * Same parameters as real_xydraw
 * Here some timeouts guaranties SPEED
 * Hands OFF!!!
 */
kMap.prototype.xydraw = function(middle_x, middle_y, zoom, force) {
	if (this.xydraw_blocked && (force != 'yes'))
		return;

	/* Different policies can be set here for better visual effect when
	 * zooming in.  */
	var int_zoom = zoom > this.max_zoom ? this.max_zoom :
			Math.floor(zoom + 0.5);

	this.xydraw_blocked = true;

	this.real_xydraw(middle_x, middle_y, zoom, int_zoom);
	this.draw_overlays(middle_x, middle_y, zoom, int_zoom);

	if (this.load_stat_array[int_zoom] == 0)
		this.visible_layer = int_zoom;
	else {
		// debug(this.load_stat_array, int_zoom, this.visible_layer);

		if (this.load_stat_array[int_zoom] != 0) {
			if (int_zoom > this.visible_layer) {
				this.real_xydraw(middle_x, middle_y, zoom,
						this.visible_layer);
			}
		}
	}

	var this_object = this;
	setTimeout(function() {
		this_object.xydraw_blocked = false;
	}, 20);
	this.autosnap();
}

/* Deletes all layers */
kMap.prototype.clear_map = function() {
	var divs = this.world.childNodes;

	for (var i = 0; i < divs.length; i++)
		if (divs.item(i).getAttribute("name") != "overlay")
			this.world.removeChild(divs.item(i));

	for (var i = Math.floor(this.ui_min_zoom); i <= this.ui_max_zoom; i++)
		this.load_stat_array[i] = 0;
}

/*
 * This function is a bit chaotic - it's opimized for speed and not for reading
 * Please don't touch !!!!!!!
 */
kMap.prototype.real_xydraw = function(middle_x, middle_y, zoom, int_zoom) {
	this.frames++;
	var faktor = Math.pow(2, (zoom - int_zoom));

	// this.world.style.border = "3px solid red";
	var divs = this.world.childNodes;
	for (var i = 0; i < divs.length; i++) {
		if (divs.item(i).getAttribute("zoomlevel") != int_zoom)
			if (divs.item(i).getAttribute("name") != "overlay")
				divs.item(i).style.display = "none";
		if (divs.item(i).getAttribute("zoomlevel") == int_zoom)
			var div = divs.item(i);
	}

	/* Create div for zoomlevel if not existing */
	if (!div) {
		var div = document.createElement("div");
		div.setAttribute("zoomlevel", int_zoom);
		// div.style.opacity = 0.2;
		div.style.position = "absolute";
		this.world.appendChild(div);
	}
	this.visualDiv = div;
	div.setAttribute("notLoaded", 0);

	/* Set div coordinates to the middle of the world */
	div.style.top = this.height / 2 + "px";
	div.style.left = this.width / 2 + "px";
	div.style.display = "none";

	var start_x = Math.floor(middle_x / this.wx / faktor) -
		Math.ceil(this.width / this.wx / 2 / faktor);
	var start_y = Math.floor(middle_y / this.wy / faktor) -
		Math.ceil(this.height / this.wy / 2 / faktor);

	var end_x = Math.floor(middle_x / this.wx / faktor) +
		Math.ceil(this.width / this.wx / 2 / faktor) + 1;
	var end_y = Math.floor(middle_y / this.wy / faktor) +
		Math.ceil(this.height / this.wy / 2 / faktor) + 1;

	// document.getElementById("zoom").firstChild.nodeValue =
	//	this.frames + " : " + start_x + " : " + end_x + " : " +
	//	int_zoom + " : " + this.zoom;
	var anz = Math.pow(2, int_zoom); /* available openstreetmap images */
					 /* for zoomlevel */
	// console.log(start_x);
	for (var x = start_x; x < end_x; x++) {
		for (var y = start_y; y < end_y; y++) {
			/* Make world recursive */
			var img_x = x % anz;
			var img_y = y;
			if (img_x < 0)
				/* Modulo function gives negative value for
				 * negative numbers.  */
				img_x = img_x + anz;
			/* Place the images */
			var url = this.make_url(img_x, img_y, int_zoom);
			var id = "img" + int_zoom + ":" + x + ":" + y;
			var img = null;
			if (img_y < 0 || img_y >= anz)
				url = "http://www.openstreetmap.org/" +
					"openlayers/img/404.png";

			var imgs = div.getElementsByTagName("img");
			for (var i = 0; i < imgs.length; i++) {
				if (imgs.item(i).getAttribute("theid") == id) {
					img = imgs.item(i);
					break;
				}
			}

			// img=document.getElementById(id);

			if (img == null) {
				// console.log("new pic");
				var img = document.createElement("img");
				// load events handling
				Event.attach(img, "load", this.img_loaded,
					     this, false, int_zoom);
				this.load_stat_array[int_zoom]++;

				img.setAttribute("theid", id);
				img.setAttribute("onload",
						 "this.style.visibility=''");

				img.style.visibility = "hidden";

				img.style.position = "absolute";

				img.setAttribute("src", url);
				div.appendChild(img);
			}
			img.setAttribute("keep", "yes");
			var top = (y * this.wy * faktor - middle_y);
			var left = (x * this.wx * faktor - middle_x);
			img.style.top = Math.round(top) + "px";
			img.style.left = Math.round(left) + "px";
			img.style.width =
			    Math.round(this.wx * faktor + left) -
			    Math.round(left) + "px";
			img.style.height =
			    Math.round(this.wx * faktor + top) -
			    Math.round(top) + "px";
		}
	}
	// if (this.clicked)
	//	console.log("loadstat: " + this.load_stat_array[int_zoom]);
	var imgs = div.getElementsByTagName("img");
	for (var i = imgs.length - 1; i >= 0; i--) {
		var img = imgs.item(i);
		if (img.getAttribute("keep") != "yes") {
			if (img.complete == false)
				if (window.opera)
					this.load_stat_array[int_zoom]--;
			img.parentNode.removeChild(img);
		}
		img.setAttribute("keep", "no");
	}
	this.clicked = false;
	div.style.display = "";

	/* Call all callback things */
	for (var i = 0; i < this.callbackHandlers.length; i++)
		this.callbackHandlers[i].callback();
}

/*
 *  Map Types
 */
kMap.prototype.osma_url = function(img_x, img_y, int_zoom) {
	var server_num = (img_x + img_y) % 3 + 1;

	switch (server_num) {
	case 1:
		server = "a";
		break;
	case 2:
		server = "b";
		break;
	case 3:
		server = "c";
		break;
	}

	var url = "http://" + server + ".tah.openstreetmap.org/Tiles/tile/" +
		int_zoom + "/" + img_x + "/" + img_y + ".png";
	return url;
}

kMap.prototype.mapnik_url = function(img_x, img_y, int_zoom) {
	var server = [ "a", "b", "c" ][(img_x + img_y) % 3];

	return url = "http://" + server + ".tile.openstreetmap.org/" +
		int_zoom + "/" + img_x + "/" + img_y + ".png";
}

kMap.prototype.bikemap_url = function(img_x, img_y, int_zoom) {
	var server_num = (img_x + img_y) % 3 + 1;

	switch (server_num) {
	case 1:
		server = "a";
		break;
	case 2:
		server = "b";
		break;
	case 3:
		server = "c";
		break;
	}

	var url = "http://" + server +
		".andy.sandbox.cloudmade.com/tiles/cycle/" + int_zoom + "/" +
		img_x + "/" + img_y + ".png";
	return url;
}

kMap.prototype.y2lat = function(a) {
	return 180 / Math.PI * (2 * Math.atan(Math.exp(a * Math.PI / 180)) -
				Math.PI / 2);
}

kMap.prototype.lat2y = function(a) {
	return 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 +
				a * (Math.PI / 180) / 2));
}

kMap.prototype.add_control = function(control) {
	control.initialize(this);
	if ((typeof control.callback) == "function")
		this.callbackHandlers.push(control);
}

/*
 *  Displays the Copyright
 *  Could be done with innerHTML - but innerHTML is evil ;-)
 *  But here it would not be a problem
 */
kMap.prototype.showCopyright = function(bool) {
	if (this.copyright) {
		if (bool)
			this.copyright_div.style.display = "";
		else
			this.copyright_div.style.display = "none";
	}
}

kMap.prototype.copyright = function(control) {
	copyright = document.createElement("p");
	var img = document.createElement("img");
	var cca = document.createElement("a");

	cca.setAttribute("href",
			"http://creativecommons.org/licenses/by-sa/2.0/");
	img.setAttribute("src",
			"http://i.creativecommons.org/l/by-sa/2.0/80x15.png");
	img.setAttribute("align", "top");
	img.style.border = "0px";
	cca.appendChild(img);
	copyright.appendChild(cca);

	var osma = document.createElement("a");
	osma.setAttribute("href", "http://www.openstreetmap.org");
	var t1 = document.createTextNode(" openstreetmap ");
	osma.appendChild(t1);
	copyright.appendChild(osma);

	var khtmla = document.createElement("a");
	khtmla.setAttribute("href", "http://www.khtml.org");
	var t1 = document.createTextNode(" khtml.org ");
	khtmla.appendChild(t1);
	copyright.appendChild(khtmla);

	copyright.style.position = "absolute";
	copyright.style.left = 5 + "px";
	copyright.style.backgroundColor = "white";
	copyright.style.opacity = 0.6;
	copyright.style.fontSize = "11px";
	return copyright;
}

//
//
//
//   Attach - Function
//
//
// Rest of Code ist a Copy from
// http://hiveminds.org.hiveminds.co.uk/phpBB/viewtopic6b81.html?t=2930
//
// This is for events in Object Oriented Programming
// Ab hier wird nur die Methode "attach" definiert. Am besten einfach ignorieren.
// 
//
//

/***
<member name="$a" type="global static method">
   <summary>Loops through each argument in the supplied argument object and puts it into an array.</summary>
   <param name="a">Argument object to loop through.</param>
   <returns>Array</returns>
</member>
***/
function $a(a)
{
	var r = new Array();
	for (var i = 0, l = a.length; i < l; i++) {
		r.push(a[i]);
	}
	return r;
}

// Creates an object called "Event" if one doesn't already exist (IE).
if (!Event) {
	var Event = { };
}

/***
<member name="Event.attach" type="static method">
   <summary>Attach an event listener to an object.</summary>
   <param name="o">Object whose event to attach.</param>
   <param name="t">Type of event.</param>
   <param name="f">Method to fire when event is raised.</param>
   <param name="fc">Context of called method f. Defaults to object o.</param>
   <param name="c">Use capture (not available in IE).</param>
   <param name="*">Arguments to pass to function f.</param>
</member>
***/

Event.attach = function(o, t, f, fc, c)
{
	var a = (arguments.length > 5 ?
			$a(arguments).slice(5, arguments.length) :
			new Array());
	var fn = function(e) {
		a.unshift(e || window.event);
		return f.apply((fc ? fc : o), a);
	}
	if (o.addEventListener) {
		if (navigator.appName.indexOf("Netscape") == -1) {
			if (t == "DOMMouseScroll")
				t = "mousewheel";
		}
		if (navigator.userAgent.indexOf("Safari") != -1) {
			if (t == "DOMMouseScroll")
				o.onmousewheel = fn;
			else
				o.addEventListener(t, fn, c);
		} else
			o.addEventListener(t, fn, c);
	} else {
		if (t == "DOMMouseScroll")
			o.attachEvent("onmousewheel", fn);
		else
			o.attachEvent("on" + t, fn);
	}
};
/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */
var hexcase=0;function hex_md5(a){return rstr2hex(rstr_md5(str2rstr_utf8(a)))}function hex_hmac_md5(a,b){return rstr2hex(rstr_hmac_md5(str2rstr_utf8(a),str2rstr_utf8(b)))}function md5_vm_test(){return hex_md5("abc").toLowerCase()=="900150983cd24fb0d6963f7d28e17f72"}function rstr_md5(a){return binl2rstr(binl_md5(rstr2binl(a),a.length*8))}function rstr_hmac_md5(c,f){var e=rstr2binl(c);if(e.length>16){e=binl_md5(e,c.length*8)}var a=Array(16),d=Array(16);for(var b=0;b<16;b++){a[b]=e[b]^909522486;d[b]=e[b]^1549556828}var g=binl_md5(a.concat(rstr2binl(f)),512+f.length*8);return binl2rstr(binl_md5(d.concat(g),512+128))}function rstr2hex(c){try{hexcase}catch(g){hexcase=0}var f=hexcase?"0123456789ABCDEF":"0123456789abcdef";var b="";var a;for(var d=0;d<c.length;d++){a=c.charCodeAt(d);b+=f.charAt((a>>>4)&15)+f.charAt(a&15)}return b}function str2rstr_utf8(c){var b="";var d=-1;var a,e;while(++d<c.length){a=c.charCodeAt(d);e=d+1<c.length?c.charCodeAt(d+1):0;if(55296<=a&&a<=56319&&56320<=e&&e<=57343){a=65536+((a&1023)<<10)+(e&1023);d++}if(a<=127){b+=String.fromCharCode(a)}else{if(a<=2047){b+=String.fromCharCode(192|((a>>>6)&31),128|(a&63))}else{if(a<=65535){b+=String.fromCharCode(224|((a>>>12)&15),128|((a>>>6)&63),128|(a&63))}else{if(a<=2097151){b+=String.fromCharCode(240|((a>>>18)&7),128|((a>>>12)&63),128|((a>>>6)&63),128|(a&63))}}}}}return b}function rstr2binl(b){var a=Array(b.length>>2);for(var c=0;c<a.length;c++){a[c]=0}for(var c=0;c<b.length*8;c+=8){a[c>>5]|=(b.charCodeAt(c/8)&255)<<(c%32)}return a}function binl2rstr(b){var a="";for(var c=0;c<b.length*32;c+=8){a+=String.fromCharCode((b[c>>5]>>>(c%32))&255)}return a}function binl_md5(p,k){p[k>>5]|=128<<((k)%32);p[(((k+64)>>>9)<<4)+14]=k;var o=1732584193;var n=-271733879;var m=-1732584194;var l=271733878;for(var g=0;g<p.length;g+=16){var j=o;var h=n;var f=m;var e=l;o=md5_ff(o,n,m,l,p[g+0],7,-680876936);l=md5_ff(l,o,n,m,p[g+1],12,-389564586);m=md5_ff(m,l,o,n,p[g+2],17,606105819);n=md5_ff(n,m,l,o,p[g+3],22,-1044525330);o=md5_ff(o,n,m,l,p[g+4],7,-176418897);l=md5_ff(l,o,n,m,p[g+5],12,1200080426);m=md5_ff(m,l,o,n,p[g+6],17,-1473231341);n=md5_ff(n,m,l,o,p[g+7],22,-45705983);o=md5_ff(o,n,m,l,p[g+8],7,1770035416);l=md5_ff(l,o,n,m,p[g+9],12,-1958414417);m=md5_ff(m,l,o,n,p[g+10],17,-42063);n=md5_ff(n,m,l,o,p[g+11],22,-1990404162);o=md5_ff(o,n,m,l,p[g+12],7,1804603682);l=md5_ff(l,o,n,m,p[g+13],12,-40341101);m=md5_ff(m,l,o,n,p[g+14],17,-1502002290);n=md5_ff(n,m,l,o,p[g+15],22,1236535329);o=md5_gg(o,n,m,l,p[g+1],5,-165796510);l=md5_gg(l,o,n,m,p[g+6],9,-1069501632);m=md5_gg(m,l,o,n,p[g+11],14,643717713);n=md5_gg(n,m,l,o,p[g+0],20,-373897302);o=md5_gg(o,n,m,l,p[g+5],5,-701558691);l=md5_gg(l,o,n,m,p[g+10],9,38016083);m=md5_gg(m,l,o,n,p[g+15],14,-660478335);n=md5_gg(n,m,l,o,p[g+4],20,-405537848);o=md5_gg(o,n,m,l,p[g+9],5,568446438);l=md5_gg(l,o,n,m,p[g+14],9,-1019803690);m=md5_gg(m,l,o,n,p[g+3],14,-187363961);n=md5_gg(n,m,l,o,p[g+8],20,1163531501);o=md5_gg(o,n,m,l,p[g+13],5,-1444681467);l=md5_gg(l,o,n,m,p[g+2],9,-51403784);m=md5_gg(m,l,o,n,p[g+7],14,1735328473);n=md5_gg(n,m,l,o,p[g+12],20,-1926607734);o=md5_hh(o,n,m,l,p[g+5],4,-378558);l=md5_hh(l,o,n,m,p[g+8],11,-2022574463);m=md5_hh(m,l,o,n,p[g+11],16,1839030562);n=md5_hh(n,m,l,o,p[g+14],23,-35309556);o=md5_hh(o,n,m,l,p[g+1],4,-1530992060);l=md5_hh(l,o,n,m,p[g+4],11,1272893353);m=md5_hh(m,l,o,n,p[g+7],16,-155497632);n=md5_hh(n,m,l,o,p[g+10],23,-1094730640);o=md5_hh(o,n,m,l,p[g+13],4,681279174);l=md5_hh(l,o,n,m,p[g+0],11,-358537222);m=md5_hh(m,l,o,n,p[g+3],16,-722521979);n=md5_hh(n,m,l,o,p[g+6],23,76029189);o=md5_hh(o,n,m,l,p[g+9],4,-640364487);l=md5_hh(l,o,n,m,p[g+12],11,-421815835);m=md5_hh(m,l,o,n,p[g+15],16,530742520);n=md5_hh(n,m,l,o,p[g+2],23,-995338651);o=md5_ii(o,n,m,l,p[g+0],6,-198630844);l=md5_ii(l,o,n,m,p[g+7],10,1126891415);m=md5_ii(m,l,o,n,p[g+14],15,-1416354905);n=md5_ii(n,m,l,o,p[g+5],21,-57434055);o=md5_ii(o,n,m,l,p[g+12],6,1700485571);l=md5_ii(l,o,n,m,p[g+3],10,-1894986606);m=md5_ii(m,l,o,n,p[g+10],15,-1051523);n=md5_ii(n,m,l,o,p[g+1],21,-2054922799);o=md5_ii(o,n,m,l,p[g+8],6,1873313359);l=md5_ii(l,o,n,m,p[g+15],10,-30611744);m=md5_ii(m,l,o,n,p[g+6],15,-1560198380);n=md5_ii(n,m,l,o,p[g+13],21,1309151649);o=md5_ii(o,n,m,l,p[g+4],6,-145523070);l=md5_ii(l,o,n,m,p[g+11],10,-1120210379);m=md5_ii(m,l,o,n,p[g+2],15,718787259);n=md5_ii(n,m,l,o,p[g+9],21,-343485551);o=safe_add(o,j);n=safe_add(n,h);m=safe_add(m,f);l=safe_add(l,e)}return Array(o,n,m,l)}function md5_cmn(h,e,d,c,g,f){return safe_add(bit_rol(safe_add(safe_add(e,h),safe_add(c,f)),g),d)}function md5_ff(g,f,k,j,e,i,h){return md5_cmn((f&k)|((~f)&j),g,f,e,i,h)}function md5_gg(g,f,k,j,e,i,h){return md5_cmn((f&j)|(k&(~j)),g,f,e,i,h)}function md5_hh(g,f,k,j,e,i,h){return md5_cmn(f^k^j,g,f,e,i,h)}function md5_ii(g,f,k,j,e,i,h){return md5_cmn(k^(f|(~j)),g,f,e,i,h)}function safe_add(a,d){var c=(a&65535)+(d&65535);var b=(a>>16)+(d>>16)+(c>>16);return(b<<16)|(c&65535)}function bit_rol(a,b){return(a<<b)|(a>>>(32-b))};
