/*
Name:       Mercator Map JS - SHBB
Version:    0.6.4 (Februar 18 2010)
Author:     Finn Rudolph
Company:    http://stadtwerk.org/

License:    This code is licensed under a Creative Commons 
            Attribution-Noncommercial 3.0 Unported License 
            (http://creativecommons.org/licenses/by-nc/3.0/).

            You are free:
                + to Share - to copy, distribute and transmit the work
                + to Remix - to adapt the work

            Under the following conditions:
                + Attribution. You must attribute the work in the manner specified by the author or licensor 
                  (but not in any way that suggests that they endorse you or your use of the work). 
                + Noncommercial. You may not use this work for commercial purposes. 

            + For any reuse or distribution, you must make clear to others the license terms of this work.
            + Any of the above conditions can be waived if you get permission from the copyright holder.
            + Nothing in this license impairs or restricts the author's moral rights.
*/

/* Mercator Map constructor */
function MercatorMap ()
{
	/* Closure for this */
	var my = this;

	/* Setting option defaults */
	this.defaults =
	{
		containerId:      'map',           /* Id of the map container */
		containerClass:   'MercatorMap',   /* Class of the map container */
		collisionRange:   8,               /* Collision range in pixel */
		jsonId:           'json'           /* Id of the container holding the JSON */
	};

	/* Initiate MercatorMap */
	this.init = function (options)
	{
		/* Evaluate options */
		var optionsArray = ['containerId', 'containerClass', 'collisionRange', 'jsonId'];
		var max = optionsArray.length;
		for (var i = 0; i < max; i++)
		{
			var name = optionsArray[i];
			this[name] = (options !== undefined && options[name] !== undefined) ? options[name] : my.defaults[name];
		}
		
		/* Global variable to store the id of the active (clicked) point on the map */
		this.activePointId = 'undefined';
		
		/* Proceed if image map exists */
		if(document.getElementById(my.containerId))
		{
			my.createStructure();
			my.getMapBoundaries();
			my.getPointsToLocalize();
			my.setMapScaleFactor();
			my.localize();
			my.detectPointCollisions();
			my.drawPoints();
		}
	};
	
	this.createStructure = function()
	{
		/* Set link to map image global */
		var mapImage = document.getElementById(my.containerId);
		var parent = mapImage.parentNode;
		
		/* Create map div container and clone image */
		var mapDiv = my.Tool.createElement('div', my.containerId, my.containerClass);
		mapDiv.style.width = mapImage.width+'px';
		mapDiv.style.height = mapImage.height+'px';
		
		/* Clone image inside map div */
		var clonedMapImage = mapImage.cloneNode(false);
		clonedMapImage.setAttribute('id', my.containerId+'_image');
		mapDiv.appendChild(clonedMapImage);
		
		/* Create content div */
		var contentDiv = my.Tool.createElement('div', my.containerId+'_content', my.containerClass);
		
		/* Create hash anchor element*/
		var anchor = my.Tool.createElement('a', my.containerId+'_anchor', my.containerClass);
		anchor.setAttribute('name', my.containerId+'_anchor');
		
		/* Add hash anchor, map and content elements to DOM */
		parent.appendChild(anchor);
		parent.appendChild(mapDiv);
		parent.appendChild(contentDiv);
		
		/* Delete original map image */
		parent.removeChild(mapImage);

		/* Make map, content and image elements globally available */
		this.map = document.getElementById(my.containerId); 
		this.content = document.getElementById(my.containerId+'_content'); 
		this.mapImage = document.getElementById(my.containerId+'_image');		
		
		/* Create new contact element */
		var contactElement = document.createElement('div');
		my.Tool.setClassName(contactElement, 'contact');
		var html = '<div id="leftColumn"><h2>Bitte w&auml;hlen Sie eine Beratungsstelle aus</h2></div>';
		contactElement.innerHTML = html;
		my.content.appendChild(contactElement);
	};
	
	this.getMapBoundaries = function()
	{
		var JSON = my.mapImage.alt;
		this.mapBoundaries = eval('(' + JSON + ')');
	};

	this.getPointsToLocalize = function()
	{
		/* Get the JSON stored between the li tags of the json ul */
		var jsonContainer = document.getElementById(my.jsonId);
		var companies = jsonContainer.getElementsByTagName('li');

		/* Evaluate JSON and store the data in a pointArray */
		var json;
		var jsonArray = [];
		var max = companies.length;
		for(i=0;i<max;i++)
		{
			json = [eval('(' + companies[i].firstChild.data + ')')];
			jsonArray = jsonArray.concat(json);
		}
		
		/* Make the array global */
		this.pointArray = jsonArray;
	};
	
	this.localize = function()
	{
		var max = my.pointArray.length;
		for(i=0;i<max;i++)
		{
			my.pointArray[i].i = i;
			my.pointArray[i].x = my.longitudeToPixel(my.pointArray[i].lon);
			my.pointArray[i].y = my.latitudeToPixel(my.pointArray[i].lat);
		}
	};
	
	this.detectPointCollisions = function()
	{
		var groupArray = [];
		var pointA, pointB, distanceX, distanceY;
		var rangeInPixel = my.collisionRange;
 
		/* Loop through all points */
		var max = my.pointArray.length;
		for(i=0;i<max;i++)
		{
			pointA = my.pointArray[i];
 
			/* Check if pointA collides with any of the other points */
			for(j=0;j<max;j++)
			{
				/* Ignore the pointA */
				if(j !== i )
				{
					pointB = my.pointArray[j];
 
					/* Get distance between A and B on the x-axis */
					distanceX = pointA.x - pointB.x;
					if(distanceX < 0) { distanceX = distanceX * -1; }
 
					/* Get distance between A and B on the y-axis */
					distanceY = pointA.y - pointB.y;
					if(distanceY < 0) { distanceY = distanceY * -1; }
 
					/* A and B are collided if they both overlap on the x _and_ the y axis */
					if( (distanceX <= rangeInPixel) && (distanceY <= rangeInPixel) )
					{
						groupArray[groupArray.length] = [i,j];
					}
				}
			}
		}
		
		/* Group the collision results */
		my.groupCollisions(groupArray);
	};
	
	this.groupCollisions = function(array)
	{
		/* Make the groupArray global */
		this.groupArray = array;
		
		/* Start recursive grouping of the groupArray with the first element */
		my.recursiveGrouping(0);
		
		/* Loop through grouped array */
		var max = my.groupArray.length;
		for(i=0;i<max;i++)
		{
			/* Remove duplicates from array */
			group = my.groupArray[i];
			
			/* Set collision group for affected points */
			k = group.length;
			for(j=0;j<k;j++)
			{
				my.pointArray[group[j]].collisionGroup = i;
			}
		}
	};
	
	this.recursiveGrouping = function(arrayIndex)
	{
		var groupCount = my.groupArray.length;
		var startIndex = arrayIndex +1;
		var group = my.groupArray[arrayIndex];
		var elementCount, indexToCopy, isNotInGroup;
		
		/* Don't check the last element, for there is nothing left to be checked against */
		if(arrayIndex === groupCount)
		{
			return;
		}
		
		/* Loop through all group elements */
		var max = group.length;
		for(i=0;i<max;i++)
		{
			pointId = group[i];
			
			/* Try to find pointId in any group, but the current (therefore begin loop at arrayIndex+1) */
			for(j=startIndex;j<groupCount;j++)
			{
				/* Catch undefined */
				if(my.groupArray[j] === undefined) { break; }
				
				/* Check pointId against every element */
				elementCount = my.groupArray[j].length;
				for(k=0;k<elementCount;k++)
				{
					/* If it matches copy the other element to the group and delete the matching group from the array */
					if(pointId == my.groupArray[j][k])
					{
						/* Get index of the other element */
						indexToCopy = (k === 0) ? 1 : 0;						
						
						/* Check if the element to copy is not already in the group */
						isNotInGroup = true;
						for(l=0;l<max;l++)
						{
							if(group[l] == my.groupArray[j][indexToCopy])
							{
								isNotInGroup = false;
								break;
							}
						}
						
						/* If it is not already in the group copy the other element to the group */
						if(isNotInGroup)
						{
							group.push(my.groupArray[j][indexToCopy]);
						}						
						
						/* Delete the group the pointId was found in */
						my.groupArray.splice(j,1);
						
						/* Recall this function to find other matches */
						my.recursiveGrouping(arrayIndex);
						break;
					}
				}
			}			
		}
		
		/* Try next index */
		my.recursiveGrouping(arrayIndex+1);		
	};
	
	this.drawPoints = function()
	{
		/* Create outer points div */
		var pointsDiv = my.Tool.createElement('div',my.containerId+'_points','points');
		pointsDiv.style.top = '-'+my.mapImage.height+'px';
		pointsDiv.style.height = my.mapImage.height+'px';
		pointsDiv.style.width = my.mapImage.width+'px';
	
		/* Create points */
		var point;
		var max = my.pointArray.length;
		for(i=0;i<max;i++)
		{
			/* Create point div elements */
			point = my.createPoint(i);
			pointsDiv.appendChild(point);
		}
		
		/* Create group div and append it to the points div - needs to be inside the pointsDiv because of the IE quirks */
		var groupDiv = my.Tool.createElement('div', my.containerId+'_group', 'group');
		pointsDiv.appendChild(groupDiv);
		
		/* Add pointsDiv to DOM */
		my.map.appendChild(pointsDiv);
		
		/* Make group element globally available */
		this.groupDiv = document.getElementById(my.containerId+'_group');
		this.pointsDiv = document.getElementById(my.containerId+'_points');
	};
	
	this.createPoint = function(pointArrayIndex)
	{
		var i = pointArrayIndex;
		
		/* Create div element */
		point = my.Tool.createElement('div', 'point_'+i, 'default');
		point.i = i;
		
		/* If point is member of a group */
		if(my.pointArray[i].collisionGroup !== undefined)
		{
			/* Add index of the groupArray to the element */
			point.groupIndex = my.pointArray[i].collisionGroup;

			/* Add group event on mouseover */
			my.Tool.addEvent(point, 'mouseover', my.groupMouseOver);
		}
		else
		{
			/* Add events */
			my.Tool.addEvent(point, 'mouseover', my.pointMouseOver);
			my.Tool.addEvent(point, 'mouseout', my.pointMouseOut);
		}
		
		/* Position element */
		point.style.marginLeft = my.pointArray[i].x+'px';
		point.style.marginTop = my.pointArray[i].y+'px';
		
		/* Store position in point element */
		point.x = my.pointArray[i].x;
		point.y = my.pointArray[i].y;
		
		/* Add click event to all points */
		my.Tool.addEvent(point, 'click', my.pointMouseClick);

		return point;
	};
	
	this.groupMouseOver = function()
	{
		var point, centerX, centerY;
		var x = 0, y = 0;
		
		/* Loop through all points in this group */
		var group = my.groupArray[this.groupIndex];
		var max = group.length;
		for(i=0;i<max;i++)
		{
			point = document.getElementById('point_'+group[i]);
			
			/* Remove this event from group point */
			my.Tool.removeEvent(point, 'mouseover', my.groupMouseOver);
			
			/* Add point events */
			my.Tool.addEvent(point, 'mouseover', my.pointMouseOver);
			my.Tool.addEvent(point, 'mouseout', my.pointMouseOut);
			
			/* Change CSS class to group_active or group_default */
			className = (point.i === my.activePointId) ? 'group_active' : 'group_default';
			my.Tool.setClassName(point, className);
	
			/* Sum x and y coordinates */
			x += my.pointArray[group[i]].x;
			y += my.pointArray[group[i]].y;
		}
		
		/* Get the center of the group in pixel from the upper left map corner */
		centerX = Math.round(x / max);
		centerY = Math.round(y / max);

		/* Center group element absolute on the group center */
		var left = (centerX + my.mapImage.offsetLeft) - (my.groupDiv.offsetWidth / 2) - parseInt(my.Tool.getStyle(my.map, 'padding-left'));
		var top = (centerY + my.mapImage.offsetTop) - (my.groupDiv.offsetHeight / 2) - parseInt(my.Tool.getStyle(my.map, 'padding-top'));		
		my.groupDiv.style.marginLeft = left + 'px';
		my.groupDiv.style.marginTop = top + 'px';		
		
		/* Unhide group element */
		my.groupDiv.style.visibility = 'visible';

		/* Spread points */
		var factor = 3;
		var distanceX, distanceY;
		for(i=0;i<max;i++)
		{
			point = document.getElementById('point_'+group[i]);
			x = my.pointArray[group[i]].x;
			y = my.pointArray[group[i]].y;
			
			/* Calculate the new distance to the center */
			distanceX = Math.round((x - centerX) * factor);
			distanceY = Math.round((y - centerY) * factor);
			
			/* New Position is old position plus the new distance */
			x += distanceX;
			y += distanceY;
			
			/* Position element */
			point.style.marginLeft = x + 'px';
			point.style.marginTop = y + 'px';
			
			/* Store new position in point element */
			point.x = x;
			point.y = y;
		}
		
		/* Store group index in group element */
		my.groupDiv.groupIndex = this.groupIndex;

		/* Checks if the mouse has left the group background radius */
		my.Tool.addEvent(document, 'mousemove', my.mouseOverGroupCheck);
	};

	this.groupMouseOut = function()
	{
		/* Hide group element */
		my.groupDiv.style.visibility = 'hidden';
		
		/* Reposition points in the group */
		var group = my.groupArray[my.groupDiv.groupIndex];
		var max = group.length;
		var point, className;
		for(i=0;i<max;i++)
		{
			point = document.getElementById('point_'+group[i]);
	
			/* Reposition element */
			point.style.marginLeft = my.pointArray[group[i]].x + 'px';
			point.style.marginTop = my.pointArray[group[i]].y + 'px';
			
			/* Store new position in point element */
			point.x = my.pointArray[group[i]].x;
			point.y = my.pointArray[group[i]].y;
			
			/* Remove default point events */
			my.Tool.removeEvent(point, 'mouseover', my.pointMouseOver);
			my.Tool.removeEvent(point, 'mouseout', my.pointMouseOut);
			
			/* Change CSS class back to default or active */
			className = (point.i === my.activePointId) ? 'active' : 'default';
			my.Tool.setClassName(point, className);
			
			/* Add group event on mouseover */
			my.Tool.addEvent(point, 'mouseover', my.groupMouseOver);
		}
	};
	
	this.mouseOverGroupCheck = function(e)
	{
		var radius = my.groupDiv.offsetWidth / 2;
		
		/* Get absolute coordinates of group center in pixel */
		var x = my.groupDiv.offsetLeft + radius + my.Tool.absoluteOffset(my.mapImage).x;
		var y = my.groupDiv.offsetTop + radius + my.Tool.absoluteOffset(my.mapImage).y;
		
		/* Get distance of mouse position to group center in pixel */
		x = x - my.Tool.getMouseX(e);
		y = y - my.Tool.getMouseY(e);
		
		/* Make negative distance values positive */
		x = (x < 0) ? x*-1 : x;
		y = (y < 0) ? y*-1 : y;
		
		/* If distance is bigger than the radius the user has left the group */
		if(x >= radius || y >= radius)
		{
			/* Remove this event */
			my.Tool.removeEvent(document, 'mousemove', my.mouseOverGroupCheck);

			/* Reset group points */
			my.groupMouseOut();
		}
	};
	
	this.pointMouseOver = function()
	{
		/* Different CSS classes for group points */
		var className = (this.groupIndex !== undefined) ? 'group_active' : 'active';
		
		/* Change CSS class to active */
		my.Tool.setClassName(this, className);
		
		/* Create and position name element */
		var name = my.Tool.createElement('div', 'name_'+this.i, 'name');
		name.innerHTML = '<p>'+my.pointArray[this.i].name+'</p>';		
		name.style.marginLeft = this.x + 'px';
		name.style.marginTop = this.y + 'px';
		
		/* Append name element to points div */
		my.pointsDiv.appendChild(name);
	};
	
	this.pointMouseOut = function()
	{
		/* Different CSS classes for group points */
		var className = (this.groupIndex !== undefined) ? 'group_default' : 'default';
		
		/* Only reset points that are not active */
		if(this.i !== my.activePointId)
		{
			/* Change CSS class to default */
			my.Tool.setClassName(this, className);
		}
		
		/* Delete name element from DOM */
		var nameElement = document.getElementById('name_'+this.i);
		if(nameElement)
		{
			my.pointsDiv.removeChild(nameElement);
		}
	};
	
	this.pointMouseClick = function()
	{
		/* Jump to hash anchor */
		window.location.hash = my.containerId+'_anchor';
		
		/* Different CSS classes for group points */
		var className = (this.groupIndex !== undefined) ? 'group_active' : 'active';
		
		/* Change CSS class to active */
		my.Tool.setClassName(this, className);
		
		/* Reset activated point CSS class to default */
		var activePoint = document.getElementById('point_'+my.activePointId);
		if(activePoint)
		{			
			/* Check if point is a group member and belongs to the active group */
			var pointHasGroup = (activePoint.groupIndex !== undefined);
			var pointIsInSameGroup = (activePoint.groupIndex === this.groupIndex);
			
			/* Different CSS classes for points that are in _this_ group */
			className = (pointHasGroup && pointIsInSameGroup ) ? 'group_default' : 'default';
			my.Tool.setClassName(activePoint, className);
		}
		
		/* Register this point as active */
		my.activePointId = this.i;
		
		/* Display the company data */
		my.displayCompany(this.i);
	};
	
	this.displayCompany = function(pointArrayIndex)
	{
		/* Get company data from pointArray */
		var company = my.pointArray[pointArrayIndex];
		
		/* Remove previous company data from content container */
		var previousContactElement = my.content.firstChild;
		if(previousContactElement)
		{
			my.content.removeChild(previousContactElement);
		}
		
		/* Create new contact element */
		var contactElement = document.createElement('div');
		my.Tool.setClassName(contactElement, 'contact');
		
		/* Create HTML */
		var html = '<div id="leftColumn"><h2>Beratungsstelle '+company.name+'</h2>';
		
		/* Create HTML - manager data */
		html += '<h3>Leitung</h3>';
		var manager;
		var max = company.manager.length;
		for(i=0;i<max;i++)
		{
			manager = company.manager[i];
			html += '<p><b>'+manager.vorname+' '+manager.name+'</b> <em>'+manager.titel+'</em><br/></p>';
		}
		html += '</div>';
		
		/* Create HTML - contact data */
		html += '<div id="rightColumn"><h3>Kontaktdaten</h3><p>';
		html += company.strasse+'<br/>';
		html += company.plz+' '+company.ort+'<br/><br/>';
		html += 'Tel: '+company.telefon+'<br/>';
		html += 'Fax: '+company.fax+'<br/><br/>';
		html += 'E-Mail: <a href="mailto:'+company.email+'">'+company.email+'</a><br/>';
		html += 'Web: <a href="'+company.www+'">'+company.www.replace(/http:\/\//g, '');
		html += '</a><br/></p></div><div class="clear"></div>';
		
		/* Write HTML to contact element */
		contactElement.innerHTML = html;
		
		/* Add contact element to DOM */
		my.content.appendChild(contactElement);
	};	

	this.setMapScaleFactor = function()
	{
		var leftTopLatitude = my.mapBoundaries.leftTop.lat;

		/* Calculate y for a mapScaleFactor of 10000 */
		my.mapScaleFactor = 10000;
		var y = my.latitudeToPixel(leftTopLatitude);

		/* Calculate ratio between y and the actual map height in px */
		var ratio = my.mapImage.height / (my.mapImage.height - y);

		/* Set the right mapScaleFactor global */
		my.mapScaleFactor = my.mapScaleFactor * ratio;
	};
	
	this.longitudeToPixel = function(longitude)
	{
		var pixelsPerLongitudeDegree = my.mapImage.width / (my.mapBoundaries.leftTop.lon - my.mapBoundaries.rightBottom.lon);
		var x = (my.mapBoundaries.leftTop.lon - longitude) * pixelsPerLongitudeDegree;
		x = Math.round(x);
		return x;
	}; 
 
	this.latitudeToPixel = function(latitude)
	{
		var radianBottomLatitude = my.mapBoundaries.rightBottom.lat * (Math.PI / 180);
		var radianLatitude = latitude * (Math.PI / 180);		
		var convertedHeightBottom = my.mapScaleFactor * Math.log((1 + Math.sin(radianLatitude)) / (1 - Math.sin(radianLatitude)));
		var convertedHeightPoint = my.mapScaleFactor * Math.log((1 + Math.sin(radianBottomLatitude)) / (1 - Math.sin(radianBottomLatitude)));
		var totalHeight = Math.abs(convertedHeightPoint - convertedHeightBottom);
		var y = my.mapImage.height - totalHeight;
		y = Math.round(y);
		return y;
	};	
	
	/* Useful tools */
	this.Tool =
	{
		/* Creates HTML elements */
		createElement: function(type, id, optional)
		{
			var element = document.createElement(type);
			element.setAttribute('id', id);
			my.Tool.setClassName(element, optional);
			return element;
		},
		
		/* Set class attribute - className must be set for the IE family */
		setClassName: function(element, className)
		{
			if(element && className !== undefined)
			{
				element.setAttribute('class',className);
				element.setAttribute('className',className);
			}
		},
		
		/* Get Mouse x coordinates */
		getMouseX: function(e)
		{
			var x = 0;
			if(e.pageX) { x = e.pageX; }
			else if(e.clientX) { x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; }
			return x;
		},

		/* Get Mouse y coordinates */
		getMouseY: function(e)
		{
			var y = 0;
			if(e.pageY) { y = e.pageY; }
			else if(e.clientY) { y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; }
			return y;
		},
		
		/* Returns absolute offset of an element by adding all offsets through the DOM */
		absoluteOffset: function(element)
		{
			var offset = {};
			offset.x = 0;
			offset.y = 0;
			while(element) 
			{
				offset.x += element.offsetLeft;
				offset.y += element.offsetTop;
				element = element.offsetParent;
			}
			return offset;
		},
		
		/* Get rendered CSS style settings from element */
		getStyle: function(element, css)
		{
			var style = '';
			if(document.defaultView && document.defaultView.getComputedStyle)
			{
				style = document.defaultView.getComputedStyle(element, '').getPropertyValue(css);
			}
			else if(element.currentStyle)
			{
				css = css.replace(/\-(\w)/g, function (strMatch, p1){ return p1.toUpperCase(); });
				style = element.currentStyle[css];
			}
			return style;
		},
	
		/* Adds event to an element */
		addEvent: function(obj, type, fn)
		{
			if(obj.addEventListener)
			{
				obj.addEventListener(type, fn, false);
			}
			else if(obj.attachEvent)
			{
				obj["e"+type+fn] = fn;
				obj[type+fn] = function() { obj["e"+type+fn]( window.event ); };
				obj.attachEvent( "on"+type, obj[type+fn] );
			}
		},
		
		/* Remove events */
		removeEvent: function( obj, type, fn )
		{
			if (obj.removeEventListener)
			{
				obj.removeEventListener( type, fn, false );
			}
			else if (obj.detachEvent)
			{
				/* The IE breaks if you're trying to detach an unattached event http://msdn.microsoft.com/en-us/library/ms536411(VS.85).aspx */
				if(obj[type+fn] === undefined)
				{
					alert('Tool.removeEvent » Pointer to detach event is undefined - perhaps you are trying to detach an unattached event?');
				}
				obj.detachEvent( 'on'+type, obj[type+fn] );
				obj[type+fn] = null;
				obj['e'+type+fn] = null;
			}
		}
	};  
}

/* Is called when DOM structure of the site has been loaded */
domReady(function()
{
	var map = new MercatorMap();
	map.init({containerId: 'map', jsonId: 'MercatorMapJSON' });
	
});