//
// Loads GPX/KML data into Ordnance Survey map
//

var zoom = 8;					// set initial scale to 50k
var radConst = Math.PI / 180;	// for converting angles to radians
var Km2Miles = 0.621371192;     // convert Km to miles
var m2Feet = 0.3048             // convert m to feet
var maxNorthing = 1300000       // OS max north grif
var maxEasting = 1300000        // OS max north grif

var numPoints = 0;				// counts how many points we have
var distance = 0;
var ascent = 0;
var descent = 0;
var elev = 0;					// elevation of a point in metres
var prevElev = 0;				// elevation of previous point in metres

var data;
var osMap;
var mapDescription = 'Facts'    // if provided in the data file
var mpInit;						// holds initial position for centering
var points = [];				// holds all the points along the track
var markerpoints = [];	        // holds any waypoint markers along the track
var errMsg = '';
var tileCount = '';

var sColor = "#FF0000";
var sOpacity = 1.0; 
var sWidth = 3;

if (fileName){

	// load the track data file as JSON using objTree.js to avoid dealing with hideous XML
	var xotree = new XML.ObjTree();

	// ensure tracks, track segments and routes are treated as arrays to help iteration
	xotree.force_array = ["trk", "trkseg","rte", "Placemark"];

	// we can now grap the data from the file
	data = xotree.parseHTTP(fileName,{});

}
addEvent(window,"load",init);

// page load
function init() { 	

        if (fileName){
    		if (data && prepareMapData(data)){
    			if (mpInit){
    
    				// we have an initial position, so centre the map and show it
    				displayMap();
    				// show the track overlay and related summary info, markers etc
    				if (osMap){
        				displayTrack();
        				getDistance();
        				displayInfoPanel();
        				displayMarkers();
    				}
    				else error('OS map problem. Cannot create map.');
    			}
    			else error('OS map problem. No initial position found.');
    		}
    		else error('Data parse problem. Cannot load map data.');
    	}         	
    	else {
    	
            if(coord){
    		// it's a search request from the gazeteer functions
    		displayMap(true);
    		var PostCodeMarker = osMap.createMarker(mpInit, null, document.title.toUpperCase()); 
		}
	} 
}  

// do basic sanity check on data then get the data points
function prepareMapData(data){

	if (data.gpx){
		getGPXDataPoints(data);
		return true;
	}
	if (data.kml) {
		getKMLDataPoints(data);
		return true;
	}
	return false;
}

// renders the map centered on the initial start point
function displayMap(isSearch) {
	if (isSearch){
		// it's a search, so get the returned coords from the gazeteer
		var eastNorth = coord.split(',');
		mpInit = new OpenSpace.MapPoint(parseInt(eastNorth[0]), parseInt(eastNorth[1]));
		zoom = 9;
	}
	else {
		// it's a GPX/KML file parse job
		if (points.length == 0){
			error('Sorry, but this is out of the range of Ordnance Survey maps.')
			return;
		}
	}

	// OK, we've got an initial position, so build the map
	osMap = new OpenSpace.Map('map');  

	// add the baby map window in the corner
	var control = new OpenSpace.Control.OverviewMap();
	osMap.addControl(control);
	control.maximizeControl();

	// show the map at the initial position and scale
	osMap.setCenter(mpInit, zoom); 
}

// obtain the data points from GPX data
function getGPXDataPoints(data){
	
//	GPX data as per http://www.topografix.com/GPX/1/1/ looks like:
//
//	Waypoint: used for single marker points
//	<wpt lat="nn" lon="nn">   
//		<ele>nn</ele>
//		<time>aa</time> 
//		<name>aa</name>
//		.. others 
//
//	Track: ride history as from GPS log	   
//  <trk>
//		<desc>
//		.. others ..
//		<trkseg>
//			<trkpt lat="nn" lon="nn">
//			.. as per wpt type ..
//
//	Route: proposed ride
//	<rte>
//		<desc>
//		.. others ..
//		<rtept lat="nn" lon="nn">
//		.. as per wpt type ..

	var tracks = false;
	var routes = false;
	var waypoints = false;

	if (data.gpx.trk) tracks = data.gpx.trk;
	if (data.gpx.rte) routes = data.gpx.rte
	if (data.gpx.wpt) waypoints = data.gpx.wpt;

	// only show Routes or Tracks, we unlikely to have both
	if (tracks){
		if (tracks[0].desc) {
			mapDescription = tracks[0].desc;
		}
		for (var i = 0; i < tracks.length; i++) {
			for (var j = 0; j < tracks[i].trkseg.length; j++) {
				for (var k = 0; k < tracks[i].trkseg[j].trkpt.length; k++){
					storeSinglePoint(tracks[i].trkseg[j].trkpt[k]);
				}
			}
		}
	}
	else if (routes){
		if (routes[0].desc) {
			mapDescription = routes[0].desc;
		}		
		for (var i = 0; i < routes.length; i++) {
			for (var j = 0; j < routes[i].rtept.length; j++) {
				storeSinglePoint(routes[i].rtept[j]);
			}
		}
	}

	// might also have marker points which must be stores as map points
	if (waypoints) {
		for (var i = 0; i < waypoints.length; i++) {
			// convert to Map Points
			var posn = new OpenLayers.LonLat(waypoints[i]['-lon'], waypoints[i]['-lat']);
			var gridProjection = new OpenSpace.GridProjection();
			var mp = gridProjection.getMapPointFromLonLat(posn);

			// convert mappoints to geographic point locations
			// and add to holding array for displaying
			eleName = (waypoints[i].name) ? (waypoints[i].name) : (null);
			var marker = new Marker(mp, eleName);		
			markerpoints.push(marker );
		}
	}
}


// stores route or track points and tallies up ascent & descent
function storeSinglePoint(wpt){

	if (!wpt) return;

	// get altitude data (meters) if present
	if (wpt.ele && (!isNaN(Number(wpt.ele)))){
		elev = Number(wpt.ele);
	}

	// convert to Map Points
	var posn = new OpenLayers.LonLat(wpt['-lon'], wpt['-lat']);
	var gridProjection = new OpenSpace.GridProjection();
	var mp = gridProjection.getMapPointFromLonLat(posn);

	// convert map points to geographic point locations
	// and add to holding array for displaying
	var gp = getOScoords(mp)
	if (gp) {
		points.push(gp);
	}

	if (numPoints == 0){
		mpInit = mp;
		if (elev) {
			prevElev = elev;
		}
	}
	else {
		// tally up the ascents and descents
		(elev < prevElev) ? (descent += prevElev - elev) : (ascent += elev - prevElev);
		if (elev) {
			prevElev = elev;
		}
	}
	numPoints++;
}

// constructor for Marker object
function Marker(oMapPoint, eleName) {
	this.mapPoint = oMapPoint;
	this.name = eleName;
}


// obtain the data points from GPX data
function getKMLDataPoints(data){

//	 Create overlay from KML data
//
//	simple KML data looks like:
//	<kml>
//		<Document> or <Folder>
//			<name>
//			<Placemark>
//				<LineString>		
//					<coordinates>lon1,lat1,ele1 lon2,lat2,ele2 ...etc</coordinates>

//	 It's also possible that coords may be stored with newlines instead of spaces, ie.
//	 lon1,lat1,ele1
//	 lon2,lat2,ele2 ...etc

	// initial string holding lats & lons from kml data
	var lc = '';        // line of coordinates
	var pm;             // Placemark
	var regex = /\n/g;  // for converting unwanted newlines to spaces

	if (data.kml.Document) {
		if (data.kml.Document.Placemark) {
			pm = data.kml.Document.Placemark;
		}
		if (data.kml.Document.name) {
			mapDescription = data.kml.Document.name;
		}	
	}
	if (data.kml.Folder) {
		if (data.kml.Folder.Placemark) {
			pm = data.kml.Folder.Placemark;
		}
		if (data.kml.Folder.name) {
			mapDescription = data.kml.Folder.name;
		}
	}

	// if we got the data OK, parse out the lat, lon & ele
	if (pm.length){
		for (i = 0; i < pm.length; i++){
			if (pm[i].LineString && pm[i].LineString.coordinates){
				// replace any newlines in some data files with spaces (for splitting coordinates on)
				lc += pm[i].LineString.coordinates.replace(regex, ' ');
			}
		}
	}

	if (!lc){
		error('Cannot find KML &lt;LineString&gt; or &lt;coordinates&gt;');
		return;
	}

	// create the points along the line for each <LineString> of every <Placemark>
	var gridProjection = new OpenSpace.GridProjection();
	var coords = lc.split(' ');
	var counter = 0;
	for (var i = 0; i < coords.length ; i++) {

		var c = coords[i].split(',');

		// should have lon, lat & altitude
		if (c.length == 3) {

			//convert to OS coords		
			lonlat = new OpenLayers.LonLat(c[0], c[1]);

			// create mappoints from OS coords
			var mp = gridProjection.getMapPointFromLonLat(lonlat);

			// get altitude data (meters) if present
			if (c[2] && (!isNaN(Number(c[2])))){
				elev = Number(c[2]);
			}

			// store initial position
			if (counter == 0){
				mpInit = mp;
				if (elev) {
					prevElev = elev;
				}
			}

			// tally up the ascents and descents
			(elev < prevElev) ? (descent += prevElev - elev) : (ascent += elev - prevElev);

			if (elev) {
				prevElev = elev;
			}

			// convert mappoints to geographic point locations
			// and add to holding array for displaying
			var gp = getOScoords(mp)
			if (gp) {
				points.push(gp);
			}
			counter++;
		}
	}
}

// work out total distance in Km
function getDistance(){
	if (points && points.length){
		for (var i = 1; i < points.length; i++) {
			distance +=  points[i].distanceTo(points[i-1]) / 1000;
		}
	}
}

// convert mappoints to geographic point locations & check data is within OS limits
function getOScoords(mp){
    var gp = 0;
	if ((mp.getNorthing() >= 0) && (mp.getNorthing() <= 1300000) && (mp.getEasting() >=0) && (mp.getEasting() <= 700000)) {
		gp = new OpenLayers.Geometry.Point(mp.getEasting(), mp.getNorthing());
	}
	return gp;
}

// render the track or route as a line of points
function displayTrack() {

	var vectorLayer = osMap.getVectorLayer();

	// create the line from the array of geographic points 
	var lineString = new OpenLayers.Geometry.LineString(points); 

	// build & apply the style for the line
	var linestyle = { 
		strokeColor: sColor, 
		strokeOpacity: sOpacity, 
		strokeWidth: sWidth 
	}; 

	var lineFeature = new OpenLayers.Feature.Vector(lineString, null, linestyle); 
	vectorLayer.addFeatures([lineFeature]); 
}

// show single waypoints as markers
function displayMarkers() {

	// add the start marker 
    var startMarker = osMap.createMarker(mpInit, null, 'Start Point'); 

	if (!markerpoints.length) return;
	for (i = 0; i < markerpoints.length; i++) {
		var content = markerpoints[i].name;
		var marker = osMap.createMarker(markerpoints[i].mapPoint, null, content);
	}
}

// populates the information panel with data 
function displayInfoPanel(){
	
	var bigZindex = '5000'; // force to display on top
 
	var info = "<p>" + mapDescription + "</p> <ul>"; 
	info += "<li>Distance: " + (distance * Km2Miles).toFixed(1) + " miles (" + distance.toFixed(1) + "Km)</li>";
	info += "<li>Ascent: " + Math.round(ascent/m2Feet) + " feet (" + Math.round(ascent) + " m)</li>";
	info += "<li>Descent: " + Math.round(descent/m2Feet) + " feet (" + Math.round(descent) + " m)</li>";
	info += "</ul><p><em>[click to hide]</em></p>";

	var infoPanel = document.getElementById('infopanel');
	infoPanel.innerHTML += info;
	infoPanel.style.display = 'block';
	infoPanel.style.zIndex = bigZindex;
    infoPanel.onclick = function(){this.style.display = 'none';}

	// check daily allowance
	var supportService = new OpenSpace.SupportService();
	supportService.getTileCount(function (tilesUsed, maxTiles){  		
		if (parseFloat(tilesUsed) >= parseFloat(maxTiles)){
			document.getElementById('infopanel').style.display = 'none';
			document.getElementById('map').style.display = 'none';
			error('The daily map allowance is used up. Please try again tomorrow.');
		}
	});
}

// displays error message in custom div
function error(msg){

	errMsg += '<li>' + msg + '</li>\n';
	var errDiv = document.getElementById('error');
	errDiv.innerHTML = '<h4>Error!</h4><ul>' + errMsg + '</ul>' ;
	errDiv.style.display = 'block';
}


// from http://javascript.crockford.com/remedial.html
// returns 'object','array','function','string','number','boolean','null','undefined' 
function typeOf(value) {    

	var s = typeof value;    
	if (s === 'object') {        
		if (value) {            
			if (value instanceof Array) {                
				s = 'array';            
			}        
		} 
		else {            
			s = 'null';        
		}    
	}    
	return s;
}
 

// -------------------------------------------------
// Event handling utility functions written by Dean Edwards, 2005
// with input from Tino Zijdel, Matthias Miller, Diego Perini
// http://dean.edwards.name/weblog/2005/10/add-event/

function addEvent(element, type, handler) {
	if (element.addEventListener) {
		element.addEventListener(type, handler, false);
	} else {
		// assign each event handler a unique ID
		if (!handler.$$guid) handler.$$guid = addEvent.guid++;
		// create a hash table of event types for the element
		if (!element.events) element.events = {};
		// create a hash table of event handlers for each element/event pair
		var handlers = element.events[type];
		if (!handlers) {
			handlers = element.events[type] = {};
			// store the existing event handler (if there is one)
			if (element["on" + type]) {
				handlers[0] = element["on" + type];
			}
		}
		// store the event handler in the hash table
		handlers[handler.$$guid] = handler;
		// assign a global event handler to do all the work
		element["on" + type] = handleEvent;
	}
};
// a counter used to create unique IDs
addEvent.guid = 1;

function removeEvent(element, type, handler) {
	if (element.removeEventListener) {
		element.removeEventListener(type, handler, false);
	} else {
		// delete the event handler from the hash table
		if (element.events && element.events[type]) {
			delete element.events[type][handler.$$guid];
		}
	}
};

function handleEvent(event) {
	var returnValue = true;
	// grab the event object (IE uses a global event object)
	event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
	// get a reference to the hash table of event handlers
	var handlers = this.events[event.type];
	// execute each event handler
	for (var i in handlers) {
		this.$$handleEvent = handlers[i];
		if (this.$$handleEvent(event) === false) {
			returnValue = false;
		}
	}
	return returnValue;
};

function fixEvent(event) {
	// add W3C standard event methods
	event.preventDefault = fixEvent.preventDefault;
	event.stopPropagation = fixEvent.stopPropagation;
	return event;
};
fixEvent.preventDefault = function() {
	this.returnValue = false;
};
fixEvent.stopPropagation = function() {
	this.cancelBubble = true;
};


