Icons = {
	distance:
	{
		url:'/media/images/routes/markers/marker.png', 
		width:20,
		height:15,
		offsetx:10,
		offsety:8
	},
	start:
	{
		url:'/media/images/routes/route_start.png', 
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	end:
	{
		url:'/media/images/routes/route_finish.png', 
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	point:
	{
		url:'http://www.run.com/img/markers/nodetypes/point.png', 
		width:10,
		height:10,
		offsety:5
	},
	pointHighlight:
	{
		url:'/media/images/routes/pointHighlight.png',
		width:10,
		height:10,
		offsetx:5,
		offsety:5
	},
	aid:
	{
		url:'/media/images/routes/route_aid.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	water:
	{
		url:'/media/images/routes/route_water.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	bathroom:
	{
		url:'/media/images/routes/route_bathroom.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	parking:
	{
		url:'/media/images/routes/route_parking.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	scenic:
	{
		url:'/media/images/routes/route_scenic.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	offroad:
	{
		url:'/media/images/routes/route_offroad.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	hazard:
	{
		url:'/media/images/routes/route_hazard.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	},
	railroad:
	{
		url:'/media/images/routes/route_railroad.png',
		width:27,
		height:49,
		offsetx:13,
		offsety:49
	}
};

function Routes(map_id, options)
{
	// Create the map instance
	var map = new GMap2(document.getElementById(map_id));
	var distanceManager = new DistanceManager();;
	var routesOverlays = new RoutesOverlays();
	var stateManager = new StateManager();
	var statisticsManager = new StatisticsManager();
	var routes = this; // reference in event listeners, so don't remove this.;
	var directions = new GDirections(null, document.createElement("input"));
	var converter = new Converter();
	var reverseGeocoder = new ReverseGeocoder();
	
	this.addressLoaded = true;
	var address = [];
	
	var updateStatsCallback;

	this.settings = { 
		minEditZoom: 9,
		elevationAjaxCount: 0,
		followPaths: true,
		distanceType: 'english',
		distanceId: 'totalDistance',
		elevationIdPrefix: 'elevation',
		estimatedTimeId: 'estimatedTime',
		trackPointsId: 'trackPoints',
		readOnly: false,
		avoidHighways: true
	};

	this.setDefaultControls = function() 
	{
		map.addControl(new GLargeMapControl3D());
		map.addControl(new GMapTypeControl());
		map.addControl(new GOverviewMapControl());
		map.addMapType(G_PHYSICAL_MAP);
		map.enableScrollWheelZoom();
		map.disableDoubleClickZoom();
	}

	// Allow overrides of our default settings here.
	for(var i in options) 
	{
		this.settings[i] = options[i];
	}
	

	this.getMap = function()
	{
		return map;
	}
	
	this.getMapStartData = function()
	{
		zoom = map.getZoom();
		center = map.getCenter();
		payload = {zoomLevel: zoom, latitude: center.y, longitude: center.x};
		return payload;
	}
	
	this.undo = function()
	{
		stateManager.undo();
		statisticsManager.update();
		if(typeof graphUpdateCallback != "undefined") {
			graphUpdateCallback();
		}
		return false;
	}
	this.redo = function()
	{
		stateManager.redo();
		statisticsManager.update();
		if(typeof graphUpdateCallback != "undefined") {
			graphUpdateCallback();
		}
		return false;
	}
	
	this.clear = function()
	{
		stateManager.clear();
		routesOverlays.clearOverlays();
		routesOverlays.line = new GPolyline();
		routesOverlays.elevation = [];
		routesOverlays.pointsOfInterest = [];
		map.addOverlay(routesOverlays.line);
		routesOverlays.initializeListeners();
		distanceManager.updateDistanceMarkers([]);
		statisticsManager.clear();
		statisticsManager.display();
		if(typeof graphUpdateCallback != "undefined") {
			graphUpdateCallback();
		}
	}
	
	this.outAndBack = function()
	{
		routesOverlays.outAndBack();
		verticies = [];
		for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
		{
			verticies.push(routesOverlays.line.getVertex(i));
		}
		distanceManager.updateDistanceMarkers(verticies);
		statisticsManager.update();
		routesOverlays.setStartEndMarkers();
	}
	
	this.closeLoop = function()
	{
		if(routesOverlays.line.getVertexCount() > 0)
		{
			if(routes.settings.followPaths)
			{
				this.initiateRoute(routesOverlays.line.getVertex(routesOverlays.line.getVertexCount()-1), routesOverlays.line.getVertex(0));
				routesOverlays.setStartEndMarkers();
			} 
			else 
			{
				routesOverlays.addPoint(routesOverlays.line.getVertex(0));
			}
		}
	}

	this.initiateRoute = function(start, end)
	{
		directions.clear();
		travelMode = this.settings.avoidHighways ? G_TRAVEL_MODE_WALKING : G_TRAVEL_MODE_DRIVING;
		directions.loadFromWaypoints([start, end], { travelMode : travelMode, getPolyline : true });
	}
	
	this.updateStats = function()
	{
		statisticsManager.update();
	}
	
	this.setUpdateStatsCallback = function(callback)
	{
		updateStatsCallback = callback;
	}
	
	this.getStat = function(statName)
	{
		return statisticsManager[statName];
	}
	
	this.loadRoute = function(points, markers)
	{
		verticies = []
		for(i = 0; i < points.length; i++)
		{
			routesOverlays.elevation[i] = parseFloat(points[i][2]);
			verticies.push(new GLatLng(points[i][0], points[i][1]));
		}
		if(markers != null)
		{
			for(i = 0; i < markers.length; i++)
			{
				if(markers[i][0] != null && markers[i][1] != null)
				{
					routesOverlays.addPointOfInterest(this.createIconMarker(markers[i][2], new GLatLng(markers[i][0], markers[i][1]), false, false, false));
				}
			}
		}
		routesOverlays.line = new GPolyline(verticies);
		map.addOverlay(routesOverlays.line);
		distanceManager.updateDistanceMarkers(verticies);
		routesOverlays.setStartEndMarkers();
	}
	this.setElevations = function(elevations)
	{
		routesOverlays.elevation = elevations;
		statisticsManager.update();
	}
	
	this.getPoints = function()
	{
		points = []
		for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
		{
			points.push([routesOverlays.line.getVertex(i).lat(), routesOverlays.line.getVertex(i).lng(), routesOverlays.elevation[i]]);
		}
		return points;
	}
	
	this.getPointsOfInterest = function()
	{
		markers = [];
		for(i = 0; i < routesOverlays.pointsOfInterest.length; i++)
		{
			markers.push([routesOverlays.pointsOfInterest[i].getLatLng().lat(), routesOverlays.pointsOfInterest[i].getLatLng().lng(), routesOverlays.pointsOfInterest[i].type]);
		}
		return markers;
	}
	
	this.getAddress = function()
	{
		return address;
	}
	
	this.getGraphingData = function()
	{
		returnArray = new Array();
		points = new Array();
		filterPoints = true;
		multiplier = 1;
		if(routesOverlays.line.getVertexCount() > 100 && jQuery.browser.msie == true)
		{
			filterPoints = true;
			multiplier = Math.floor(routesOverlays.line.getVertexCount()/100);
		}
		for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
		{
			if(!filterPoints || i%multiplier == 0)
			{
				points.push(routesOverlays.line.getVertex(i));
			}
		}
		//x coords = distance
		returnArray["x"] = distanceManager.calculateDistances(points);
		returnArray["xType"] = this.settings.distanceType == 'english' ? 'miles' : 'km';
		//y coords = elevation
		elevationArray = new Array();
		for(i = 0; i < routesOverlays.elevation.length; i++)
		{
			if(!filterPoints || i%multiplier == 0)
			{
				if(this.settings.distanceType == 'metric')
				{
					elevationArray.push(routesOverlays.elevation[i]);
				}
				else
				{
					elevationArray.push(converter.convert(routesOverlays.elevation[i], 'meters', 'ft'));
				}
			}
		}
		returnArray["y"] = elevationArray;
		returnArray["yType"] = this.settings.distanceType == 'english' ? 'ft' : 'meters';
		return returnArray;
	}
	
	this.setUnits = function(type)
	{
		if(type == "english" || type == "metric")
		{
			oldType = this.settings.distanceType;
			if(oldType != type)
			{
				$("#speedUnits").text(type=="metric" ? "km/h" : "miles/h");
				$("#avgSpeed").val( converter.convert($("#avgSpeed").val(), oldType=="metric" ? "km" : "miles", oldType=="metric" ? "miles" : "km").toFixed(2));
				$("#weightUnits").text(type=="metric" ? "kg" : "lbs");
				$("#weight").val( converter.convert($("#weight").val(), oldType=="metric" ? "kg" : "lbs", oldType=="metric" ? "lbs" : "kg").toFixed(2));
				
			}
			this.settings.distanceType = type;
			//get line verticies and update distance markings
			points = [];
			for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
			{
				points.push(routesOverlays.line.getVertex(i));
			}
			if(points.length > 0)
			{
				distanceManager.updateDistanceMarkers(points);
			}
			//will need to update stats also
			statisticsManager.update();
			if(typeof graphUpdateCallback != "undefined") {
				graphUpdateCallback();
			}
		}
	}

	this.showAddress = function(address) 
	{
		this.addressLoaded = false;
		geocoder = new GClientGeocoder();
		if(typeof(address) == "undefined") 
		{
			return;
		}
		geocoder.getLocations(address, function(response) 
		{
			if (!response || response.Status.code != 200) 
			{
				alert("Sorry, we were unable to geocode that address");
			}
			else
			{
				// map.clearOverlays(); // this was breaking zooming in and out, and mouse over stuff.
				place = response.Placemark[0];
				point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);
				map.setCenter(point, 13);
				routes.addressLoaded = true;
			}
		});
	}
	
	this.centerMap = function(lat, lng, zoom)
	{
		point = new GLatLng(lat, lng);
		map.setCenter(point, zoom);
	}
		
	this.createIconMarker = function(name, point, clickable, replacement, draggable)
	{
		custIcon = new GIcon();
		custIcon.iconSize = new GSize(Icons[name].width, Icons[name].height);
		custIcon.iconAnchor = new GPoint(Icons[name].offsetx,Icons[name].offsety);

		if(Icons[name].url.indexOf('#'))
		{
			custIcon.image = Icons[name].url.replace("#", replacement);
			marker = new LabeledMarker(point,{icon: custIcon, clickable: clickable, draggable: draggable, bouncy: false, dragCrossMove: true, labelText: replacement, labelClass:'loopsDistanceMarker', labelOffset:new GSize(-10, -7)});		    
		}
		else
		{
			custIcon.image = Icons[name].url;
			marker = new GMarker(point,{icon: custIcon, clickable: clickable, draggable: draggable, bouncy: false, dragCrossMove: true});
		}
		marker.type = name;
		return marker;
	}
	
	this.addHighlightedPoint = function(index)
	{
		marker = routes.createIconMarker('pointHighlight', routesOverlays.line.getVertex(index), false);
		map.addOverlay(marker);
		return marker;
	}
	
	this.addPointOfInterest = function(name, point)
	{
		marker = this.createIconMarker(name, point, false, 0, true);
		routesOverlays.registerPointOfInterest(marker);
	}
	
	this.removeHighlightedPoint = function(marker)
	{
		map.removeOverlay(marker);
	}

	this.initializeListeners = function() 
	{
		GEvent.addListener(map, "click", function(overlay, point)
		{
			try
			{
				if (point) 
				{
					if (this.getZoom() < routes.settings.minEditZoom)
					{
						alert('You are zoomed too far out to add points');
						return;
					}

					// Determine if we follow a path or not.
					if(routes.settings.followPaths && routesOverlays.line.getVertexCount() > 0)
					{
						routes.initiateRoute(routesOverlays.line.getVertex(routesOverlays.line.getVertexCount()-1), point);
					} 
					else 
					{
						routesOverlays.addPoint(point);
					}
				}
				else if (overlay)
				{
					//printd(overlay);
				}
			}
			catch(e) 
			{
				alert('map click handler exception: ' + e);
			}
		});

		GEvent.addListener(directions, "load", function()
		{
			polyline = directions.getPolyline();
			lastPoint = routesOverlays.line.getVertex(routesOverlays.line.getVertexCount()-1);
			for (var x = 0; x < polyline.getVertexCount(); x++)
			{
				newPoint = polyline.getVertex(x);
				if(newPoint.distanceFrom(lastPoint) > 10)
				{
					if(!routesOverlays.addPoint(newPoint))
						break;
				}
				lastPoint = newPoint;
			}
		});
	};
	if(routes.settings.readOnly == false)
		this.initializeListeners();
	
	GEvent.addListener(map, "zoomend", function(oldZoom, newZoom)
	{
		verticies = [];
		for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
		{
			verticies.push(routesOverlays.line.getVertex(i));
		}
		distanceManager.updateDistanceMarkers(verticies);
	});
	
	this.getElevation = function(lat, lng, pointNum) 
	{
		this.settings.elevationAjaxCount++;
		url = '/ajax/map/elevation.php?'+ 'lon=' + lng + '&lat=' + lat + '&pointId=' + pointNum;
		$.getJSON(url, function(data) 
		{
			routes.settings.elevationAjaxCount--;
			routesOverlays.elevation[data['pointId']] = data['srtm'];
			
			if(routes.settings.elevationAjaxCount == 0) 
			{
				// routes.graph.graph(routes.points);
				statisticsManager.update();
				//verticies = routes.getPoints();
				points = [];
		  		for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
		  		{
		  			points.push(routesOverlays.line.getVertex(i));
		  		}
				if(typeof graphUpdateCallback != "undefined") {
					graphUpdateCallback();
				}
			}
		});
	}

	
	function StateManager()
	{
		var currentState = {line:"", markers:"", elevation:""};
		var undoStack = [];
		var redoStack = [];
		
		this.generateCurrentState = function()
		{
			//get line verticies
			stateVerticies = [];
			for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
			{
				stateVerticies.push(routesOverlays.line.getVertex(i));
			}
			//get markers
			stateMarkers = [];
			for(i = 0; i < routesOverlays.pointsOfInterest.length; i++)
			{
				stateMarkers.push({type:routesOverlays.pointsOfInterest[i].type, point:routesOverlays.pointsOfInterest[i].getLatLng()});
			}
			//return object encapsulating verticies and markers
			elevations = routesOverlays.elevation.slice(0);
			return {line:stateVerticies, markers:stateMarkers, elevation:elevations};
		}
		
		this.updateState = function()
		{
			//called whenever something on the map changes (line modification, adding markers etc)
			if(typeof(currentState) != "undefined")
			{
				undoStack.push(currentState);
			}
			currentState = this.generateCurrentState();
			redoStack = [];
		}
		
		this.loadState = function(state)
		{
			if(state.line.length > 0)
			{
				routesOverlays.line = new GPolyline(state.line);
				map.addOverlay(routesOverlays.line);
				routesOverlays.initializeListeners();
				routesOverlays.setStartEndMarkers();
				routesOverlays.elevation = state.elevation;
				distanceManager.updateDistanceMarkers(state.line);
			}
			else
			{
				routesOverlays.line = new GPolyline();
				map.addOverlay(routesOverlays.line);
				routesOverlays.initializeListeners();
				routesOverlays.elevation = [];
				distanceManager.updateDistanceMarkers([]);
			}
			for(i = 0; i < state.markers.length; i++)
			{
				marker = routes.createIconMarker(state.markers[i].type, state.markers[i].point, false, 0, true);
				routesOverlays.addPointOfInterest(marker);
			}
		}
		
		this.undo = function()
		{
			if(undoStack.length > 0)
			{
				state = this.generateCurrentState();
				redoStack.push(state);
				routesOverlays.clearOverlays();
				routesOverlays.pointsOfInterest = [];
				currentState = undoStack.pop();
				this.loadState(currentState);
			}
		}
		
		this.redo = function()
		{
			if(redoStack.length > 0)
			{
				state = this.generateCurrentState();
				undoStack.push(state);
				routesOverlays.clearOverlays();
				routesOverlays.pointsOfInterest = [];
				currentState = redoStack.pop();
				this.loadState(currentState);
			}
		}
		
		this.clear = function()
		{
			undoStack = [];
			redoStack = [];
			currentState = {line:"", markers:"", elevation:""};
		}
		
		this.getPreviousState = function()
		{
			if(undoStack.length > 0)
			{
				return undoStack[undoStack.length-1];
			}
			else
			{
				return null;
			}
		}
		
		this.compareWithLastState = function()
		{
			latLng2 = currentState.line;
			latLng1 = undoStack[undoStack.length-1].line;
			for(i = 0; i < latLng1.length; i++)
			{
				if(latLng1[i].lat() != latLng2[i].lat() || latLng1[i].lng() != latLng2[i].lng())
				{
					//check next point in array 2, if exists to see if it's same as current one..checks if point was inserted
					if(latLng2.length > (i+1) && latLng1[i].lat() == latLng2[i+1].lat() && latLng1[i].lng() == latLng2[i+1].lng())
					{
						return -1 * i;
					}
					else
					{
						return i;
					}
				}
			}
			return null;
		}
	}

	function dataManager() 
	{	
		this.settings = { 
			
		};
	}

	// New object for RoutesOverlays YAY
	function RoutesOverlays() 
	{	
		var startMarker;
		var endMarker;
		this.line = new GPolyline(null);
		this.elevation = [];
		this.pointsOfInterest = [];
		map.addOverlay(this.line);
	
		this.initializeListeners = function() 
		{
			GEvent.addListener(this.line, "mouseover", function()
			{
				this.enableEditing();
			});
			GEvent.addListener(this.line, "mouseout", function()
			{
				this.disableEditing();
			});
			GEvent.addListener(this.line, "lineupdated", function()
			{
				vertexCount = this.getVertexCount();
				
				stateManager.updateState();
				index = stateManager.compareWithLastState();
				//check previous state.  Compare lengths to see if point added or if point modified
				//if modified, need to replace old elevation with new
				if(index < 0)
				{
					//negative means we inserted a vertex at that index
					//insert blank array element at the index, moving everythign back
					tempArray = routesOverlays.elevation.splice(Math.abs(index));
					routesOverlays.elevation.push("");
					for(i = 0; i < tempArray.length; i++)
					{
						routesOverlays.elevation.push(tempArray[i]);
					}
					routesOverlays.elevation[Math.abs(index)] = "";
				}
				else if(index != null)
				{
					//some point changed...regrab the elevation data
					routesOverlays.elevation[index] = "";
				}
				else
				{
					//default is added new point at end of line
					routesOverlays.elevation[vertexCount-1] = "";
				}

				verticies = [];
				for (i = 0; i < vertexCount; i++)
				{
					verticies.push(this.getVertex(i));
					if(typeof(elevation[i]) == "undefined" || elevation[i] == "" )
					{
						routes.getElevation(this.getVertex(i).lat(), this.getVertex(i).lng(), i);
					}
				}

				distanceManager.updateDistanceMarkers(verticies);

				// Now set the start and end makers after each click
				routesOverlays.setStartEndMarkers();

				if(typeof lineUpdatedCallback != "undefined") {
					lineUpdatedCallback(verticies);
				}
				/*
				if(vertexCount == 1)
				{
					//first point, get the address
					//geoCoder.getAddress(verticies[0].lat(), verticies[0].lng());
					geoCoder = new GClientGeocoder();
					
					geoCoder.getLocations(verticies[0], function(data){
						address = new Array();
						
						mark = reverseGeocoder.getBestMatchingPlacemark(data, routesOverlays.line.getVertex(0));						
						reverseGeocoder.getPlacemarkProperty(mark, '');
						
						address['country'] = reverseGeocoder.getPlacemarkProperty(mark, 'CountryNameCode');
						address['region'] = reverseGeocoder.getPlacemarkProperty(mark, 'AdministrativeAreaName');
						address['postal'] = reverseGeocoder.getPlacemarkProperty(mark, 'PostalCodeNumber');
						address['city'] = reverseGeocoder.getPlacemarkProperty(mark, 'LocalityName');
					});
					
				}
				*/
			});
		};
			
		
		this.setStartEndMarkers = function()
		{
			// Now set the start and end makers after each click
			if(typeof(startMarker) != "undefined")
			{
				map.removeOverlay(startMarker);
			}
			startMarker = routes.createIconMarker('start', this.line.getVertex(0), false);
			map.addOverlay(startMarker);

			if(typeof(endMarker) != "undefined")
			{
				map.removeOverlay(endMarker);
			}
			if(this.line.getVertexCount() > 1)
			{
				endMarker = routes.createIconMarker('end', this.line.getVertex(this.line.getVertexCount()-1), false);
				map.addOverlay(endMarker);
			}
		}

		this.addPoint = function(GLatLng) 
		{
			// ATTEMPTING to add more attributes to the GLatLng
			// But for some wierd reason after adding it to the insertVertex it kills off my attribute.
			GLatLng.origX = GLatLng.x;
			GLatLng.origY = GLatLng.y;
			this.line.insertVertex(this.line.getVertexCount(), GLatLng);
			return true;
		}
		
		this.clearOverlays = function()
		{
			map.removeOverlay(this.line);
			if(typeof(startMarker) != "undefined")
				map.removeOverlay(startMarker);
			if(typeof(endMarker) != "undefined")
				map.removeOverlay(endMarker);
			for(i = 0; i < this.pointsOfInterest.length; i++)
			{
				map.removeOverlay(this.pointsOfInterest[i]);
			}
		}
		
		this.registerPointOfInterest = function(marker)
		{
			this.addPointOfInterest(marker);
			stateManager.updateState();
		}
		
		this.addPointOfInterest = function(marker)
		{
			this.pointsOfInterest.push(marker);
			map.addOverlay(marker);
		}
		
		this.outAndBack = function()
		{
			for (i = this.line.getVertexCount()-2; i >= 0; i--)
			{
				this.addPoint(this.line.getVertex(i));
			}
		}
	}
	
	if(this.settings.readOnly == false)
		routesOverlays.initializeListeners();

	// New object for RoutesOverlays YAY
	function DistanceManager() 
	{	
		this.distanceMarkers = [];
		this.remainder = 0;
		this.settings = { 
			
		};

		this.getTotalDistance = function (points)
		{
			var distance = 0
			var dm = 1;
			if (routes.settings.distanceType == 'english'){	dm = 0.000621371192; }
			if (routes.settings.distanceType == 'metric'){ dm = 0.001; }
			for(var i = 1; i < points.length; i++) 
			{
				distance += points[i].distanceFrom(points[i-1]);
			}
			var d = (distance * dm) ;  

			return d;
		}

		//adds a distance (mile or km) marker to the map at the correct location on the polyline
		this.addDistanceMarker = function (i, totalDistance, remainder, dm, points)
		{
			remainder = totalDistance - this.distanceMarkers.length;
			var tempPoint = new GLatLng(points[i].y, points[i-1].x); 
			var segmentDistance = points[i].distanceFrom(points[i-1]) * dm;
			var xDist = points[i].x - tempPoint.x;
			var yDist = points[i-1].y - tempPoint.y ;
			if (Math.floor(segmentDistance) >= remainder) 
			{
				percentAlongLine = (1 - (remainder - segmentDistance) ) / segmentDistance; 
			}
			else
			{
				percentAlongLine = (1 - (remainder - segmentDistance) ) / segmentDistance;
			}
			distanceMarker = new GLatLng(tempPoint.y + ((1 - percentAlongLine) * yDist),tempPoint.x + percentAlongLine * xDist);
			var marker = this.createDistanceMarker(distanceMarker, points);
			
			if(map.getZoom() > 11 || (this.distanceMarkers.length+1)%10 == 0)
			{
				map.addOverlay(marker);
			}
			remainder -= 1;
			this.distanceMarkers.push(marker);
			return(remainder);
		}
			
		//clears all distance markers
		this.cleardistanceMarkers = function ()
		{
			for(var i = 0; i < this.distanceMarkers.length; i++) 
			{
				map.removeOverlay(this.distanceMarkers[i])
			}
			this.distanceMarkers = [];
			this.remainder = 0;
		}
		
		//checks to see if a new distance marker should be added between the latest point and the previous one.
		this.updateDistanceMarkers = function (points)
		{
			if(this.distanceMarkers.length > 0)
			{
				this.cleardistanceMarkers();
			}
			var distance = 0.0;
			var dm = 1.0;
			if (routes.settings.distanceType == 'english'){dm = 0.000621371192;}
			if (routes.settings.distanceType == 'metric') {dm = 0.001;}
			for(var i = 1; i < points.length; i++) {
				distance += dm * points[i].distanceFrom(points[i-1]);
				while (Math.floor(distance) > this.distanceMarkers.length)
				{
					this.remainder = this.addDistanceMarker(i, distance, this.remainder, dm, points);
				}
			}
		}
		
		this.createDistanceMarker = function (point, points)
		{
			marker = routes.createIconMarker('distance', point, false, ''+(this.distanceMarkers.length + 1), false);
			marker.nodeid = points.length - 1;
			return marker;
		}
		
		this.calculateDistances = function (points)
		{
			var distanceArray = [];
			distanceArray.push(0);
			var distance = 0
			var dm = 1;
			if (routes.settings.distanceType == 'english'){	dm = 0.000621371192; }
			if (routes.settings.distanceType == 'metric'){ dm = 0.001; }
			for(var i = 1; i < points.length; i++) 
			{
				distance += points[i].distanceFrom(points[i-1]) * dm;
				distanceArray.push(distance);
			}
			return distanceArray;
		}
	}
	
	function StatisticsManager()
	{
		this.elevationMin;
		this.elevationMax;
		this.elevationAscent;
		this.elevationDescent;
		this.elevationStart;
		this.elevationEnd;
		this.totalDistance;
		this.totalPoints;
		this.estimatedTime;
		
		this.weight;
		
		this.map = new Array();
		this.map["ft"] = "meters";
		this.map["meters"] = "ft";
		this.map["miles"] = "km";
		this.map["km"] = "miles";
		
		this.metricMap = new Array();
		this.metricMap['km'] = 'miles';
		this.metricMap['meters'] = 'ft';
		
		this.englishMap = new Array();
		this.englishMap['miles'] = 'km';
		this.englishMap['ft'] = 'meters';
		
		this.update = function()
		{
			this.clear();
			this.calculate();
			this.display();
			if(typeof updateStatsCallback != "undefined") {
				updateStatsCallback(this);
			}
		}
		
		this.display = function()
		{
			$("#"+routes.settings.distanceId).text(this.addUnits(this.totalDistance, routes.settings.distanceType == 'english' ? "miles" : "km"));
			$("#"+routes.settings.elevationIdPrefix+"Min").text(this.addUnits(this.elevationMin, "meters"));
			$("#"+routes.settings.elevationIdPrefix+"Max").text(this.addUnits(this.elevationMax, "meters"));
			$("#"+routes.settings.elevationIdPrefix+"Ascent").text(this.addUnits(this.elevationAscent, "meters"));
			$("#"+routes.settings.elevationIdPrefix+"Descent").text(this.addUnits(this.elevationDescent, "meters"));
			$("#"+routes.settings.elevationIdPrefix+"Start").text(this.addUnits(this.elevationStart, "meters"));
			$("#"+routes.settings.elevationIdPrefix+"End").text(this.addUnits(this.elevationEnd, "meters"));
			$("#"+routes.settings.estimatedTimeId).text(this.formatTime(this.estimatedTime));
			$("#"+routes.settings.trackPointsId).text(this.totalPoints);
		}
		
		this.clear = function()
		{
			this.elevationMin = null;
			this.elevationMax = null;
			this.elevationAscent = 0;
			this.elevationDescent = 0;
			this.elevationStart = null;
			this.elevationEnd = null;
			this.totalDistance = 0;
			this.totalPoints = 0;
			this.estimatedTime = 0;
		}
		
		this.calculate = function()
		{
			elevation = routesOverlays.elevation;
			verticies = [];
			this.totalPoints = routesOverlays.line.getVertexCount();
			for(i = 0; i < routesOverlays.line.getVertexCount(); i++)
			{
				verticies.push(routesOverlays.line.getVertex(i));
			}
			this.totalDistance = distanceManager.getTotalDistance(verticies);
			
			prevElevation = null;
			for(i = 0; i < elevation.length; i++)
			{
				if(this.elevationMin == null || this.elevationMin > elevation[i])
				{
					this.elevationMin = elevation[i];
				}
				if(this.elevationMax == null || this.elevationMax < elevation[i])
				{
					this.elevationMax = elevation[i];
				}
				if(prevElevation != null)
				{
					if(prevElevation > elevation[i])
					{
						this.elevationDescent += prevElevation - elevation[i];
					}
					if(prevElevation < elevation[i])
					{
						this.elevationAscent += elevation[i] - prevElevation;
					}
				}
				prevElevation = elevation[i];
			}
			if(elevation.length > 0)
			{
				this.elevationStart = elevation[0];
				this.elevationEnd = elevation[elevation.length-1];
			}
			
			//time...should move this somewhere else?
			speed = $("#avgSpeed").val() / (60 * 60); //get distance per second from distance per hour
			
			this.estimatedTime = this.totalDistance / speed;
		}
		
		this.formatTime = function(time)
		{
			//we get time in seconds
			seconds = time % 60;
			time = Math.floor(time/60);
			minutes = time % 60;
			time = Math.floor(time/60);
			hours = time;
			time = "";
			if(hours > 0)
				time = hours + "h ";
			else
				time = "0h ";
			if(minutes > 0)
				time += minutes + "m ";
			else
				time += "0m ";
			if(seconds > 0)
				time += Math.round(seconds) + "s";
			else
				time += "0s";
			return time;
			
		}
		
		this.addUnits = function(value, sourceType)
		{
			if(value != null)
			{
				if(routes.settings.distanceType == 'metric')
				{
					if(typeof(this.metricMap[sourceType]) != 'undefined')
					{
						return value.toFixed(2) + " " + sourceType;
					}
					else
					{
						return (converter.convert(value, sourceType, this.map[sourceType])).toFixed(2) + " " + this.map[sourceType];
					}
				}
				else
				{
					if(typeof(this.englishMap[sourceType]) != 'undefined')
					{
						return parseFloat(value).toFixed(2) + " " + sourceType;
					}
					else
					{
						return (converter.convert(value, sourceType, this.map[sourceType])).toFixed(2) + " " + this.map[sourceType];
					}
				}
			}
			else
				return "N/A";
		}
		
		this.getSpeed = function()
		{
			speed = $("#avgSpeed").val();
			if(routes.settings.distanceType == 'metric')
				sourceType = 'km';
			else
				sourceType = 'miles';
			return converter.convert(speed, sourceType, 'miles');
		}
		
		this.getWeight = function()
		{
			this.weight = $("#weight").val();
			if(routes.settings.distanceType == 'metric')
				sourceType = 'kg';
			else
				sourceType = 'lbs';
			return converter.convert(this.weight, sourceType, 'lbs');
		}
	}
	
	function ReverseGeocoder()
	{
		this.getBestMatchingPlacemark = function(response)
		{
			if (!response || response.Status.code != 200) return null;
			var j = -1;
			var accuracy = -1;
			for (var i = 0; i < response.Placemark.length; i++){
				var place = response.Placemark[i];
				newAccuracy = this.getPlacemarkProperty(place,'Accuracy');
				if (accuracy < newAccuracy) {
					j = i;
					accuracy = newAccuracy;
				}
			}
			if(j < 0 ) return null;
			return response.Placemark[j];
		}

		this.getPlacemarkProperty = function (placemark,propertyname)
		{
			for (var property in placemark) {
				if((property == propertyname)) {
					return String(placemark[property]);
				} else if (typeof(placemark[property]) == 'object') {
					var r = this.getPlacemarkProperty(placemark[property], propertyname);
					if (r != null) return r;
				}
			}
			return null;
		}
	}
	
	function LabeledMarker(latlng, options){
	    this.latlng = latlng;
	    this.labelText = options.labelText || "";
	    this.labelClass = options.labelClass || "markerLabel";
	    this.labelOffset = options.labelOffset || new GSize(0, 0);
	    
	    this.clickable = options.clickable || true;
	    
	    if (options.draggable) {
	    	// This version of LabeledMarker doesn't support dragging.
	    	options.draggable = false;
	    }
	    
	    GMarker.apply(this, arguments);
	}


	/* It's a limitation of JavaScript inheritance that we can't conveniently
	   extend GMarker without having to run its constructor. In order for the
	   constructor to run, it requires some dummy GLatLng. */
	LabeledMarker.prototype = new GMarker(new GLatLng(0, 0));


	// Creates the text div that goes over the marker.
	LabeledMarker.prototype.initialize = function(map) {
		// Do the GMarker constructor first.
		GMarker.prototype.initialize.apply(this, arguments);
		
		var div = document.createElement("div");
		div.className = this.labelClass;
		div.innerHTML = this.labelText;
		div.style.position = "absolute";
		map.getPane(G_MAP_MARKER_PANE).appendChild(div);

		if (this.clickable) {
			// Pass through events fired on the text div to the marker.
			var eventPassthrus = ['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'];
			for(var i = 0; i < eventPassthrus.length; i++) {
				var name = eventPassthrus[i];
				GEvent.addDomListener(div, name, newEventPassthru(this, name));
			}

			// Mouseover behaviour for the cursor.
			div.style.cursor = "pointer";
		}
		
		this.map = map;
		this.div = div;
	}

	function newEventPassthru(obj, event) {
		return function() { 
			GEvent.trigger(obj, event);
		};
	}

	// Redraw the rectangle based on the current projection and zoom level
	LabeledMarker.prototype.redraw = function(force) {
		GMarker.prototype.redraw.apply(this, arguments);
		
		// We only need to do anything if the coordinate system has changed
		if (!force) return;
		
		// Calculate the DIV coordinates of two opposite corners of our bounds to
		// get the size and position of our rectangle
		var p = this.map.fromLatLngToDivPixel(this.latlng);
		var z = GOverlay.getZIndex(this.latlng.lat());
		
		// Now position our DIV based on the DIV coordinates of our bounds
		this.div.style.left = (p.x + this.labelOffset.width) + "px";
		this.div.style.top = (p.y + this.labelOffset.height) + "px";
		this.div.style.zIndex = z + 1; // in front of the marker
	}

	// Remove the main DIV from the map pane, destroy event handlers
	LabeledMarker.prototype.remove = function() {
		GEvent.clearInstanceListeners(this.div);
		this.div.parentNode.removeChild(this.div);
		this.div = null;
		GMarker.prototype.remove.apply(this, arguments);
	}
}

function Converter()
{
	this.conversion = new Array();
	this.conversion['ft'] = new Array();
	this.conversion['ft']['ft'] = 1;
	this.conversion['ft']['meters'] = .3048;
	this.conversion['miles'] = new Array();
	this.conversion['miles']['miles'] = 1;
	this.conversion['miles']['km'] = 1.609344;
	this.conversion['miles']['meters'] = 1609.344;
	this.conversion['km'] = new Array();
	this.conversion['km']['km'] = 1;
	this.conversion['km']['miles'] = 0.621371192;
	this.conversion['meters'] = new Array();
	this.conversion['meters']['meters'] = 1;
	this.conversion['meters']['ft'] = 3.2808399;
	this.conversion['meters']['miles'] = 0.000621371192;
	this.conversion['kg'] = new Array();
	this.conversion['kg']['kg'] = 1;
	this.conversion['kg']['lbs'] = 2.20462262;
	this.conversion['lbs'] = new Array();
	this.conversion['lbs']['lbs'] = 1;
	this.conversion['lbs']['kg'] = 0.45359237;
	
	this.convert = function(value, from, to)
	{
		return value * this.conversion[from][to];
	}
}
